diff --git a/create_new_worker_instructions.md b/create_new_worker_instructions.md new file mode 100644 index 00000000..b3824a12 --- /dev/null +++ b/create_new_worker_instructions.md @@ -0,0 +1,6 @@ +## Instructions for Upgrading the PowerShell Language Worker to a New PowerShell SDK Minor Version (7.6+) +Once a new PowerShell SDK version is released on [GiHub](https://github.com/PowerShell/PowerShell/releases), follow these steps to upgrade the PowerShell SDK reference used by the language worker: + +- Update the solution targets as needed for whatever .NET version is targeted by the new PowerShell +- Follow instructions in upgrade_ps_sdk_instructions.md to update loosely linked dependencies in project files +- Update the Managed Dependency shutoff date in src/DependencyManagement/WorkerEnvironment.cs diff --git a/src/DependencyManagement/BackgroundDependencySnapshotMaintainer.cs b/src/DependencyManagement/BackgroundDependencySnapshotMaintainer.cs index deba2e90..642ac624 100644 --- a/src/DependencyManagement/BackgroundDependencySnapshotMaintainer.cs +++ b/src/DependencyManagement/BackgroundDependencySnapshotMaintainer.cs @@ -20,6 +20,8 @@ internal class BackgroundDependencySnapshotMaintainer : IBackgroundDependencySna private TimeSpan MaxBackgroundUpgradePeriod { get; } = PowerShellWorkerConfiguration.GetTimeSpan("MDMaxBackgroundUpgradePeriod") ?? TimeSpan.FromDays(7); + private Func _getShouldPerformManagedDependencyUpgrades; + private readonly IDependencyManagerStorage _storage; private readonly IDependencySnapshotInstaller _installer; private readonly IDependencySnapshotPurger _purger; @@ -29,11 +31,14 @@ internal class BackgroundDependencySnapshotMaintainer : IBackgroundDependencySna public BackgroundDependencySnapshotMaintainer( IDependencyManagerStorage storage, IDependencySnapshotInstaller installer, - IDependencySnapshotPurger purger) + IDependencySnapshotPurger purger, + Func getShouldPerformManagedDependencyUpgrades) { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); _installer = installer ?? throw new ArgumentNullException(nameof(installer)); _purger = purger ?? throw new ArgumentNullException(nameof(purger)); + _getShouldPerformManagedDependencyUpgrades = getShouldPerformManagedDependencyUpgrades; + } public void Start(string currentSnapshotPath, DependencyManifestEntry[] dependencyManifest, ILogger logger) @@ -56,6 +61,23 @@ public string InstallAndPurgeSnapshots(Func pwshFactory, ILogger log { try { + if (!_getShouldPerformManagedDependencyUpgrades()) + { + logger.Log( + isUserOnlyLog: false, + RpcLog.Types.Level.Warning, + PowerShellWorkerStrings.AutomaticUpgradesAreDisabled); + + // Shutdown the timer that calls this method after the EOL date + if (_installAndPurgeTimer is not null) + { + _installAndPurgeTimer.Dispose(); + _installAndPurgeTimer = null; + } + + return null; + } + // Purge before installing a new snapshot, as we may be able to free some space. _purger.Purge(logger); diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 71caf2bd..a2bbe57f 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -41,6 +41,9 @@ internal class DependencyManager : IDisposable private Task _dependencyInstallationTask; + private bool EnableAutomaticUpgrades { get; } = + PowerShellWorkerConfiguration.GetBoolean("MDEnableAutomaticUpgrades") ?? false; + #endregion public DependencyManager( @@ -67,7 +70,8 @@ public DependencyManager( maintainer ?? new BackgroundDependencySnapshotMaintainer( _storage, _installer, - new DependencySnapshotPurger(_storage)); + new DependencySnapshotPurger(_storage), + ShouldEnableManagedDpendencyUpgrades); _currentSnapshotContentLogger = currentSnapshotContentLogger ?? new BackgroundDependencySnapshotContentLogger(snapshotContentLogger); } @@ -126,6 +130,18 @@ internal string Initialize(ILogger logger) } } + /// + /// Determines whether the function app should enable automatic upgrades for managed dependencies + /// + /// + /// True if Managed Dependencies should be upgraded (SDK is not past it's deprecation date OR user has configured this behavior via MDEnableAutomaticUpgrades env var + /// False if Managed Dependencies should not be upgraded + /// + private bool ShouldEnableManagedDpendencyUpgrades() + { + return !WorkerEnvironment.IsPowerShellSDKDeprecated() || EnableAutomaticUpgrades; + } + /// /// Start dependency installation if needed. /// firstPowerShell is the first PowerShell instance created in this process (which this is important for local debugging), diff --git a/src/DependencyManagement/WorkerEnvironment.cs b/src/DependencyManagement/WorkerEnvironment.cs index ba6cbe9e..2e59242a 100644 --- a/src/DependencyManagement/WorkerEnvironment.cs +++ b/src/DependencyManagement/WorkerEnvironment.cs @@ -16,6 +16,8 @@ internal static class WorkerEnvironment private const string ContainerName = "CONTAINER_NAME"; private const string LegionServiceHost = "LEGION_SERVICE_HOST"; + private static readonly DateTime PowerShellSDKDeprecationDate = new DateTime(2026, 11, 10); + public static bool IsAppService() { return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(AzureWebsiteInstanceId)); @@ -32,5 +34,10 @@ public static bool IsLinuxConsumptionOnLegion() !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(ContainerName)) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(LegionServiceHost)); } + + public static bool IsPowerShellSDKDeprecated() + { + return DateTime.Now > PowerShellSDKDeprecationDate; + } } } diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index 53f0c801..d801396c 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -352,6 +352,9 @@ Dependency snapshot '{0}' does not contain acceptable module versions. + + Worker init request completed in {0} ms. + Found external Durable Functions SDK in session: Name='{0}', Version='{1}', Path='{2}'. @@ -361,9 +364,6 @@ Unable to initialize orchestrator function due to presence of other bindings. Total number of bindings found is '{0}'. Orchestrator Functions should never use any input or output bindings other than the orchestration trigger itself. See: aka.ms/df-bindings - - Worker init request completed in {0} ms. - Operation '{0}' expected '{1}' result(s) but received '{2}'. @@ -388,4 +388,7 @@ The app is configured to use OpenTelemetry but the TraceContext passed from host was null. + + Automatic upgrades are disabled in PowerShell 7.4 function apps. This warning should not be emitted until PowerShell 7.4's End of Life date, at which time, more guidance will be available regarding how to upgrade your function app to the latest version. + \ No newline at end of file diff --git a/test/Unit/DependencyManagement/BackgroundDependencySnapshotMaintainerTests.cs b/test/Unit/DependencyManagement/BackgroundDependencySnapshotMaintainerTests.cs index aac937d9..b4a2619f 100644 --- a/test/Unit/DependencyManagement/BackgroundDependencySnapshotMaintainerTests.cs +++ b/test/Unit/DependencyManagement/BackgroundDependencySnapshotMaintainerTests.cs @@ -13,8 +13,8 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test.DependencyManagement using Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement; using Microsoft.Azure.Functions.PowerShellWorker.Utility; - using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; + using Microsoft.Azure.WebJobs.Script.Grpc.Messages; public class BackgroundDependencySnapshotMaintainerTests { @@ -30,7 +30,7 @@ public class BackgroundDependencySnapshotMaintainerTests [Fact] public void SetsCurrentlyUsedSnapshotOnPurger() { - using (var maintainer = CreateMaintainerWithMocks()) + using (var maintainer = CreateMaintainerWithMocks(true)) { maintainer.Start("current snapshot", _dependencyManifest, _mockLogger.Object); } @@ -56,7 +56,7 @@ public void InstallsSnapshotIfNoRecentlyInstalledSnapshotFound() It.IsAny())); using (var dummyPowerShell = PowerShell.Create()) - using (var maintainer = CreateMaintainerWithMocks(_minBackgroundUpgradePeriod)) + using (var maintainer = CreateMaintainerWithMocks(true, _minBackgroundUpgradePeriod)) { maintainer.Start("current snapshot", _dependencyManifest, _mockLogger.Object); @@ -73,6 +73,41 @@ public void InstallsSnapshotIfNoRecentlyInstalledSnapshotFound() } } + [Fact] + public void DoesNothingIfManagedDependenciesUpgradesAreDisabled() + { + _mockStorage.Setup(_ => _.GetInstalledAndInstallingSnapshots()).Returns(new[] { "older snapshot" }); + _mockStorage.Setup(_ => _.GetSnapshotCreationTimeUtc("older snapshot")) + .Returns(DateTime.UtcNow - _minBackgroundUpgradePeriod - TimeSpan.FromSeconds(1)); + + _mockStorage.Setup(_ => _.CreateNewSnapshotPath()).Returns("new snapshot path"); + + _mockInstaller.Setup( + _ => _.InstallSnapshot( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())); + + using (var dummyPowerShell = PowerShell.Create()) + using (var maintainer = CreateMaintainerWithMocks(false, _minBackgroundUpgradePeriod)) + { + maintainer.Start("current snapshot", _dependencyManifest, _mockLogger.Object); + + // ReSharper disable once AccessToDisposedClosure + var installedSnapshotPath = maintainer.InstallAndPurgeSnapshots(() => dummyPowerShell, _mockLogger.Object); + Assert.Equal(null, installedSnapshotPath); + + // ReSharper disable once AccessToDisposedClosure + _mockInstaller.Verify( + _ => _.InstallSnapshot(_dependencyManifest, "new snapshot path", dummyPowerShell, DependencySnapshotInstallationMode.Optional, _mockLogger.Object), + Times.Never); + + _mockLogger.Verify(_ => _.Log(false, LogLevel.Warning, PowerShellWorkerStrings.AutomaticUpgradesAreDisabled, null), Times.Once); + } + } + [Fact] public void DoesNotInstallSnapshotIfRecentlyInstalledSnapshotFound() { @@ -80,7 +115,7 @@ public void DoesNotInstallSnapshotIfRecentlyInstalledSnapshotFound() _mockStorage.Setup(_ => _.GetSnapshotCreationTimeUtc("older snapshot")) .Returns(DateTime.UtcNow - _minBackgroundUpgradePeriod + TimeSpan.FromSeconds(1)); - using (var maintainer = CreateMaintainerWithMocks(_minBackgroundUpgradePeriod)) + using (var maintainer = CreateMaintainerWithMocks(true, _minBackgroundUpgradePeriod)) { maintainer.Start("current snapshot", _dependencyManifest, _mockLogger.Object); @@ -112,7 +147,7 @@ public void LogsWarningIfCannotInstallSnapshot() .Throws(injectedException); using (var dummyPowerShell = PowerShell.Create()) - using (var maintainer = CreateMaintainerWithMocks(_minBackgroundUpgradePeriod)) + using (var maintainer = CreateMaintainerWithMocks(true, _minBackgroundUpgradePeriod)) { maintainer.Start("current snapshot", _dependencyManifest, _mockLogger.Object); @@ -129,12 +164,13 @@ public void LogsWarningIfCannotInstallSnapshot() Times.Once); } - private BackgroundDependencySnapshotMaintainer CreateMaintainerWithMocks(TimeSpan? minBackgroundUpgradePeriod = null) + private BackgroundDependencySnapshotMaintainer CreateMaintainerWithMocks(bool shouldPerformManagedDependenciesUpgrades, TimeSpan? minBackgroundUpgradePeriod = null) { var maintainer = new BackgroundDependencySnapshotMaintainer( _mockStorage.Object, _mockInstaller.Object, - _mockPurger.Object); + _mockPurger.Object, + () => { return shouldPerformManagedDependenciesUpgrades; }); if (minBackgroundUpgradePeriod != null) {