diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index ce4785ca..28f4c59b 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -55,7 +55,7 @@ public DependencyManager( ILogger logger = null) { _storage = storage ?? new DependencyManagerStorage(GetFunctionAppRootPath(requestMetadataDirectory)); - _installedDependenciesLocator = installedDependenciesLocator ?? new InstalledDependenciesLocator(_storage); + _installedDependenciesLocator = installedDependenciesLocator ?? new InstalledDependenciesLocator(_storage, logger); var snapshotContentLogger = new PowerShellModuleSnapshotLogger(); _installer = installer ?? new DependencySnapshotInstaller( moduleProvider ?? new PowerShellGalleryModuleProvider(logger), diff --git a/src/DependencyManagement/DependencyManagerStorage.cs b/src/DependencyManagement/DependencyManagerStorage.cs index 833f5db3..514d6664 100644 --- a/src/DependencyManagement/DependencyManagerStorage.cs +++ b/src/DependencyManagement/DependencyManagerStorage.cs @@ -24,8 +24,7 @@ public DependencyManagerStorage(string functionAppRootPath) public IEnumerable GetDependencies() { - var dependencyManifest = new DependencyManifest(_functionAppRootPath); - return dependencyManifest.GetEntries(); + return GetAppDependencyManifest().GetEntries(); } public bool SnapshotExists(string path) @@ -127,6 +126,30 @@ public DateTime GetSnapshotAccessTimeUtc(string path) return heartbeatLastWrite >= snapshotCreation ? heartbeatLastWrite : snapshotCreation; } + public void PreserveDependencyManifest(string path) + { + var source = GetAppDependencyManifest().GetPath(); + var destination = Path.Join(path, Path.GetFileName(source)); + File.Copy(source, destination, overwrite: true); + } + + public bool IsEquivalentDependencyManifest(string path) + { + var source = GetAppDependencyManifest().GetPath(); + if (!File.Exists(source)) + { + return false; + } + + var destination = Path.Join(path, Path.GetFileName(source)); + if (!File.Exists(destination)) + { + return false; + } + + return File.ReadAllText(source) == File.ReadAllText(destination); + } + private IEnumerable GetInstalledSnapshots() { if (!Directory.Exists(_managedDependenciesRootPath)) @@ -138,5 +161,10 @@ private IEnumerable GetInstalledSnapshots() _managedDependenciesRootPath, DependencySnapshotFolderNameTools.InstalledPattern); } + + private DependencyManifest GetAppDependencyManifest() + { + return new DependencyManifest(_functionAppRootPath); + } } } diff --git a/src/DependencyManagement/DependencyManifest.cs b/src/DependencyManagement/DependencyManifest.cs index 070af254..b1c472cc 100644 --- a/src/DependencyManagement/DependencyManifest.cs +++ b/src/DependencyManagement/DependencyManifest.cs @@ -32,6 +32,11 @@ public DependencyManifest(string functionAppRootPath, int maxDependencyEntries = _maxDependencyEntries = maxDependencyEntries; } + public string GetPath() + { + return Path.Combine(_functionAppRootPath, RequirementsPsd1FileName); + } + public IEnumerable GetEntries() { var hashtable = ParsePowerShellDataFile(); @@ -93,7 +98,7 @@ private static DependencyManifestEntry CreateDependencyManifestEntry(string name private Hashtable ParsePowerShellDataFile() { // Path to requirements.psd1 file. - var requirementsFilePath = Path.Join(_functionAppRootPath, RequirementsPsd1FileName); + var requirementsFilePath = GetPath(); if (!File.Exists(requirementsFilePath)) { diff --git a/src/DependencyManagement/DependencySnapshotInstaller.cs b/src/DependencyManagement/DependencySnapshotInstaller.cs index 7c3520aa..5e687770 100644 --- a/src/DependencyManagement/DependencySnapshotInstaller.cs +++ b/src/DependencyManagement/DependencySnapshotInstaller.cs @@ -108,7 +108,9 @@ private string CreateInstallingSnapshot(string path) { try { - return _storage.CreateInstallingSnapshot(path); + var installingPath = _storage.CreateInstallingSnapshot(path); + _storage.PreserveDependencyManifest(installingPath); + return installingPath; } catch (Exception e) { diff --git a/src/DependencyManagement/IDependencyManagerStorage.cs b/src/DependencyManagement/IDependencyManagerStorage.cs index edbb4d91..21747bbb 100644 --- a/src/DependencyManagement/IDependencyManagerStorage.cs +++ b/src/DependencyManagement/IDependencyManagerStorage.cs @@ -37,5 +37,9 @@ internal interface IDependencyManagerStorage void SetSnapshotAccessTimeToUtcNow(string path); DateTime GetSnapshotAccessTimeUtc(string path); + + void PreserveDependencyManifest(string path); + + bool IsEquivalentDependencyManifest(string path); } } diff --git a/src/DependencyManagement/InstalledDependenciesLocator.cs b/src/DependencyManagement/InstalledDependenciesLocator.cs index c2f86080..7373cc2a 100644 --- a/src/DependencyManagement/InstalledDependenciesLocator.cs +++ b/src/DependencyManagement/InstalledDependenciesLocator.cs @@ -7,28 +7,46 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement { using System; using System.Linq; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + using static Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types; internal class InstalledDependenciesLocator : IInstalledDependenciesLocator { private readonly IDependencyManagerStorage _storage; - public InstalledDependenciesLocator(IDependencyManagerStorage storage) + private readonly ILogger _logger; + + public InstalledDependenciesLocator(IDependencyManagerStorage storage, ILogger logger) { - _storage = storage; + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public string GetPathWithAcceptableDependencyVersionsInstalled() { var lastSnapshotPath = _storage.GetLatestInstalledSnapshot(); - if (lastSnapshotPath != null) + if (lastSnapshotPath == null) + { + _logger.Log(isUserOnlyLog: false, Level.Information, string.Format(PowerShellWorkerStrings.NoInstalledDependencySnapshot, lastSnapshotPath)); + return null; + } + + _logger.Log(isUserOnlyLog: false, Level.Information, string.Format(PowerShellWorkerStrings.LastInstalledDependencySnapshotFound, lastSnapshotPath)); + + if (_storage.IsEquivalentDependencyManifest(lastSnapshotPath)) + { + _logger.Log(isUserOnlyLog: false, Level.Information, string.Format(PowerShellWorkerStrings.EquivalentDependencySnapshotManifest, lastSnapshotPath)); + return lastSnapshotPath; + } + + var dependencies = _storage.GetDependencies(); + if (dependencies.All(entry => IsAcceptableVersionInstalled(lastSnapshotPath, entry))) { - var dependencies = _storage.GetDependencies(); - if (dependencies.All(entry => IsAcceptableVersionInstalled(lastSnapshotPath, entry))) - { - return lastSnapshotPath; - } + _logger.Log(isUserOnlyLog: false, Level.Information, string.Format(PowerShellWorkerStrings.DependencySnapshotContainsAcceptableModuleVersions, lastSnapshotPath)); + return lastSnapshotPath; } + _logger.Log(isUserOnlyLog: false, Level.Information, string.Format(PowerShellWorkerStrings.DependencySnapshotDoesNotContainAcceptableModuleVersions, lastSnapshotPath)); return null; } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 8baf1238..ff966b56 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -337,4 +337,19 @@ Number of dependency snapshots installed: '{0}'. Dependency snapshots to keep: '{1}'. + + No installed dependency snapshot found. + + + Last installed dependency snapshot found: '{0}'. + + + Dependency snapshot '{0}' manifest is equivalent to the current manifest. + + + Dependency snapshot '{0}' contains acceptable module versions. + + + Dependency snapshot '{0}' does not contain acceptable module versions. + \ No newline at end of file diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs index e176ed79..401334df 100644 --- a/test/Unit/DependencyManagement/DependencyManagementTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagementTests.cs @@ -221,7 +221,7 @@ public void TestManagedDependencySuccessfulModuleDownload() var mockModuleProvider = new MockModuleProvider { SuccessfulDownload = true }; // Create DependencyManager and process the requirements.psd1 file at the function app root. - using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, mockModuleProvider)) + using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, mockModuleProvider, logger: _testLogger)) { dependencyManager.Initialize(_testLogger); @@ -268,7 +268,7 @@ public void TestManagedDependencySuccessfulModuleDownloadAfterTwoTries() var mockModuleProvider = new MockModuleProvider { ShouldNotThrowAfterCount = 2 }; // Create DependencyManager and process the requirements.psd1 file at the function app root. - using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, mockModuleProvider)) + using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, mockModuleProvider, logger: _testLogger)) { dependencyManager.Initialize(_testLogger); @@ -325,7 +325,7 @@ public void TestManagedDependencyRetryLogicMaxNumberOfTries() var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); // Create DependencyManager and process the requirements.psd1 file at the function app root. - using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, new MockModuleProvider())) + using (var dependencyManager = new DependencyManager(functionLoadRequest.Metadata.Directory, new MockModuleProvider(), logger: _testLogger)) { dependencyManager.Initialize(_testLogger); @@ -382,7 +382,8 @@ public void FunctionAppExecutionShouldStopIfNoPreviousDependenciesAreInstalled() // the PSGallery to retrieve the latest module version using (var dependencyManager = new DependencyManager( functionLoadRequest.Metadata.Directory, - new MockModuleProvider { GetLatestModuleVersionThrows = true })) + new MockModuleProvider { GetLatestModuleVersionThrows = true }, + logger: _testLogger)) { dependencyManager.Initialize(_testLogger); dependencyManager.StartDependencyInstallationIfNeeded(PowerShell.Create(), PowerShell.Create, _testLogger); @@ -417,7 +418,8 @@ public void FunctionAppExecutionShouldContinueIfPreviousDependenciesExist() // the PSGallery to retrive the latest module version using (var dependencyManager = new DependencyManager( functionLoadRequest.Metadata.Directory, - new MockModuleProvider { GetLatestModuleVersionThrows = true })) + new MockModuleProvider { GetLatestModuleVersionThrows = true }, + logger: _testLogger)) { // Create a path to mimic an existing installation of the Az module AzModulePath = Path.Join(managedDependenciesFolderPath, "FakeDependenciesSnapshot", "Az"); diff --git a/test/Unit/DependencyManagement/DependencyManagerTests.cs b/test/Unit/DependencyManagement/DependencyManagerTests.cs index b4eb5ecb..e6249468 100644 --- a/test/Unit/DependencyManagement/DependencyManagerTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagerTests.cs @@ -313,7 +313,8 @@ private DependencyManager CreateDependencyManagerWithMocks() installer: _mockInstaller.Object, newerSnapshotDetector: _mockNewerDependencySnapshotDetector.Object, maintainer: _mockBackgroundDependencySnapshotMaintainer.Object, - currentSnapshotContentLogger: _mockBackgroundDependencySnapshotContentLogger.Object); + currentSnapshotContentLogger: _mockBackgroundDependencySnapshotContentLogger.Object, + logger: _mockLogger.Object); } } } diff --git a/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs b/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs index 415c1d04..5de1e3ea 100644 --- a/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs +++ b/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs @@ -34,6 +34,7 @@ public DependencySnapshotInstallerTests() _targetPathInstalled = DependencySnapshotFolderNameTools.CreateUniqueName(); _targetPathInstalling = DependencySnapshotFolderNameTools.ConvertInstalledToInstalling(_targetPathInstalled); _mockStorage.Setup(_ => _.CreateInstallingSnapshot(_targetPathInstalled)).Returns(_targetPathInstalling); + _mockStorage.Setup(_ => _.PreserveDependencyManifest(_targetPathInstalling)); _mockStorage.Setup(_ => _.PromoteInstallingSnapshotToInstalledAtomically(_targetPathInstalled)); _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns(default(string)); } diff --git a/test/Unit/DependencyManagement/InstalledDependenciesLocatorTests.cs b/test/Unit/DependencyManagement/InstalledDependenciesLocatorTests.cs index 684df2b4..326591ad 100644 --- a/test/Unit/DependencyManagement/InstalledDependenciesLocatorTests.cs +++ b/test/Unit/DependencyManagement/InstalledDependenciesLocatorTests.cs @@ -9,10 +9,12 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.DependencyManagement using Xunit; using Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; public class InstalledDependenciesLocatorTests { private readonly Mock _mockStorage = new Mock(MockBehavior.Strict); + private readonly Mock _mockLogger = new Mock(); private readonly DependencyManifestEntry[] _dependencyManifestEntries = { @@ -21,18 +23,31 @@ public class InstalledDependenciesLocatorTests }; [Fact] - public void ReturnsLatestSnapshotPath_WhenAllDependenciesHaveAcceptableVersionInstalled() + public void ReturnsLatestSnapshotPath_WhenSnapshotWithEquivalentDependencyManifestInstalled() { // Even though multiple snapshots can be currently installed, only the latest one will be considered // (determined by name). _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns("snapshot"); + _mockStorage.Setup(_ => _.IsEquivalentDependencyManifest("snapshot")).Returns(true); - _mockStorage.Setup(_ => _.GetDependencies()).Returns(_dependencyManifestEntries); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); + var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); + Assert.Equal("snapshot", result); + } + + [Fact] + public void ReturnsLatestSnapshotPath_WhenAllDependenciesHaveAcceptableVersionInstalled() + { + // Even though multiple snapshots can be currently installed, only the latest one will be considered + // (determined by name). + _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns("snapshot"); + _mockStorage.Setup(_ => _.IsEquivalentDependencyManifest("snapshot")).Returns(false); + _mockStorage.Setup(_ => _.GetDependencies()).Returns(_dependencyManifestEntries); _mockStorage.Setup(_ => _.IsModuleVersionInstalled("snapshot", "A", "exact version of A")).Returns(true); _mockStorage.Setup(_ => _.GetInstalledModuleVersions("snapshot", "B", "major version of B")).Returns(new [] { "exact version of B" }); - var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); Assert.Equal("snapshot", result); @@ -43,7 +58,7 @@ public void ReturnsNull_WhenNoInstalledDependencySnapshotsFound() { _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns(default(string)); - var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); Assert.Null(result); @@ -55,14 +70,14 @@ public void ReturnsNull_WhenNoMajorVersionInstalled() // Even though multiple snapshots can be currently installed, only the latest one will be considered // (determined by name). _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns("snapshot"); - + _mockStorage.Setup(_ => _.IsEquivalentDependencyManifest("snapshot")).Returns(false); _mockStorage.Setup(_ => _.GetDependencies()).Returns(_dependencyManifestEntries); // No version for module B detected! _mockStorage.Setup(_ => _.IsModuleVersionInstalled("snapshot", "A", "exact version of A")).Returns(true); _mockStorage.Setup(_ => _.GetInstalledModuleVersions("snapshot", "B", "major version of B")).Returns(new string[0]); - var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); Assert.Null(result); @@ -74,14 +89,14 @@ public void ReturnsNull_WhenExactModuleVersionIsNotInstalled() // Even though multiple snapshots can be currently installed, only the latest one will be considered // (determined by name). _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns("snapshot"); - + _mockStorage.Setup(_ => _.IsEquivalentDependencyManifest("snapshot")).Returns(false); _mockStorage.Setup(_ => _.GetDependencies()).Returns(_dependencyManifestEntries); // The specified module A version is not installed _mockStorage.Setup(_ => _.IsModuleVersionInstalled("snapshot", "A", "exact version of A")).Returns(false); _mockStorage.Setup(_ => _.GetInstalledModuleVersions("snapshot", "B", "major version of B")).Returns(new [] { "exact version of B" }); - var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); Assert.Null(result); @@ -106,16 +121,16 @@ public void ReturnsLatestSnapshotPath_WhenPreviewVersionInstalled(string postfix new DependencyManifestEntry("A", VersionSpecificationType.ExactVersion, fullVersion) }; - _mockStorage.Setup(_ => _.GetDependencies()).Returns(dependencyManifestEntries); - _mockStorage.Setup(_ => _.GetLatestInstalledSnapshot()).Returns("snapshot"); + _mockStorage.Setup(_ => _.IsEquivalentDependencyManifest("snapshot")).Returns(false); + _mockStorage.Setup(_ => _.GetDependencies()).Returns(dependencyManifestEntries); // No exact match... _mockStorage.Setup(_ => _.IsModuleVersionInstalled("snapshot", "A", fullVersion)).Returns(false); // ...but the base version is here _mockStorage.Setup(_ => _.IsModuleVersionInstalled("snapshot", "A", baseVersion)).Returns(true); - var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object); + var installedDependenciesLocator = new InstalledDependenciesLocator(_mockStorage.Object, _mockLogger.Object); var result = installedDependenciesLocator.GetPathWithAcceptableDependencyVersionsInstalled(); Assert.Equal("snapshot", result);