From b22ed89708bf1b4e05ec4b89d774f841be431331 Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Fri, 28 Jun 2024 11:04:54 -0400 Subject: [PATCH] Clear dependency check cache when starting a deployment --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 10 ++- .../Controllers/DeploymentController.cs | 4 + .../SystemCapabilityEvaluator.cs | 27 ++++-- .../SystemCapabilityEvaluatorTests.cs | 90 ++++++++++++++++++- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 5446369f3..612818a20 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -128,6 +128,10 @@ public async Task ExecuteAsync(string applicationName, string deploymentProjectP return; } + // Because we're starting a deployment, clear the cached system capabilities checks + // in case the deployment fails and the user reruns it after modifying Docker or Node + _systemCapabilityEvaluator.ClearCachedCapabilityChecks(); + await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication); if (saveSettingsConfig.SettingsType != SaveSettingsType.None) @@ -282,14 +286,14 @@ private void DisplayOutputResources(List displayedResourc /// The selected recommendation settings used for deployment. public async Task EvaluateSystemCapabilities(Recommendation selectedRecommendation) { - var systemCapabilities = await _systemCapabilityEvaluator.EvaluateSystemCapabilities(selectedRecommendation); + var missingSystemCapabilities = await _systemCapabilityEvaluator.EvaluateSystemCapabilities(selectedRecommendation); var missingCapabilitiesMessage = ""; - foreach (var capability in systemCapabilities) + foreach (var capability in missingSystemCapabilities) { missingCapabilitiesMessage = $"{missingCapabilitiesMessage}{Environment.NewLine}{capability.GetMessage()}{Environment.NewLine}"; } - if (systemCapabilities.Any()) + if (missingSystemCapabilities.Any()) throw new MissingSystemCapabilityException(DeployToolErrorCode.MissingSystemCapabilities, missingCapabilitiesMessage); } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 4ceb91d23..da87716a8 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -582,6 +582,10 @@ public async Task StartDeployment(string sessionId) if (capabilities.Any()) return Problem($"Unable to start deployment due to missing system capabilities.{Environment.NewLine}{missingCapabilitiesMessage}", statusCode: Microsoft.AspNetCore.Http.StatusCodes.Status424FailedDependency); + // Because we're starting a deployment, clear the cached system capabilities checks + // in case the deployment fails and the user reruns it after modifying Docker or Node + systemCapabilityEvaluator.ClearCachedCapabilityChecks(); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); state.DeploymentTask = task.Execute(); diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index b82272e15..5108e1a97 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -14,6 +14,13 @@ namespace AWS.Deploy.Orchestration { public interface ISystemCapabilityEvaluator { + /// + /// Clears the cache of successful capability checks, to ensure + /// that next time + /// is called they will be evaluated again. + /// + void ClearCachedCapabilityChecks(); + Task> EvaluateSystemCapabilities(Recommendation selectedRecommendation); } @@ -42,19 +49,22 @@ public class SystemCapabilityEvaluator : ISystemCapabilityEvaluator /// If we ran a successful Node evaluation, this is the timestamp until which that result /// is valid and we will skip subsequent evaluations /// - private DateTime? _nodeDependencyValidUntilUtc = null; + private DateTime _nodeDependencyValidUntilUtc = DateTime.MinValue; /// /// If we ran a successful Docker evaluation, this is the timestamp until which that result /// is valid and we will skip subsequent evaluations /// - private DateTime? _dockerDependencyValidUntilUtc = null; + private DateTime _dockerDependencyValidUntilUtc = DateTime.MinValue; public SystemCapabilityEvaluator(ICommandLineWrapper commandLineWrapper) { _commandLineWrapper = commandLineWrapper; } + /// + /// Attempt to determine whether Docker is running and its current OS type + /// private async Task HasDockerInstalledAndRunningAsync() { var processExitCode = -1; @@ -92,8 +102,7 @@ await _commandLineWrapper.Run( } /// - /// From https://docs.aws.amazon.com/cdk/latest/guide/work-with.html#work-with-prerequisites, - /// min version is 10.3 + /// Attempt to determine the installed Node.js version /// private async Task GetNodeJsVersionAsync() { @@ -135,7 +144,7 @@ public async Task> EvaluateSystemCapabilities(Recommendat if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.CdkProject) { // If we haven't cached that NodeJS installation is valid, or the cache is expired - if (_nodeDependencyValidUntilUtc == null || DateTime.UtcNow >= _nodeDependencyValidUntilUtc) + if (DateTime.UtcNow >= _nodeDependencyValidUntilUtc) { var nodeInfo = await GetNodeJsVersionAsync(); @@ -161,7 +170,7 @@ public async Task> EvaluateSystemCapabilities(Recommendat // We only need to check that Docker is installed if the user is deploying a recipe that uses Docker if (selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) { - if (_dockerDependencyValidUntilUtc == null || DateTime.UtcNow >= _dockerDependencyValidUntilUtc) + if (DateTime.UtcNow >= _dockerDependencyValidUntilUtc) { var dockerInfo = await HasDockerInstalledAndRunningAsync(); @@ -184,5 +193,11 @@ public async Task> EvaluateSystemCapabilities(Recommendat return missingCapabilitiesForRecipe; } + + public void ClearCachedCapabilityChecks() + { + _nodeDependencyValidUntilUtc = DateTime.MinValue; + _dockerDependencyValidUntilUtc = DateTime.MinValue; + } } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/SystemCapabilityEvaluatorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/SystemCapabilityEvaluatorTests.cs index eb3e0c72d..6f1fc92fd 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/SystemCapabilityEvaluatorTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/SystemCapabilityEvaluatorTests.cs @@ -1,12 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; +using System.Collections; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using AWS.Deploy.CLI.Common.UnitTests.Utilities; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Utilities; +using Moq; using Xunit; namespace AWS.Deploy.Orchestration.UnitTests @@ -42,12 +47,35 @@ public async Task CdkAndContainerRecipe_NoMissing_Cache() Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); // we still expect the first two commands, since the results should be cached } + [Fact] + public async Task CdkAndContainerRecipe_NoMissing_CacheClearing() + { + var commandLineWrapper = new TestCommandLineWrapper(); + commandLineWrapper.MockedResults.Add(_expectedNodeCommand, new TryRunResult { ExitCode = 0, StandardOut = "v18.16.1" }); + commandLineWrapper.MockedResults.Add(_expectedDockerCommand, new TryRunResult { ExitCode = 0, StandardOut = "linux" }); + + var evaluator = new SystemCapabilityEvaluator(commandLineWrapper); + var missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkAndContainerRecommendation); + + Assert.Empty(missingCapabilities); + Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); + Assert.Contains(commandLineWrapper.CommandsToExecute, command => command.Command == _expectedNodeCommand); + Assert.Contains(commandLineWrapper.CommandsToExecute, command => command.Command == _expectedDockerCommand); + + // Evaluate again after clearing the cache to verify that the checks are run again + evaluator.ClearCachedCapabilityChecks(); + missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkAndContainerRecommendation); + + Assert.Empty(missingCapabilities); + Assert.Equal(4, commandLineWrapper.CommandsToExecute.Count); + } + [Fact] public async Task CdkAndContainerRecipe_MissingDocker_NoCache() { var commandLineWrapper = new TestCommandLineWrapper(); commandLineWrapper.MockedResults.Add(_expectedNodeCommand, new TryRunResult { ExitCode = 0, StandardOut = "v18.16.1" }); - commandLineWrapper.MockedResults.Add(_expectedDockerCommand, new TryRunResult { ExitCode = -1, StandardOut = "windows" }); + commandLineWrapper.MockedResults.Add(_expectedDockerCommand, new TryRunResult { ExitCode = -1, StandardOut = "" }); var evaluator = new SystemCapabilityEvaluator(commandLineWrapper); var missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkAndContainerRecommendation); @@ -106,6 +134,27 @@ public async Task ContainerOnlyRecipe_DockerMissing_NoCache() Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); // verify that this was incremented for the second check } + [Fact] + public async Task ContainerOnlyRecipe_DockerInWindowsMode_NoCache() + { + var commandLineWrapper = new TestCommandLineWrapper(); + commandLineWrapper.MockedResults.Add(_expectedNodeCommand, new TryRunResult { ExitCode = 0, StandardOut = "v18.16.1" }); + commandLineWrapper.MockedResults.Add(_expectedDockerCommand, new TryRunResult { ExitCode = 0, StandardOut = "windows" }); + + var evaluator = new SystemCapabilityEvaluator(commandLineWrapper); + var missingCapabilities = await evaluator.EvaluateSystemCapabilities(_containerOnlyRecommendation); + + Assert.Single(missingCapabilities); + Assert.Single(commandLineWrapper.CommandsToExecute); // only expect Docker, since don't need CDK for the ECR recipe + Assert.Contains(commandLineWrapper.CommandsToExecute, command => command.Command == _expectedDockerCommand); + + // Evaluate again, to verify that it checks Docker again + missingCapabilities = await evaluator.EvaluateSystemCapabilities(_containerOnlyRecommendation); + + Assert.Single(missingCapabilities); + Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); // verify that this was incremented for the second check + } + [Fact] public async Task CdkOnlyRecipe_NoMissing_Cache() { @@ -147,5 +196,44 @@ public async Task CdkOnlyRecipe_MissingNode_NoCache() Assert.Single(missingCapabilities); Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); // verify that this was incremented for the second check } + + [Fact] + public async Task CdkOnlyRecipe_NodeTooOld_NoCache() + { + var commandLineWrapper = new TestCommandLineWrapper(); + commandLineWrapper.MockedResults.Add(_expectedNodeCommand, new TryRunResult { ExitCode = 0, StandardOut = "v10.24.1" }); + commandLineWrapper.MockedResults.Add(_expectedDockerCommand, new TryRunResult { ExitCode = 0, StandardOut = "linux" }); + + var evaluator = new SystemCapabilityEvaluator(commandLineWrapper); + var missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkOnlyRecommendation); + + Assert.Single(missingCapabilities); + Assert.Single(commandLineWrapper.CommandsToExecute); // even though Node is installed, it's older than the minimum required version + Assert.Contains(commandLineWrapper.CommandsToExecute, command => command.Command == _expectedNodeCommand); + + // Evaluate again, to verify that it checks Node again + missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkOnlyRecommendation); + + Assert.Single(missingCapabilities); + Assert.Equal(2, commandLineWrapper.CommandsToExecute.Count); // verify that this was incremented for the second check + } + + + [Fact] + public async Task CdkAndContainerRecipe_ChecksTimeout() + { + // Mock the CommandLineWrapper to throw TaskCanceledException, which is similar to if the node or docker commands timed out + var mock = new Mock(); + mock.Setup(x => x.Run(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).ThrowsAsync(new TaskCanceledException()); + + var evaluator = new SystemCapabilityEvaluator(mock.Object); + var missingCapabilities = await evaluator.EvaluateSystemCapabilities(_cdkAndContainerRecommendation); + + // Assert that both Node and Docker are reported missing for a CDK+Container recipe + Assert.Equal(2, missingCapabilities.Count); + } + + } }