Skip to content

Commit

Permalink
Clear dependency check cache when starting a deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
ashovlin committed Jun 28, 2024
1 parent 21125fd commit b22ed89
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 10 deletions.
10 changes: 7 additions & 3 deletions src/AWS.Deploy.CLI/Commands/DeployCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -282,14 +286,14 @@ private void DisplayOutputResources(List<DisplayedResourceItem> displayedResourc
/// <param name="selectedRecommendation">The selected recommendation settings used for deployment.<see cref="Recommendation"/></param>
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,10 @@ public async Task<IActionResult> 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();

Expand Down
27 changes: 21 additions & 6 deletions src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ namespace AWS.Deploy.Orchestration
{
public interface ISystemCapabilityEvaluator
{
/// <summary>
/// Clears the cache of successful capability checks, to ensure
/// that next time <see cref="EvaluateSystemCapabilities(Recommendation)"/>
/// is called they will be evaluated again.
/// </summary>
void ClearCachedCapabilityChecks();

Task<List<SystemCapability>> EvaluateSystemCapabilities(Recommendation selectedRecommendation);
}

Expand Down Expand Up @@ -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
/// </summary>
private DateTime? _nodeDependencyValidUntilUtc = null;
private DateTime _nodeDependencyValidUntilUtc = DateTime.MinValue;

/// <summary>
/// If we ran a successful Docker evaluation, this is the timestamp until which that result
/// is valid and we will skip subsequent evaluations
/// </summary>
private DateTime? _dockerDependencyValidUntilUtc = null;
private DateTime _dockerDependencyValidUntilUtc = DateTime.MinValue;

public SystemCapabilityEvaluator(ICommandLineWrapper commandLineWrapper)
{
_commandLineWrapper = commandLineWrapper;
}

/// <summary>
/// Attempt to determine whether Docker is running and its current OS type
/// </summary>
private async Task<DockerInfo> HasDockerInstalledAndRunningAsync()
{
var processExitCode = -1;
Expand Down Expand Up @@ -92,8 +102,7 @@ await _commandLineWrapper.Run(
}

/// <summary>
/// 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
/// </summary>
private async Task<NodeInfo> GetNodeJsVersionAsync()
{
Expand Down Expand Up @@ -135,7 +144,7 @@ public async Task<List<SystemCapability>> 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();

Expand All @@ -161,7 +170,7 @@ public async Task<List<SystemCapability>> 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();

Expand All @@ -184,5 +193,11 @@ public async Task<List<SystemCapability>> EvaluateSystemCapabilities(Recommendat

return missingCapabilitiesForRecipe;
}

public void ClearCachedCapabilityChecks()
{
_nodeDependencyValidUntilUtc = DateTime.MinValue;
_dockerDependencyValidUntilUtc = DateTime.MinValue;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<ICommandLineWrapper>();
mock.Setup(x => x.Run(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<Action<TryRunResult>>(), It.IsAny<bool>(),
It.IsAny<string>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())).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);
}


}
}

0 comments on commit b22ed89

Please sign in to comment.