Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e14d062
Add VersionSpecificationType
AnatoliB Jul 25, 2019
5fc4f27
Add DependencyManifestEntry.VersionSpecificationType property
AnatoliB Jul 25, 2019
70788eb
Add IDependencyManagerStorage.IsModuleVersionInstalled
AnatoliB Jul 25, 2019
70fcaae
Fix test name
AnatoliB Jul 25, 2019
f60b8d4
Rename DependencyManifestEntry.MajorVersion to VersionSpecification
AnatoliB Jul 25, 2019
fb4c0cd
Pass VersionSpecificationType to DependencyManifestEntry ctor
AnatoliB Jul 25, 2019
23f2600
Handle VersionSpecificationType.ExactVersion in InstalledDependencies…
AnatoliB Jul 25, 2019
aefae60
Better fake version values
AnatoliB Jul 25, 2019
9f52701
Reorder methods
AnatoliB Jul 25, 2019
9748145
Rename DependencyInfo.LatestVersion to ExactVersion
AnatoliB Jul 25, 2019
e17c631
Don't try to determine latest published version if exact version is s…
AnatoliB Jul 25, 2019
a52d35c
Move local variable declaration
AnatoliB Jul 25, 2019
00b7def
Update InvalidVersionFormat message
AnatoliB Jul 25, 2019
2d0a7dc
Allow exact versions in the manifest
AnatoliB Jul 25, 2019
308c025
Extract CreateDependencyManifestEntry method
AnatoliB Jul 25, 2019
d88660c
Add an invalid version specification case
AnatoliB Jul 25, 2019
c52fe5c
Avoid parsing the version specification twice
AnatoliB Jul 25, 2019
a290ee1
Remove TODO
AnatoliB Jul 25, 2019
52e874f
Add a comment
AnatoliB Jul 25, 2019
0a7cae5
Inline unnecessary local variables
AnatoliB Jul 25, 2019
800107a
Simplify test data
AnatoliB Jul 25, 2019
b8b5cce
Remove outdated test: StartDependencyInstallationIfNeeded_InstallsSna…
AnatoliB Jul 25, 2019
d9876ad
Simplify test data
AnatoliB Jul 25, 2019
ca5e078
Add InstallsSpecifiedVersion_WhenExactVersionIsSpecified test
AnatoliB Jul 25, 2019
9ca28c5
Simplify tests
AnatoliB Jul 25, 2019
f9be712
Add LogsInstallationStartAndFinish test
AnatoliB Jul 25, 2019
2150583
Add PromotesInstallingSnapshotToInstalledAfterSuccessfullySavingModul…
AnatoliB Jul 25, 2019
951b082
Remove InstallsDependencySnapshots test
AnatoliB Jul 25, 2019
f2b9c23
Rename tests
AnatoliB Jul 25, 2019
4b83c08
Reorder tests
AnatoliB Jul 25, 2019
66f1099
Get rid of _testDependencyManifestEntries and _testLatestPublishedMod…
AnatoliB Jul 25, 2019
81b2ad3
Add CleansUpPowerShellRunspaceAfterSuccessfullySavingModule test
AnatoliB Jul 25, 2019
b93b8e3
Verify no other calls in DoesNothingOnConstruction test
AnatoliB Jul 25, 2019
0586d22
Remove unnecessary SaveModule setups
AnatoliB Jul 25, 2019
66e7c31
Extract ExpectSnapshotCreationAndPromotion method
AnatoliB Jul 25, 2019
60964d4
Compact formatting
AnatoliB Jul 25, 2019
f559f9e
Add comments
AnatoliB Jul 25, 2019
a113269
Remove AAA comments
AnatoliB Jul 25, 2019
0359023
Fix formatting
AnatoliB Jul 25, 2019
df2bcca
Move ExpectSnapshotCreationAndPromotion invocation to the ctor
AnatoliB Jul 25, 2019
9c2f686
Inline ExpectSnapshotCreationAndPromotion
AnatoliB Jul 25, 2019
c4f7f26
Rename test
AnatoliB Jul 25, 2019
d1166e1
Inline local variables
AnatoliB Jul 30, 2019
6432e86
Extract majorVersion variable
AnatoliB Jul 30, 2019
b45f30c
Improve a comment
AnatoliB Jul 30, 2019
231ff24
Allow installing explicitly specified prerelease versions
AnatoliB Jul 30, 2019
45a0243
Fix AllowPrerelease value
AnatoliB Jul 30, 2019
5de92a0
Update design doc
AnatoliB Jul 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/designs/PowerShell-AzF-Overall-Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,20 @@ Note that, checking out a PowerShell Manager instance from the pool is a blockin

The goal is to let the user declare the dependencies required by functions, and rely on the service automatically locating and installing the dependencies from the PowerShell Gallery or other sources, taking care of selecting the proper versions, and automatically upgrading the dependencies to the latest versions (if allowed by the version specifications provided by the user).

Dependencies are declared in the _requirements.psd1_ file (_manifest_) as a collection of pairs (<_name_>, <_version specification_>). Currently, the version specification should strictly match the following pattern: `<major version>.*`, so a typical manifest looks like this:
Dependencies are declared in the _requirements.psd1_ file (_manifest_) as a collection of pairs (<_name_>, <_version specification_>). Currently, the version specification should either be an exact and complete version, or strictly match the following pattern: `<major version>.*`. So, a typical manifest may look like this:

``` PowerShell
@{
'Az' = '2.*'
'PSDepend' = '0.*'
'Pester' = '5.0.0-alpha3'
}
```

When the `<major version>.*` format is used, the worker will retrieve the latest available module version (within the specified major version) from the PowerShell Gallery, ignoring prerelease versions.

When the exact version is specified, the worker will retrieve the specified version only, ignoring any other version. Prerelease versions are allowed in this case.

The number of entries in the _requirements.psd1_ file should not exceed **10**. This limit is not user-configurable.

Installing and upgrading dependencies should be performed automatically, without requiring any interaction with the user, and without interfering with the currently running functions. This represents an important design challenge. In a different context, dependencies could be stored on a single location on the file system, managed by regular PowerShell tools (`Install-Module`/`Save-Module`, `PSDepend`, etc.), while having the same file system location added to _PSModulePath_ to make all the modules available to scripts running on this machine. This is what PowerShell users normally do, and this approach looks attractive because it is simple and conventional. However, in the contexts where multiple independent workers load modules and execute scripts concurrently, and at the same time some module versions are being added, upgraded, or removed, this simple approach causes many known problems. The root causes of these problems are in the fundamentals of PowerShell and PowerShell modules design. The managed dependencies design in Azure Functions must take this into account. The problems will be solved if we satisfy the following conditions:
Expand Down
6 changes: 3 additions & 3 deletions src/DependencyManagement/DependencyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
internal class DependencyInfo
{
internal readonly string Name;
internal readonly string LatestVersion;
internal readonly string ExactVersion;

internal DependencyInfo(string name, string latestVersion)
internal DependencyInfo(string name, string exactVersion)
{
Name = name;
LatestVersion = latestVersion;
ExactVersion = exactVersion;
}
}
}
6 changes: 6 additions & 0 deletions src/DependencyManagement/DependencyManagerStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public IEnumerable<string> GetInstalledModuleVersions(string snapshotPath, strin
return Directory.EnumerateDirectories(modulePath, $"{majorVersion}.*");
}

public bool IsModuleVersionInstalled(string snapshotPath, string moduleName, string version)
{
var moduleVersionPath = Path.Join(snapshotPath, moduleName, version);
return Directory.Exists(moduleVersionPath);
}

public string CreateNewSnapshotPath()
{
return Path.Join(
Expand Down
81 changes: 43 additions & 38 deletions src/DependencyManagement/DependencyManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,52 @@ public IEnumerable<DependencyManifestEntry> GetEntries()

foreach (DictionaryEntry entry in hashtable)
{
// A valid entry is of the form: 'ModuleName'='MajorVersion.*"
var name = (string)entry.Key;
var version = (string)entry.Value;
// A valid entry is of the form:
// 'ModuleName'='MajorVersion.*'
// or
// 'ModuleName'='ExactVersion'

yield return CreateDependencyManifestEntry(
name: (string)entry.Key,
version: (string)entry.Value);
}
}

private static DependencyManifestEntry CreateDependencyManifestEntry(string name, string version)
{
ValidateModuleName(name);

ValidateModuleName(name);
var match = Regex.Match(version, @"^(\d+)(.*)");
if (match.Success)
{
// Look for the 'MajorVersion.*' pattern first.
var majorVersion = match.Groups[1].Value;
var afterMajorVersion = match.Groups[2].Value;
if (afterMajorVersion == ".*")
{
return new DependencyManifestEntry(
name,
VersionSpecificationType.MajorVersion,
majorVersion);
}

yield return new DependencyManifestEntry(name, GetMajorVersion(version));
// At this point, we know this is not the 'MajorVersion.*' pattern.
// We want to perform a very basic sanity check of the format to detect some
// obviously wrong cases: make sure afterMajorVersion starts with a dot,
// does not contain * anywhere, and ends with a word character.
// Not even trying to match the actual version format rules,
// as they are quite complex and controlled by the server side anyway.
if (Regex.IsMatch(afterMajorVersion, @"^(\.[^\*]*?\w)?$"))
{
return new DependencyManifestEntry(
name,
VersionSpecificationType.ExactVersion,
version);
}
}

var errorMessage = string.Format(PowerShellWorkerStrings.InvalidVersionFormat, version, "MajorVersion.*");
throw new ArgumentException(errorMessage);
}

/// <summary>
Expand Down Expand Up @@ -106,38 +144,5 @@ private static void ValidateModuleName(string name)
throw new ArgumentException(PowerShellWorkerStrings.DependencyNameIsNullOrEmpty);
}
}

/// <summary>
/// Parses the given string version and extracts the major version.
/// Please note that the only version we currently support is of the form '1.*'.
/// </summary>
private static string GetMajorVersion(string version)
{
ValidateVersionFormat(version);
return version.Split(".")[0];
}

private static void ValidateVersionFormat(string version)
{
if (version == null)
{
throw new ArgumentNullException(version);
}

if (!IsValidVersionFormat(version))
{
var errorMessage = string.Format(PowerShellWorkerStrings.InvalidVersionFormat, "MajorVersion.*");
throw new ArgumentException(errorMessage);
}
}

/// <summary>
/// Validates the given version format. Currently, we only support 'Number.*'.
/// </summary>
private static bool IsValidVersionFormat(string version)
{
var pattern = @"^(\d)+(\.)(\*)";
return Regex.IsMatch(version, pattern);
}
}
}
12 changes: 9 additions & 3 deletions src/DependencyManagement/DependencyManifestEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ internal class DependencyManifestEntry
{
public string Name { get; }

public string MajorVersion { get; }
public VersionSpecificationType VersionSpecificationType { get; }

public DependencyManifestEntry(string name, string majorVersion)
public string VersionSpecification { get; }

public DependencyManifestEntry(
string name,
VersionSpecificationType versionSpecificationType,
string versionSpecification)
{
Name = name;
MajorVersion = majorVersion;
VersionSpecificationType = versionSpecificationType;
VersionSpecification = versionSpecification;
}
}
}
34 changes: 22 additions & 12 deletions src/DependencyManagement/DependencySnapshotInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,27 @@ public void InstallSnapshot(

try
{
foreach (DependencyInfo module in GetLatestPublishedVersionsOfDependencies(dependencies))
foreach (DependencyInfo module in GetExactVersionsOfDependencies(dependencies))
{
string moduleName = module.Name;
string latestVersion = module.LatestVersion;

logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.StartedInstallingModule, moduleName, latestVersion));
logger.Log(isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.StartedInstallingModule, module.Name, module.ExactVersion));

int tries = 1;

while (true)
{
try
{
_moduleProvider.SaveModule(pwsh, moduleName, latestVersion, installingPath);
_moduleProvider.SaveModule(pwsh, module.Name, module.ExactVersion, installingPath);

var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, latestVersion);
var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, module.Name, module.ExactVersion);
logger.Log(isUserOnlyLog: false, LogLevel.Trace, message);

break;
}
catch (Exception e)
{
string currentAttempt = GetCurrentAttemptMessage(tries);
var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, moduleName, latestVersion, currentAttempt, e.Message);
var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, module.Name, module.ExactVersion, currentAttempt, e.Message);
logger.Log(isUserOnlyLog: false, LogLevel.Error, errorMsg);

if (tries >= MaxNumberOfTries)
Expand Down Expand Up @@ -127,22 +124,35 @@ internal static string GetCurrentAttemptMessage(int attempt)
}
}

private List<DependencyInfo> GetLatestPublishedVersionsOfDependencies(
private List<DependencyInfo> GetExactVersionsOfDependencies(
IEnumerable<DependencyManifestEntry> dependencies)
{
var result = new List<DependencyInfo>();

foreach (var entry in dependencies)
{
var latestVersion = GetModuleLatestPublishedVersion(entry.Name, entry.MajorVersion);

var dependencyInfo = new DependencyInfo(entry.Name, latestVersion);
var dependencyInfo = new DependencyInfo(entry.Name, GetExactVersion(entry));
result.Add(dependencyInfo);
}

return result;
}

private string GetExactVersion(DependencyManifestEntry entry)
{
switch (entry.VersionSpecificationType)
{
case VersionSpecificationType.ExactVersion:
return entry.VersionSpecification;

case VersionSpecificationType.MajorVersion:
return GetModuleLatestPublishedVersion(entry.Name, entry.VersionSpecification);

default:
throw new ArgumentException($"Unknown version specification type: {entry.VersionSpecificationType}");
}
}

/// <summary>
/// Gets the latest published module version for the given module name and major version.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyManagement/IDependencyManagerStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal interface IDependencyManagerStorage

IEnumerable<string> GetInstalledModuleVersions(string snapshotPath, string moduleName, string majorVersion);

bool IsModuleVersionInstalled(string snapshotPath, string moduleName, string version);

string CreateNewSnapshotPath();

string CreateInstallingSnapshot(string path);
Expand Down
25 changes: 20 additions & 5 deletions src/DependencyManagement/InstalledDependenciesLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
{
using System;
using System.Linq;

internal class InstalledDependenciesLocator : IInstalledDependenciesLocator
Expand All @@ -23,7 +24,7 @@ public string GetPathWithAcceptableDependencyVersionsInstalled()
if (lastSnapshotPath != null)
{
var dependencies = _storage.GetDependencies();
if (dependencies.All(entry => IsMajorVersionInstalled(lastSnapshotPath, entry)))
if (dependencies.All(entry => IsAcceptableVersionInstalled(lastSnapshotPath, entry)))
{
return lastSnapshotPath;
}
Expand All @@ -32,12 +33,26 @@ public string GetPathWithAcceptableDependencyVersionsInstalled()
return null;
}

private bool IsMajorVersionInstalled(string snapshotPath, DependencyManifestEntry dependency)
private bool IsAcceptableVersionInstalled(string snapshotPath, DependencyManifestEntry dependency)
{
var installedVersions =
_storage.GetInstalledModuleVersions(
snapshotPath, dependency.Name, dependency.MajorVersion);
switch (dependency.VersionSpecificationType)
{
case VersionSpecificationType.ExactVersion:
return _storage.IsModuleVersionInstalled(
snapshotPath, dependency.Name, dependency.VersionSpecification);

case VersionSpecificationType.MajorVersion:
return IsMajorVersionInstalled(
snapshotPath, dependency.Name, dependency.VersionSpecification);

default:
throw new ArgumentException($"Unknown version specification type: {dependency.VersionSpecificationType}");
}
}

private bool IsMajorVersionInstalled(string snapshotPath, string name, string majorVersion)
{
var installedVersions = _storage.GetInstalledModuleVersions(snapshotPath, name, majorVersion);
return installedVersions.Any();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public void SaveModule(PowerShell pwsh, string moduleName, string version, strin
.AddParameter("Repository", Repository)
.AddParameter("Name", moduleName)
.AddParameter("RequiredVersion", version)
.AddParameter("AllowPrerelease", Utils.BoxedTrue)
.AddParameter("Path", path)
.AddParameter("Force", Utils.BoxedTrue)
.AddParameter("ErrorAction", "Stop")
Expand Down
15 changes: 15 additions & 0 deletions src/DependencyManagement/VersionSpecificationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

#pragma warning disable 1591 // Missing XML comment for publicly visible type or member 'member'

namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
{
public enum VersionSpecificationType
{
ExactVersion,
MajorVersion
}
}
2 changes: 1 addition & 1 deletion src/resources/PowerShellWorkerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
<value>The PowerShell data file '{0}' is invalid since it cannot be evaluated into a Hashtable object.</value>
</data>
<data name="InvalidVersionFormat" xml:space="preserve">
<value>Version is not in the correct format. Please use the following notation: '{0}'</value>
<value>Version specification '{0}' is not in the correct format. Please specify the exact version or use the following notation: '{1}'</value>
</data>
<data name="FailToInstallFuncAppDependencies" xml:space="preserve">
<value>Fail to install FunctionApp dependencies. Error: '{0}'</value>
Expand Down
6 changes: 3 additions & 3 deletions test/Unit/DependencyManagement/DependencyManagementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ public void TestManagedDependencyInvalidRequirementsFormatShouldThrow()
var exception = Assert.Throws<DependencyInstallationException>(
() => { dependencyManager.Initialize(_testLogger); });

Assert.Contains("Version is not in the correct format.", exception.Message);
Assert.Contains("Please use the following notation:", exception.Message);
Assert.Contains("MajorVersion.*", exception.Message);
Assert.Contains("not in the correct format.", exception.Message);
Assert.Contains("1.0.*", exception.Message);
Assert.Contains("Please specify the exact version or use the following notation: 'MajorVersion.*'", exception.Message);
}
}

Expand Down
Loading