From 72791b73f55449f36df92e0e617006300474558e Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Thu, 6 Jun 2019 14:57:26 -0700 Subject: [PATCH 1/8] Fixing faulty logic in InstallFunctionAppDependencies and refactoring DependencyManager.cs to accommodate for unit test. --- src/DependencyManagement/DependencyManager.cs | 111 ++++++++++++------ src/resources/PowerShellWorkerStrings.resx | 8 +- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 94fe871b..03e508f9 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -60,10 +60,10 @@ internal class DependencyManager // Managed Dependencies folder name. private const string ManagedDependenciesFolderName = "ManagedDependencies"; - //Set when any error occurs while downloading dependencies + // Set when any error occurs while downloading dependencies private Exception _dependencyError; - //Dependency download task + // Dependency download task private Task _dependencyDownloadTask; // This flag is used to figure out if we need to install/reinstall all the function app dependencies. @@ -73,6 +73,9 @@ internal class DependencyManager // Maximum number of tries for retry logic when installing function app dependencies. private const int MaxNumberOfTries = 3; + // Save-Module cmdlet name + private const string SaveModuleCmdletName = "PowerShellGet\\Save-Module"; + internal DependencyManager() { Dependencies = new List(); @@ -192,25 +195,15 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) // Install the function dependencies. logger.Log(LogLevel.Trace, PowerShellWorkerStrings.InstallingFunctionAppDependentModules, isUserLog: true); - if (Directory.Exists(DependenciesPath)) + try { - // Save-Module supports downloading side-by-size module versions. However, we only want to keep one version at the time. - // If the ManagedDependencies folder exits, remove all its contents. - DependencyManagementUtils.EmptyDirectory(DependenciesPath); + SetDependenciesDestinationPath(DependenciesPath); } - else + catch (Exception e) { - // If the destination path does not exist, create it. - // If the user does not have write access to the path, an exception will be raised. - try - { - Directory.CreateDirectory(DependenciesPath); - } - catch (Exception e) - { - var message = string.Format(PowerShellWorkerStrings.FailToCreateFunctionAppDependenciesDestinationPath, DependenciesPath, e.Message); - logger.Log(LogLevel.Trace, message, isUserLog: true); - } + logger.Log(LogLevel.Error, e.Message, isUserLog: true); + _dependencyError = new DependencyInstallationException(e.Message, e); + return; } try @@ -227,14 +220,7 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) try { // Save the module to the given path - pwsh.AddCommand("PowerShellGet\\Save-Module") - .AddParameter("Repository", Repository) - .AddParameter("Name", moduleName) - .AddParameter("RequiredVersion", latestVersion) - .AddParameter("Path", DependenciesPath) - .AddParameter("Force", Utils.BoxedTrue) - .AddParameter("ErrorAction", "Stop") - .InvokeAndClearCommands(); + RunSaveModuleCommand(pwsh, Repository, moduleName, latestVersion, DependenciesPath); var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, latestVersion); logger.Log(LogLevel.Trace, message, isUserLog: true); @@ -243,17 +229,14 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) } catch (Exception e) { + var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, moduleName, latestVersion, e.Message); + logger.Log(LogLevel.Error, errorMsg, isUserLog: true); + if (tries >= MaxNumberOfTries) { - var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependencies, e.Message); + errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependencies, e.Message); _dependencyError = new DependencyInstallationException(errorMsg, e); - - throw _dependencyError; - } - else - { - var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependency, moduleName, latestVersion, e.Message); - logger.Log(LogLevel.Error, errorMsg, isUserLog: true); + return; } } @@ -269,16 +252,66 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) finally { // Clean up - pwsh.AddCommand(Utils.RemoveModuleCmdletInfo) - .AddParameter("Name", "PackageManagement, PowerShellGet") - .AddParameter("Force", Utils.BoxedTrue) - .AddParameter("ErrorAction", "SilentlyContinue") - .InvokeAndClearCommands(); + RemoveSaveModuleModules(pwsh); } } #region Helper_Methods + /// + /// Runs Save-Module which downloads a module locally from the specifed repository. + /// + protected virtual void RunSaveModuleCommand(PowerShell pwsh, string repository, string moduleName, string version, string path) + { + pwsh.AddCommand(SaveModuleCmdletName) + .AddParameter("Repository", repository) + .AddParameter("Name", moduleName) + .AddParameter("RequiredVersion", version) + .AddParameter("Path", path) + .AddParameter("Force", Utils.BoxedTrue) + .AddParameter("ErrorAction", "Stop") + .InvokeAndClearCommands(); + } + + /// + /// Removes the PowerShell modules used by Save-Module. + /// + protected virtual void RemoveSaveModuleModules(PowerShell pwsh) + { + pwsh.AddCommand(Utils.RemoveModuleCmdletInfo) + .AddParameter("Name", "PackageManagement, PowerShellGet") + .AddParameter("Force", Utils.BoxedTrue) + .AddParameter("ErrorAction", "SilentlyContinue") + .InvokeAndClearCommands(); + } + + /// + /// Sets/prepares the destination path where the function app dependencies will be installed. + /// + internal void SetDependenciesDestinationPath(string path) + { + // Save-Module supports downloading side-by-size module versions. However, we only want to keep one version at the time. + // If the ManagedDependencies folder exits, remove all its contents. + if (Directory.Exists(path)) + { + DependencyManagementUtils.EmptyDirectory(path); + } + else + { + // If the destination path does not exist, create it. + // If the user does not have write access to the path, an exception will be raised. + try + { + Directory.CreateDirectory(path); + } + catch (Exception e) + { + var errorMsg = string.Format(PowerShellWorkerStrings.FailToCreateFunctionAppDependenciesDestinationPath, path, e.Message); + throw new InvalidOperationException(errorMsg); + } + } + } + /// /// Validates that the given major version is less or equal to the latest supported major version. /// diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index f6ed3e04..1bf17bed 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -176,7 +176,7 @@ Dependency '{0}' is null or empty. - Module name '{0}' version '{0}' has been installed. + Module name '{0}' version '{1}' has been installed. FunctionApp has the latest dependencies already installed. @@ -194,7 +194,7 @@ Invalid major version for module '{0}'. The maximum available major version is '{1}'. - Fail to install FunctionApp dependencies. For more information, please see error logs. Restarting the app may resolve the error. Error: '{0}' + Fail to install FunctionApp dependencies. Error: '{0}' Fail to resolve '{0}' path in App Service. @@ -229,7 +229,7 @@ Fail to create FunctionApp dependencies destination path '{0}'. Please make sure you have write access to this location. Error '{1}''. - - Fail to install FunctionApp dependent module '{0}' version '{1}'. Error: '{2}''. + + Fail to install module '{0}' version '{1}'. Error: '{2}' \ No newline at end of file From 45d770dfc64f8a6208356a068ace9f58b9c27dc4 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Thu, 6 Jun 2019 14:59:25 -0700 Subject: [PATCH 2/8] Adding unit test to validate the retry logic in InstallFunctionAppDependencies as well as a successful module download. --- .../DependencyManagementTests.cs | 140 +++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs index 83c3f6f3..5048625c 100644 --- a/test/Unit/DependencyManagement/DependencyManagementTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagementTests.cs @@ -11,18 +11,27 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test { - public class DependencyManagementTests + using System.Management.Automation; + + public class DependencyManagementTests : IDisposable { private readonly string _dependencyManagementDirectory; private readonly string _functionId; private const string ManagedDependenciesFolderName = "ManagedDependencies"; private const string AzureFunctionsFolderName = "AzureFunctions"; + private readonly ConsoleLogger _testLogger; public DependencyManagementTests() { _dependencyManagementDirectory = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "DependencyManagement"); _functionId = Guid.NewGuid().ToString(); + _testLogger = new ConsoleLogger(); + } + + public void Dispose() + { + _testLogger.FullLog.Clear(); } private FunctionLoadRequest GetFuncLoadRequest(string functionAppRoot, bool managedDependencyEnabled) @@ -106,7 +115,7 @@ public void TestManagedDependencyEmptyHashtableRequirement() { // Test case setup. var requirementsDirectoryName = "EmptyHashtableRequirement"; - var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName,"FunctionDirectory"); + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); @@ -177,5 +186,132 @@ public void TestManagedDependencyNoRequirementsFileShouldThrow() Assert.Contains("No 'requirements.psd1'", exception.Message); Assert.Contains("is found at the FunctionApp root folder", exception.Message); } + + [Fact] + public void TestManagedDependencySuccessfulModuleDownload() + { + string filePath = null; + try + { + // Test case setup. + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, + "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and process the requirements.psd1 file at the function app root. + var dependencyManager = new TestDependencyManager(); + dependencyManager.Initialize(functionLoadRequest); + + // Install the function app dependencies. + dependencyManager.InstallFunctionAppDependencies(null, _testLogger); + + // Here we will get two logs: one that says that we are installing the dependencies, and one for a successful download. + bool correctLogCount = (_testLogger.FullLog.Count == 2); + Assert.True(correctLogCount); + + // The first log should say "Installing FunctionApp dependent modules." + Assert.Contains(PowerShellWorkerStrings.InstallingFunctionAppDependentModules, _testLogger.FullLog[0]); + + // In the overwritten RunSaveModuleCommand method, we write a text file with module name and version. + // Read the file content of the generated file. + filePath = Path.Join(DependencyManager.DependenciesPath, dependencyManager.TestFileName); + string fileContent = File.ReadAllText(filePath); + + // After running save module, we write a log with the module name and version. + // This should match was is written in the log. + Assert.Contains(fileContent, _testLogger.FullLog[1]); + + // Lastly, DependencyError should be null since the module was downloaded successfully. + Assert.Null(dependencyManager.DependencyError); + } + finally + { + TestCaseCleanup(); + if (filePath != null && File.Exists(filePath)) + { + try { File.Delete(filePath); } catch { } + } + } + } + + [Fact] + public void TestManagedDependencyRetryLogic() + { + try + { + // Test case setup + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and process the requirements.psd1 file at the function app root. + var dependencyManager = new TestDependencyManager(); + dependencyManager.Initialize(functionLoadRequest); + + // Set the dependencyManager to throw in the RunSaveModuleCommand call. + dependencyManager.ShouldThrow = true; + + // Validate retry logic. + dependencyManager.InstallFunctionAppDependencies(null, _testLogger); + + // Here we will get four logs: one that says that we are installing the + // dependencies, and three for failing to install the module. + bool correctLogCount = (_testLogger.FullLog.Count == 4); + Assert.True(correctLogCount); + + // The first log should say "Installing FunctionApp dependent modules." + Assert.Contains(PowerShellWorkerStrings.InstallingFunctionAppDependentModules, _testLogger.FullLog[0]); + + // The subsequent logs should contain the following: + for (int index = 1; index < _testLogger.FullLog.Count; index++) + { + Assert.Contains("Fail to install module", _testLogger.FullLog[index]); + } + + // Lastly, DependencyError should get set after unsuccessfully retyring 3 times. + Assert.NotNull(dependencyManager.DependencyError); + Assert.Contains("Fail to install FunctionApp dependencies. Error:", dependencyManager.DependencyError.Message); + } + finally + { + TestCaseCleanup(); + } + } + } + + internal class TestDependencyManager : DependencyManager + { + public bool ShouldThrow { get; set; } + + public string TestFileName { get; set; } = "ModuleInstalled.txt"; + + internal TestDependencyManager() + { + } + + protected override void RunSaveModuleCommand(PowerShell pwsh, string repository, string moduleName, string version, string path) + { + if (ShouldThrow) + { + var errorMsg = $"Fail to install module '{moduleName}' version '{version}'"; + throw new InvalidOperationException(errorMsg); + } + + // Write a text file to the given path with the information of the module that was downloaded. + var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, version); + var filePath = Path.Join(path, TestFileName); + File.WriteAllText(filePath, message); + } + + protected override void RemoveSaveModuleModules(PowerShell pwsh) + { + return; + } } } From deb025ea7d54d90bd9dde080fa9e499e52ac2a43 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Thu, 6 Jun 2019 20:41:45 -0700 Subject: [PATCH 3/8] Fixing faulty logic in InstallFunctionAppDependencies and refactoring DependencyManager.cs to accommodate for unit test. --- src/DependencyManagement/DependencyManager.cs | 111 ++++++++++++------ src/resources/PowerShellWorkerStrings.resx | 8 +- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 94fe871b..7b6eb227 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -60,10 +60,10 @@ internal class DependencyManager // Managed Dependencies folder name. private const string ManagedDependenciesFolderName = "ManagedDependencies"; - //Set when any error occurs while downloading dependencies + // Set when any error occurs while downloading dependencies private Exception _dependencyError; - //Dependency download task + // Dependency download task private Task _dependencyDownloadTask; // This flag is used to figure out if we need to install/reinstall all the function app dependencies. @@ -73,6 +73,9 @@ internal class DependencyManager // Maximum number of tries for retry logic when installing function app dependencies. private const int MaxNumberOfTries = 3; + // Save-Module cmdlet name + private const string SaveModuleCmdletName = "PowerShellGet\\Save-Module"; + internal DependencyManager() { Dependencies = new List(); @@ -192,25 +195,15 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) // Install the function dependencies. logger.Log(LogLevel.Trace, PowerShellWorkerStrings.InstallingFunctionAppDependentModules, isUserLog: true); - if (Directory.Exists(DependenciesPath)) + try { - // Save-Module supports downloading side-by-size module versions. However, we only want to keep one version at the time. - // If the ManagedDependencies folder exits, remove all its contents. - DependencyManagementUtils.EmptyDirectory(DependenciesPath); + SetDependenciesDestinationPath(DependenciesPath); } - else + catch (Exception e) { - // If the destination path does not exist, create it. - // If the user does not have write access to the path, an exception will be raised. - try - { - Directory.CreateDirectory(DependenciesPath); - } - catch (Exception e) - { - var message = string.Format(PowerShellWorkerStrings.FailToCreateFunctionAppDependenciesDestinationPath, DependenciesPath, e.Message); - logger.Log(LogLevel.Trace, message, isUserLog: true); - } + logger.Log(LogLevel.Error, e.Message, isUserLog: true); + _dependencyError = new DependencyInstallationException(e.Message, e); + return; } try @@ -227,14 +220,7 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) try { // Save the module to the given path - pwsh.AddCommand("PowerShellGet\\Save-Module") - .AddParameter("Repository", Repository) - .AddParameter("Name", moduleName) - .AddParameter("RequiredVersion", latestVersion) - .AddParameter("Path", DependenciesPath) - .AddParameter("Force", Utils.BoxedTrue) - .AddParameter("ErrorAction", "Stop") - .InvokeAndClearCommands(); + RunSaveModuleCommand(pwsh, Repository, moduleName, latestVersion, DependenciesPath); var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, latestVersion); logger.Log(LogLevel.Trace, message, isUserLog: true); @@ -243,17 +229,14 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) } catch (Exception e) { + var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, moduleName, latestVersion, e.Message); + logger.Log(LogLevel.Error, errorMsg, isUserLog: true); + if (tries >= MaxNumberOfTries) { - var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependencies, e.Message); + errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependencies, e.Message); _dependencyError = new DependencyInstallationException(errorMsg, e); - - throw _dependencyError; - } - else - { - var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallFuncAppDependency, moduleName, latestVersion, e.Message); - logger.Log(LogLevel.Error, errorMsg, isUserLog: true); + return; } } @@ -269,16 +252,66 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) finally { // Clean up - pwsh.AddCommand(Utils.RemoveModuleCmdletInfo) - .AddParameter("Name", "PackageManagement, PowerShellGet") - .AddParameter("Force", Utils.BoxedTrue) - .AddParameter("ErrorAction", "SilentlyContinue") - .InvokeAndClearCommands(); + RemoveSaveModuleModules(pwsh); } } #region Helper_Methods + /// + /// Runs Save-Module which downloads a module locally from the specified repository. + /// + protected virtual void RunSaveModuleCommand(PowerShell pwsh, string repository, string moduleName, string version, string path) + { + pwsh.AddCommand(SaveModuleCmdletName) + .AddParameter("Repository", repository) + .AddParameter("Name", moduleName) + .AddParameter("RequiredVersion", version) + .AddParameter("Path", path) + .AddParameter("Force", Utils.BoxedTrue) + .AddParameter("ErrorAction", "Stop") + .InvokeAndClearCommands(); + } + + /// + /// Removes the PowerShell modules used by the Save-Module cmdlet. + /// + protected virtual void RemoveSaveModuleModules(PowerShell pwsh) + { + pwsh.AddCommand(Utils.RemoveModuleCmdletInfo) + .AddParameter("Name", "PackageManagement, PowerShellGet") + .AddParameter("Force", Utils.BoxedTrue) + .AddParameter("ErrorAction", "SilentlyContinue") + .InvokeAndClearCommands(); + } + + /// + /// Sets/prepares the destination path where the function app dependencies will be installed. + /// + internal void SetDependenciesDestinationPath(string path) + { + // Save-Module supports downloading side-by-size module versions. However, we only want to keep one version at the time. + // If the ManagedDependencies folder exits, remove all its contents. + if (Directory.Exists(path)) + { + DependencyManagementUtils.EmptyDirectory(path); + } + else + { + // If the destination path does not exist, create it. + // If the user does not have write access to the path, an exception will be raised. + try + { + Directory.CreateDirectory(path); + } + catch (Exception e) + { + var errorMsg = string.Format(PowerShellWorkerStrings.FailToCreateFunctionAppDependenciesDestinationPath, path, e.Message); + throw new InvalidOperationException(errorMsg); + } + } + } + /// /// Validates that the given major version is less or equal to the latest supported major version. /// diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index f6ed3e04..1bf17bed 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -176,7 +176,7 @@ Dependency '{0}' is null or empty. - Module name '{0}' version '{0}' has been installed. + Module name '{0}' version '{1}' has been installed. FunctionApp has the latest dependencies already installed. @@ -194,7 +194,7 @@ Invalid major version for module '{0}'. The maximum available major version is '{1}'. - Fail to install FunctionApp dependencies. For more information, please see error logs. Restarting the app may resolve the error. Error: '{0}' + Fail to install FunctionApp dependencies. Error: '{0}' Fail to resolve '{0}' path in App Service. @@ -229,7 +229,7 @@ Fail to create FunctionApp dependencies destination path '{0}'. Please make sure you have write access to this location. Error '{1}''. - - Fail to install FunctionApp dependent module '{0}' version '{1}'. Error: '{2}''. + + Fail to install module '{0}' version '{1}'. Error: '{2}' \ No newline at end of file From 626c793411fa4dbf992b8f09132adae22c773b44 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Thu, 6 Jun 2019 20:43:12 -0700 Subject: [PATCH 4/8] Adding unit test to validate the retry logic in InstallFunctionAppDependencies as well as a successful module download. --- .../DependencyManagementTests.cs | 139 +++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs index 83c3f6f3..9723abf4 100644 --- a/test/Unit/DependencyManagement/DependencyManagementTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagementTests.cs @@ -11,18 +11,27 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test { - public class DependencyManagementTests + using System.Management.Automation; + + public class DependencyManagementTests : IDisposable { private readonly string _dependencyManagementDirectory; private readonly string _functionId; private const string ManagedDependenciesFolderName = "ManagedDependencies"; private const string AzureFunctionsFolderName = "AzureFunctions"; + private readonly ConsoleLogger _testLogger; public DependencyManagementTests() { _dependencyManagementDirectory = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "DependencyManagement"); _functionId = Guid.NewGuid().ToString(); + _testLogger = new ConsoleLogger(); + } + + public void Dispose() + { + _testLogger.FullLog.Clear(); } private FunctionLoadRequest GetFuncLoadRequest(string functionAppRoot, bool managedDependencyEnabled) @@ -106,7 +115,7 @@ public void TestManagedDependencyEmptyHashtableRequirement() { // Test case setup. var requirementsDirectoryName = "EmptyHashtableRequirement"; - var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName,"FunctionDirectory"); + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); @@ -177,5 +186,131 @@ public void TestManagedDependencyNoRequirementsFileShouldThrow() Assert.Contains("No 'requirements.psd1'", exception.Message); Assert.Contains("is found at the FunctionApp root folder", exception.Message); } + + [Fact] + public void TestManagedDependencySuccessfulModuleDownload() + { + string filePath = null; + try + { + // Test case setup. + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and process the requirements.psd1 file at the function app root. + var dependencyManager = new TestDependencyManager(); + dependencyManager.Initialize(functionLoadRequest); + + // Install the function app dependencies. + dependencyManager.InstallFunctionAppDependencies(null, _testLogger); + + // Here we will get two logs: one that says that we are installing the dependencies, and one for a successful download. + bool correctLogCount = (_testLogger.FullLog.Count == 2); + Assert.True(correctLogCount); + + // The first log should say "Installing FunctionApp dependent modules." + Assert.Contains(PowerShellWorkerStrings.InstallingFunctionAppDependentModules, _testLogger.FullLog[0]); + + // In the overwritten RunSaveModuleCommand method, we write a text file with module name and version. + // Read the file content of the generated file. + filePath = Path.Join(DependencyManager.DependenciesPath, dependencyManager.TestFileName); + string fileContent = File.ReadAllText(filePath); + + // After running save module, we write a log with the module name and version. + // This should match was is written in the log. + Assert.Contains(fileContent, _testLogger.FullLog[1]); + + // Lastly, DependencyError should be null since the module was downloaded successfully. + Assert.Null(dependencyManager.DependencyError); + } + finally + { + TestCaseCleanup(); + if (filePath != null && File.Exists(filePath)) + { + try { File.Delete(filePath); } catch { } + } + } + } + + [Fact] + public void TestManagedDependencyRetryLogic() + { + try + { + // Test case setup + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and process the requirements.psd1 file at the function app root. + var dependencyManager = new TestDependencyManager(); + dependencyManager.Initialize(functionLoadRequest); + + // Set the dependencyManager to throw in the RunSaveModuleCommand call. + dependencyManager.ShouldThrow = true; + + // Validate retry logic. + dependencyManager.InstallFunctionAppDependencies(null, _testLogger); + + // Here we will get four logs: one that says that we are installing the + // dependencies, and three for failing to install the module. + bool correctLogCount = (_testLogger.FullLog.Count == 4); + Assert.True(correctLogCount); + + // The first log should say "Installing FunctionApp dependent modules." + Assert.Contains(PowerShellWorkerStrings.InstallingFunctionAppDependentModules, _testLogger.FullLog[0]); + + // The subsequent logs should contain the following: + for (int index = 1; index < _testLogger.FullLog.Count; index++) + { + Assert.Contains("Fail to install module", _testLogger.FullLog[index]); + } + + // Lastly, DependencyError should get set after unsuccessfully retyring 3 times. + Assert.NotNull(dependencyManager.DependencyError); + Assert.Contains("Fail to install FunctionApp dependencies. Error:", dependencyManager.DependencyError.Message); + } + finally + { + TestCaseCleanup(); + } + } + } + + internal class TestDependencyManager : DependencyManager + { + public bool ShouldThrow { get; set; } + + public string TestFileName { get; set; } = "ModuleInstalled.txt"; + + internal TestDependencyManager() + { + } + + protected override void RunSaveModuleCommand(PowerShell pwsh, string repository, string moduleName, string version, string path) + { + if (ShouldThrow) + { + var errorMsg = $"Fail to install module '{moduleName}' version '{version}'"; + throw new InvalidOperationException(errorMsg); + } + + // Write a text file to the given path with the information of the module that was downloaded. + var message = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, version); + var filePath = Path.Join(path, TestFileName); + File.WriteAllText(filePath, message); + } + + protected override void RemoveSaveModuleModules(PowerShell pwsh) + { + return; + } } } From 7bfd8f0400f7eb56b24994bde42aea5fa8d04159 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Mon, 10 Jun 2019 17:34:51 -0700 Subject: [PATCH 5/8] Adding number of attempts to error log. Updating unit test to reflect this change. --- src/DependencyManagement/DependencyManager.cs | 32 ++++++++++++++++++- src/resources/PowerShellWorkerStrings.resx | 13 ++++++-- .../DependencyManagementTests.cs | 4 +++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 7b6eb227..afff137c 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -229,7 +229,8 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger) } catch (Exception e) { - var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, moduleName, latestVersion, e.Message); + string currentAttempt = GetCurrentAttemptMessage(tries); + var errorMsg = string.Format(PowerShellWorkerStrings.FailToInstallModule, moduleName, latestVersion, currentAttempt, e.Message); logger.Log(LogLevel.Error, errorMsg, isUserLog: true); if (tries >= MaxNumberOfTries) @@ -285,6 +286,35 @@ protected virtual void RemoveSaveModuleModules(PowerShell pwsh) .InvokeAndClearCommands(); } + /// + /// Returs the string representation of the given attempt number. + /// 1 returns 1st + /// 2 returns 2nd + /// 3 returns 3rd + /// + internal string GetCurrentAttemptMessage(int attempt) + { + string result = null; + + switch (attempt) + { + case 1: + result = PowerShellWorkerStrings.FirstAttempt; + break; + case 2: + result = PowerShellWorkerStrings.SecondAttempt; + break; + case 3: + result = PowerShellWorkerStrings.ThirdAttempt; + break; + default: + throw new InvalidOperationException("Invalid attempt number. Unreachable code."); + + } + + return result; + } + /// /// Sets/prepares the destination path where the function app dependencies will be installed. /// diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 1bf17bed..e8698f2f 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -229,7 +229,16 @@ Fail to create FunctionApp dependencies destination path '{0}'. Please make sure you have write access to this location. Error '{1}''. - - Fail to install module '{0}' version '{1}'. Error: '{2}' + + Fail to install module '{0}' version '{1}'. {2} attempt. Error: '{3}' + + + 1st + + + 2nd + + + 3rd \ No newline at end of file diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs index 7d065b92..088687fd 100644 --- a/test/Unit/DependencyManagement/DependencyManagementTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagementTests.cs @@ -266,6 +266,8 @@ public void TestManagedDependencySuccessfulModuleDownloadAfterTwoTries() for (int index = 1; index < _testLogger.FullLog.Count - 1; index++) { Assert.Contains("Fail to install module", _testLogger.FullLog[index]); + var currentAttempt = dependencyManager.GetCurrentAttemptMessage(index); + Assert.Contains(currentAttempt, _testLogger.FullLog[index]); } // Successful module download log after two retries. @@ -314,6 +316,8 @@ public void TestManagedDependencyRetryLogicMaxNumberOfTries() for (int index = 1; index < _testLogger.FullLog.Count; index++) { Assert.Contains("Fail to install module", _testLogger.FullLog[index]); + var currentAttempt = dependencyManager.GetCurrentAttemptMessage(index); + Assert.Contains(currentAttempt, _testLogger.FullLog[index]); } // Lastly, DependencyError should get set after unsuccessfully retyring 3 times. From 95ae8e5e2fa06d85316ea2775b66e63fc0c1d807 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Wed, 19 Jun 2019 09:33:59 -0700 Subject: [PATCH 6/8] Adding logic to dependency manager to fail in the first dependencies installation run if the PSGallery could not be reached, but continue if previous dependencies exist. --- .../DependencyManagementUtils.cs | 104 ---------- src/DependencyManagement/DependencyManager.cs | 196 +++++++++++++++--- src/resources/PowerShellWorkerStrings.resx | 15 +- 3 files changed, 177 insertions(+), 138 deletions(-) diff --git a/src/DependencyManagement/DependencyManagementUtils.cs b/src/DependencyManagement/DependencyManagementUtils.cs index 8d0962e2..4a79e67e 100644 --- a/src/DependencyManagement/DependencyManagementUtils.cs +++ b/src/DependencyManagement/DependencyManagementUtils.cs @@ -6,16 +6,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; -using System.Xml; namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement { internal class DependencyManagementUtils { - // The PowerShellGallery uri to query for the latest module version. - private const string PowerShellGalleryFindPackagesByIdUri = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id="; - /// /// Deletes the contents at the given directory. /// @@ -51,104 +46,5 @@ internal static void EmptyDirectory(string path) throw new InvalidOperationException(errorMsg); } } - - /// - /// Returns the latest module version from the PSGallery for the given module name and major version. - /// - internal static string GetModuleLatestSupportedVersion(string moduleName, string majorVersion) - { - Uri address = new Uri($"{PowerShellGalleryFindPackagesByIdUri}'{moduleName}'"); - int configuredRetries = 3; - int noOfRetries = 1; - - string latestVersionForMajorVersion = null; - - while (noOfRetries <= configuredRetries) - { - try - { - HttpWebRequest request = WebRequest.Create(address) as HttpWebRequest; - using (HttpWebResponse response = request?.GetResponse() as HttpWebResponse) - { - if (response != null) - { - // Load up the XML response - XmlDocument doc = new XmlDocument(); - using (XmlReader reader = XmlReader.Create(response.GetResponseStream())) - { - doc.Load(reader); - } - - // Add the namespaces for the gallery xml content - XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); - nsmgr.AddNamespace("ps", "http://www.w3.org/2005/Atom"); - nsmgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); - nsmgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"); - - // Find the version information - XmlNode root = doc.DocumentElement; - var props = root.SelectNodes("//m:properties/d:Version", nsmgr); - if (props != null && props.Count > 0) - { - for (int i = 0; i < props.Count; i++) - { - if (props[i].FirstChild.Value.StartsWith(majorVersion)) - { - latestVersionForMajorVersion = props[i].FirstChild.Value; - } - } - } - break; - } - } - } - catch (Exception ex) - { - WebException webEx = ex as WebException; - if (webEx == null || noOfRetries >= configuredRetries) - { - throw; - } - - // Only retry the web exception - if (ShouldRetry(webEx)) - { - noOfRetries++; - } - } - } - - // If we could not find the latest module version error out. - if (string.IsNullOrEmpty(latestVersionForMajorVersion)) - { - var errorMsg = string.Format(PowerShellWorkerStrings.CannotFindModuleVersion, moduleName, majorVersion); - var argException = new ArgumentException(errorMsg); - throw argException; - } - - return latestVersionForMajorVersion; - } - - /// - /// Returns true if the given WebException status matches one of the following: - /// SendFailure, ConnectFailure, UnknownError or Timeout. - /// - private static bool ShouldRetry(WebException webEx) - { - if (webEx == null) - { - return false; - } - - if (webEx.Status == WebExceptionStatus.SendFailure || - webEx.Status == WebExceptionStatus.ConnectFailure || - webEx.Status == WebExceptionStatus.UnknownError || - webEx.Status == WebExceptionStatus.Timeout) - { - return true; - } - - return false; - } } } diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index afff137c..315aee17 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -10,8 +10,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using System.Xml; using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; using Microsoft.Azure.Functions.PowerShellWorker.Utility; using Microsoft.Azure.Functions.PowerShellWorker.Messaging; @@ -22,7 +24,6 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement { using System.Management.Automation; using System.Management.Automation.Language; - using System.Management.Automation.Runspaces; internal class DependencyManager { @@ -70,12 +71,18 @@ internal class DependencyManager // If we do, we use it to clean up the module destination path. private bool _shouldUpdateFunctionAppDependencies; + // This string holds the message to be logged if we skipped updating function app dependencies. + private string _dependenciesNotUpdatedMessage; + // Maximum number of tries for retry logic when installing function app dependencies. private const int MaxNumberOfTries = 3; - // Save-Module cmdlet name + // Save-Module cmdlet name. private const string SaveModuleCmdletName = "PowerShellGet\\Save-Module"; + // The PowerShellGallery uri to query for the latest module version. + private const string PowerShellGalleryFindPackagesByIdUri = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id="; + internal DependencyManager() { Dependencies = new List(); @@ -93,17 +100,28 @@ internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMess { var rpcLogger = new RpcLogger(msgStream); rpcLogger.SetContext(request.RequestId, null); - if (Dependencies.Count == 0) + + if (!_shouldUpdateFunctionAppDependencies) { - // If there are no dependencies to install, log and return. - rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.FunctionAppDoesNotHaveDependentModulesToInstall, isUserLog: true); - return; + if (!string.IsNullOrEmpty(_dependenciesNotUpdatedMessage)) + { + // We were not able to update the function app dependencies. + // However, there is a previous installation, so continue with the function app execution. + rpcLogger.Log(LogLevel.Warning, _dependenciesNotUpdatedMessage, isUserLog: true); + return; + } + else + { + // The function app already has the latest dependencies installed. + rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.LatestFunctionAppDependenciesAlreadyInstalled, isUserLog: true); + return; + } } - if (!_shouldUpdateFunctionAppDependencies) + if (Dependencies.Count == 0) { - // The function app already has the latest dependencies installed. - rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.LatestFunctionAppDependenciesAlreadyInstalled, isUserLog: true); + // If there are no dependencies to install, log and return. + rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.FunctionAppDoesNotHaveDependentModulesToInstall, isUserLog: true); return; } @@ -161,19 +179,49 @@ internal void Initialize(FunctionLoadRequest request) // Validate the module version. string majorVersion = GetMajorVersion(version); - string latestVersion = DependencyManagementUtils.GetModuleLatestSupportedVersion(name, majorVersion); - ValidateModuleMajorVersion(name, majorVersion, latestVersion); - // Before installing the module, check the path to see if it is already installed. - var moduleVersionFolderPath = Path.Combine(DependenciesPath, name, latestVersion); - if (!Directory.Exists(moduleVersionFolderPath)) + // Try to connect to the PSGallery via C# API to get the latest module supported version. + string latestVersion = null; + bool latestVersionRetrieved = false; + try + { + latestVersion = GetModuleLatestSupportedVersion(name, majorVersion); + latestVersionRetrieved = true; + } + catch (Exception e) { - _shouldUpdateFunctionAppDependencies = true; + // If we fail to get the latest module version (this could be because the PSGallery is down). + + // Check to see if there are previous managed dependencies installed. If this is the case, + // DependenciesPath is already set, so Get-Module will be able to find the modules. + var pathToInstalledModule = Path.Combine(DependenciesPath, name); + if (Directory.Exists(pathToInstalledModule)) + { + // Message to the user for skipped dependencies upgrade. + _dependenciesNotUpdatedMessage = string.Format(PowerShellWorkerStrings.DependenciesUpgradeSkippedMessage, e.Message); + + // Make sure that function app dependencies will NOT be installed, just continue with the function app execution. + _shouldUpdateFunctionAppDependencies = false; + return; + } + + // Otherwise, rethrow and stop the function app execution. + throw; } - // Create a DependencyInfo object and add it to the list of dependencies to install. - var dependencyInfo = new DependencyInfo(name, majorVersion, latestVersion); - Dependencies.Add(dependencyInfo); + if (latestVersionRetrieved) + { + // Before installing the module, check the path to see if it is already installed. + var moduleVersionFolderPath = Path.Combine(DependenciesPath, name, latestVersion); + if (!Directory.Exists(moduleVersionFolderPath)) + { + _shouldUpdateFunctionAppDependencies = true; + } + + // Create a DependencyInfo object and add it to the list of dependencies to install. + var dependencyInfo = new DependencyInfo(name, majorVersion, latestVersion); + Dependencies.Add(dependencyInfo); + } } } catch (Exception e) @@ -309,7 +357,6 @@ internal string GetCurrentAttemptMessage(int attempt) break; default: throw new InvalidOperationException("Invalid attempt number. Unreachable code."); - } return result; @@ -343,20 +390,43 @@ internal void SetDependenciesDestinationPath(string path) } /// - /// Validates that the given major version is less or equal to the latest supported major version. + /// Gets the latest module version from the PSGallery for the given module name and major version. /// - private void ValidateModuleMajorVersion(string moduleName, string majorVersion, string latestVersion) + internal string GetModuleLatestSupportedVersion(string moduleName, string majorVersion) { - // A Version object cannot be created with a single digit so add a '.0' to it. - var requestedVersion = new Version($"{majorVersion}.0"); - var latestSupportedVersion = new Version(latestVersion); + string latestVersion = null; + + string errorDetails = null; + bool throwException = false; - if (requestedVersion.Major > latestSupportedVersion.Major) + try { - // The requested major version is greater than the latest major supported version. - var errorMsg = string.Format(PowerShellWorkerStrings.InvalidModuleMajorVersion, moduleName, majorVersion); - throw new ArgumentException(errorMsg); + latestVersion = GetLatestModuleVersionFromThePSGallery(moduleName, majorVersion); + } + catch (Exception e) + { + throwException = true; + + if (!string.IsNullOrEmpty(e.Message)) + { + errorDetails = string.Format(PowerShellWorkerStrings.ErrorDetails, e.Message.ToString()); + } } + + // If we could not find the latest module version error out. + if (string.IsNullOrEmpty(latestVersion) || throwException) + { + if (string.IsNullOrEmpty(errorDetails)) + { + errorDetails = string.Empty; + } + + var errorMsg = string.Format(PowerShellWorkerStrings.FailToGetModuleLatestVersion, moduleName, majorVersion, errorDetails); + var argException = new ArgumentException(errorMsg); + throw argException; + } + + return latestVersion; } /// @@ -486,6 +556,76 @@ private string GetManagedDependenciesPath(string functionAppRootPath) return managedDependenciesFolderPath; } + /// + /// Returns the latest module version from the PSGallery for the given module name and major version. + /// + protected virtual string GetLatestModuleVersionFromThePSGallery(string moduleName, string majorVersion) + { + Uri address = new Uri($"{PowerShellGalleryFindPackagesByIdUri}'{moduleName}'"); + + string latestMajorVersion = null; + Stream stream = null; + + var retryCount = 3; + while (true) + { + using (var client = new HttpClient()) + { + try + { + var response = client.GetAsync(address).Result; + + // Throw is not a successful request + response.EnsureSuccessStatusCode(); + + stream = response.Content.ReadAsStreamAsync().Result; + break; + } + catch (Exception e) + { + if (retryCount <= 0) + { + throw e; + } + retryCount--; + } + } + } + + if (stream != null) + { + // Load up the XML response + XmlDocument doc = new XmlDocument(); + using (XmlReader reader = XmlReader.Create(stream)) + { + doc.Load(reader); + } + + // Add the namespaces for the gallery xml content + XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("ps", "http://www.w3.org/2005/Atom"); + nsmgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); + nsmgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"); + + // Find the version information + XmlNode root = doc.DocumentElement; + var props = root.SelectNodes("//m:properties/d:Version", nsmgr); + + if (props != null && props.Count > 0) + { + foreach (XmlNode prop in props) + { + if (prop.FirstChild.Value.StartsWith(majorVersion)) + { + latestMajorVersion = prop.FirstChild.Value; + } + } + } + } + + return latestMajorVersion; + } + #endregion } } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index e8698f2f..49a40a21 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -166,8 +166,8 @@ Custom pipe name specified. You can attach to the process by using vscode or by running `Enter-PSHostProcess -CustomPipeName {0}` - - Cannot find a supported version for module '{0}' with major version '{0}'. + + Fail to get latest version for module '{0}' with major version '{1}'. {2} FunctionApp does have dependent modules to install. @@ -190,9 +190,6 @@ Version is not in the correct format. Please use the following notation: '{0}' - - Invalid major version for module '{0}'. The maximum available major version is '{1}'. - Fail to install FunctionApp dependencies. Error: '{0}' @@ -227,7 +224,7 @@ Unrecognized data collecting behavior '{0}'. - Fail to create FunctionApp dependencies destination path '{0}'. Please make sure you have write access to this location. Error '{1}''. + Fail to create FunctionApp dependencies destination path '{0}'. Please make sure you have write access to this location. Error '{1}'. Fail to install module '{0}' version '{1}'. {2} attempt. Error: '{3}' @@ -241,4 +238,10 @@ 3rd + + Error: '{0}' + + + Function app dependencies upgrade skipped. Error details: {0}. + \ No newline at end of file From 61ce5b2dbf6f4de4c86dfc8b7e21fb34ef8e2e78 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Wed, 19 Jun 2019 09:36:18 -0700 Subject: [PATCH 7/8] Adding unit test to validate that a function app execution should continue if the PSGallery could not be reached but a previous installation of managed dependencies exists. --- .../DependencyManagementTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs index 088687fd..074f47d9 100644 --- a/test/Unit/DependencyManagement/DependencyManagementTests.cs +++ b/test/Unit/DependencyManagement/DependencyManagementTests.cs @@ -329,6 +329,89 @@ public void TestManagedDependencyRetryLogicMaxNumberOfTries() TestCaseCleanup(); } } + + [Fact] + public void FunctionAppExecutionShouldStopIfNoPreviousDependenciesAreInstalled() + { + try + { + // Test case setup + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and configure it to mimic being unable to reach + // the PSGallery to retrieve the latest module version + var dependencyManager = new TestDependencyManager(); + dependencyManager.GetLatestModuleVersionThrows = true; + + // Trying to initialize the dependencyManager should throw + var exception = Assert.Throws(() => dependencyManager.Initialize(functionLoadRequest)); + Assert.Contains("Fail to install FunctionApp dependencies.", exception.Message); + Assert.Contains("Fail to connect to the PSGallery", exception.Message); + + // Dependencies.Count should be 0, and DependencyManager.DependenciesPath should null + Assert.True(DependencyManager.Dependencies.Count == 0); + Assert.Null(DependencyManager.DependenciesPath); + } + finally + { + TestCaseCleanup(); + } + } + + [Fact] + public void FunctionAppExecutionShouldContinueIfPreviousDependenciesExist() + { + string AzModulePath = null; + try + { + // Test case setup + var requirementsDirectoryName = "BasicRequirements"; + var functionFolderPath = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName, "FunctionDirectory"); + var functionAppRoot = Path.Combine(_dependencyManagementDirectory, requirementsDirectoryName); + var managedDependenciesFolderPath = GetManagedDependenciesPath(functionAppRoot); + var functionLoadRequest = GetFuncLoadRequest(functionFolderPath, true); + + // Create DependencyManager and configure it to mimic being unable to reach + // the PSGallery to retrive the latest module version + var dependencyManager = new TestDependencyManager(); + dependencyManager.GetLatestModuleVersionThrows = true; + + // Create a path to mimic an existing installation of the Az module + AzModulePath = Path.Join(managedDependenciesFolderPath, "Az"); + if (Directory.Exists(AzModulePath)) + { + Directory.Delete(AzModulePath, true); + } + Directory.CreateDirectory(AzModulePath); + + // Initializing the dependency manager should not throw even though we were not able + // to connect to the PSGallery--given that a previous installation of the Az module is present + dependencyManager.Initialize(functionLoadRequest); + + // Dependencies.Count should be 0 (since no dependencies will be installed) + Assert.True(DependencyManager.Dependencies.Count == 0); + + // Validate that DependencyManager.DependenciesPath is set, so + // Get-Module can find the existing dependencies installed + var dependenciesPathIsValid = managedDependenciesFolderPath.Equals(DependencyManager.DependenciesPath, + StringComparison.CurrentCultureIgnoreCase); + Assert.True(dependenciesPathIsValid); + } + finally + { + if (Directory.Exists(AzModulePath)) + { + Directory.Delete(AzModulePath, true); + } + + TestCaseCleanup(); + } + } } internal class TestDependencyManager : DependencyManager @@ -343,6 +426,9 @@ internal class TestDependencyManager : DependencyManager private int SaveModuleCount { get; set; } + // Settings for GetLatestModuleVersionFromTheGallery + public bool GetLatestModuleVersionThrows { get; set; } + internal TestDependencyManager() { } @@ -366,5 +452,15 @@ protected override void RemoveSaveModuleModules(PowerShell pwsh) { return; } + + protected override string GetLatestModuleVersionFromThePSGallery(string moduleName, string majorVersion) + { + if (GetLatestModuleVersionThrows) + { + throw new InvalidOperationException("Fail to connect to the PSGallery"); + } + + return "2.0"; + } } } From 90a39c42bfb236ada240fd4456cd7510092d8fa1 Mon Sep 17 00:00:00 2001 From: Francisco Gomez Gamino Date: Wed, 19 Jun 2019 12:41:04 -0700 Subject: [PATCH 8/8] Addressing code review comments. --- src/DependencyManagement/DependencyManager.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 315aee17..88f8442d 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -108,14 +108,14 @@ internal void ProcessDependencyDownload(MessagingStream msgStream, StreamingMess // We were not able to update the function app dependencies. // However, there is a previous installation, so continue with the function app execution. rpcLogger.Log(LogLevel.Warning, _dependenciesNotUpdatedMessage, isUserLog: true); - return; } else { // The function app already has the latest dependencies installed. rpcLogger.Log(LogLevel.Trace, PowerShellWorkerStrings.LatestFunctionAppDependenciesAlreadyInstalled, isUserLog: true); - return; } + + return; } if (Dependencies.Count == 0) @@ -180,13 +180,11 @@ internal void Initialize(FunctionLoadRequest request) // Validate the module version. string majorVersion = GetMajorVersion(version); - // Try to connect to the PSGallery via C# API to get the latest module supported version. + // Try to connect to the PSGallery to get the latest module supported version. string latestVersion = null; - bool latestVersionRetrieved = false; try { latestVersion = GetModuleLatestSupportedVersion(name, majorVersion); - latestVersionRetrieved = true; } catch (Exception e) { @@ -209,19 +207,16 @@ internal void Initialize(FunctionLoadRequest request) throw; } - if (latestVersionRetrieved) + // Before installing the module, check the path to see if it is already installed. + var moduleVersionFolderPath = Path.Combine(DependenciesPath, name, latestVersion); + if (!Directory.Exists(moduleVersionFolderPath)) { - // Before installing the module, check the path to see if it is already installed. - var moduleVersionFolderPath = Path.Combine(DependenciesPath, name, latestVersion); - if (!Directory.Exists(moduleVersionFolderPath)) - { - _shouldUpdateFunctionAppDependencies = true; - } - - // Create a DependencyInfo object and add it to the list of dependencies to install. - var dependencyInfo = new DependencyInfo(name, majorVersion, latestVersion); - Dependencies.Add(dependencyInfo); + _shouldUpdateFunctionAppDependencies = true; } + + // Create a DependencyInfo object and add it to the list of dependencies to install. + var dependencyInfo = new DependencyInfo(name, majorVersion, latestVersion); + Dependencies.Add(dependencyInfo); } } catch (Exception e) @@ -416,14 +411,8 @@ internal string GetModuleLatestSupportedVersion(string moduleName, string majorV // If we could not find the latest module version error out. if (string.IsNullOrEmpty(latestVersion) || throwException) { - if (string.IsNullOrEmpty(errorDetails)) - { - errorDetails = string.Empty; - } - - var errorMsg = string.Format(PowerShellWorkerStrings.FailToGetModuleLatestVersion, moduleName, majorVersion, errorDetails); - var argException = new ArgumentException(errorMsg); - throw argException; + var errorMsg = string.Format(PowerShellWorkerStrings.FailToGetModuleLatestVersion, moduleName, majorVersion, errorDetails ?? string.Empty); + throw new InvalidOperationException(errorMsg); } return latestVersion;