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 94fe871b..88f8442d 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
{
@@ -60,19 +61,28 @@ 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.
// 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.
+ 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();
@@ -90,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);
+ 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);
+ }
+ 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;
}
@@ -158,8 +179,33 @@ internal void Initialize(FunctionLoadRequest request)
// Validate the module version.
string majorVersion = GetMajorVersion(version);
- string latestVersion = DependencyManagementUtils.GetModuleLatestSupportedVersion(name, majorVersion);
- ValidateModuleMajorVersion(name, majorVersion, latestVersion);
+
+ // Try to connect to the PSGallery to get the latest module supported version.
+ string latestVersion = null;
+ try
+ {
+ latestVersion = GetModuleLatestSupportedVersion(name, majorVersion);
+ }
+ catch (Exception e)
+ {
+ // 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;
+ }
// Before installing the module, check the path to see if it is already installed.
var moduleVersionFolderPath = Path.Combine(DependenciesPath, name, latestVersion);
@@ -192,25 +238,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 +263,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 +272,15 @@ internal void InstallFunctionAppDependencies(PowerShell pwsh, ILogger logger)
}
catch (Exception e)
{
+ 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)
{
- 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,31 +296,126 @@ 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
///
- /// Validates that the given major version is less or equal to the latest supported major version.
+ /// 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.
///
- private void ValidateModuleMajorVersion(string moduleName, string majorVersion, string latestVersion)
+ protected virtual void RemoveSaveModuleModules(PowerShell pwsh)
{
- // 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);
+ pwsh.AddCommand(Utils.RemoveModuleCmdletInfo)
+ .AddParameter("Name", "PackageManagement, PowerShellGet")
+ .AddParameter("Force", Utils.BoxedTrue)
+ .AddParameter("ErrorAction", "SilentlyContinue")
+ .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;
- if (requestedVersion.Major > latestSupportedVersion.Major)
+ switch (attempt)
{
- // The requested major version is greater than the latest major supported version.
- var errorMsg = string.Format(PowerShellWorkerStrings.InvalidModuleMajorVersion, moduleName, majorVersion);
- throw new ArgumentException(errorMsg);
+ 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.
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Gets the latest module version from the PSGallery for the given module name and major version.
+ ///
+ internal string GetModuleLatestSupportedVersion(string moduleName, string majorVersion)
+ {
+ string latestVersion = null;
+
+ string errorDetails = null;
+ bool throwException = false;
+
+ try
+ {
+ 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)
+ {
+ var errorMsg = string.Format(PowerShellWorkerStrings.FailToGetModuleLatestVersion, moduleName, majorVersion, errorDetails ?? string.Empty);
+ throw new InvalidOperationException(errorMsg);
+ }
+
+ return latestVersion;
}
///
@@ -423,6 +545,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 f6ed3e04..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.
@@ -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.
@@ -190,11 +190,8 @@
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. 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.
@@ -227,9 +224,24 @@
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}'
+
+
+ 1st
+
+
+ 2nd
+
+
+ 3rd
+
+
+ Error: '{0}'
-
- Fail to install FunctionApp dependent module '{0}' version '{1}'. Error: '{2}''.
+
+ Function app dependencies upgrade skipped. Error details: {0}.
\ No newline at end of file
diff --git a/test/Unit/DependencyManagement/DependencyManagementTests.cs b/test/Unit/DependencyManagement/DependencyManagementTests.cs
index 83c3f6f3..074f47d9 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,281 @@ 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()
+ {
+ 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);
+
+ // Configure the dependency manager to mimic a successful download.
+ dependencyManager.SuccessfulDownload = true;
+
+ // 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 saved in DownloadedModuleInfo the module name and version.
+ // This same information is logged after running save-module, so validate that they match.
+ Assert.Contains(dependencyManager.DownloadedModuleInfo, _testLogger.FullLog[1]);
+
+ // Lastly, DependencyError should be null since the module was downloaded successfully.
+ Assert.Null(dependencyManager.DependencyError);
+ }
+ finally
+ {
+ TestCaseCleanup();
+ }
+ }
+
+ [Fact]
+ public void TestManagedDependencySuccessfulModuleDownloadAfterTwoTries()
+ {
+ 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);
+
+ // Configure the dependencyManager to not throw in the RunSaveModuleCommand call after 2 tries.
+ dependencyManager.ShouldNotThrowAfterCount = 2;
+
+ // Try to install the function app dependencies.
+ dependencyManager.InstallFunctionAppDependencies(null, _testLogger);
+
+ // Here we will get four logs:
+ // - one that say that we are installing the dependencies
+ // - two that say that we failed to download the module
+ // - one for a successful module download
+ 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 two logs should contain the following: "Fail to install module"
+ 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.
+ // In the overwritten RunSaveModuleCommand method, we saved in DownloadedModuleInfo the module name and version.
+ // This same information is logged after running save-module, so validate that they match.
+ Assert.Contains(dependencyManager.DownloadedModuleInfo, _testLogger.FullLog[3]);
+
+ // Lastly, DependencyError should be null since the module was downloaded successfully after two tries.
+ Assert.Null(dependencyManager.DependencyError);
+ }
+ finally
+ {
+ TestCaseCleanup();
+ }
+ }
+
+ [Fact]
+ public void TestManagedDependencyRetryLogicMaxNumberOfTries()
+ {
+ 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);
+
+ // Try to install the function app dependencies.
+ 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]);
+ var currentAttempt = dependencyManager.GetCurrentAttemptMessage(index);
+ Assert.Contains(currentAttempt, _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();
+ }
+ }
+
+ [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
+ {
+ // RunSaveModuleCommand in the DependencyManager class has retry logic with a max number of tries
+ // set to three. By default, we set ShouldNotThrowAfterCount to 4 to always throw.
+ public int ShouldNotThrowAfterCount { get; set; } = 4;
+
+ public bool SuccessfulDownload { get; set; }
+
+ public string DownloadedModuleInfo { get; set; }
+
+ private int SaveModuleCount { get; set; }
+
+ // Settings for GetLatestModuleVersionFromTheGallery
+ public bool GetLatestModuleVersionThrows { get; set; }
+
+ internal TestDependencyManager()
+ {
+ }
+
+ protected override void RunSaveModuleCommand(PowerShell pwsh, string repository, string moduleName, string version, string path)
+ {
+ if (SuccessfulDownload || (SaveModuleCount >= ShouldNotThrowAfterCount))
+ {
+ // Save the module name and version for a successful download.
+ DownloadedModuleInfo = string.Format(PowerShellWorkerStrings.ModuleHasBeenInstalled, moduleName, version);
+ return;
+ }
+
+ SaveModuleCount++;
+
+ var errorMsg = $"Fail to install module '{moduleName}' version '{version}'";
+ throw new InvalidOperationException(errorMsg);
+ }
+
+ 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";
+ }
}
}