From bcede96b75df6bc0361f19c9f1876361c446f080 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Wed, 11 Aug 2021 15:43:26 +0000 Subject: [PATCH 1/4] build: version bump to 0.16 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 567425880..a9d6f1ac6 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.15", + "version": "0.16", "publicReleaseRefSpec": [ ".*" ], From 3a3d197c5f7dd65152ada14a8ab6750d52118611 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 5 Aug 2021 17:48:32 -0700 Subject: [PATCH 2/4] fix: Run integration tests in an independent sandbox. --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 122 +++++++++++------- .../Commands/DeleteDeploymentCommand.cs | 56 ++++++-- .../GenerateDeploymentProjectCommand.cs | 2 +- .../CdkProjectHandler.cs | 10 +- .../TemplateEngine.cs | 15 ++- .../DeploymentManifestFileTests.cs | 6 +- .../Extensions/DirectoryCopyExtension.cs | 42 ++++++ .../IO/TestAppManager.cs | 24 ++++ .../Services/InMemoryInteractiveService.cs | 5 + .../BlazorWasmTests.cs | 8 +- .../ECSFargateDeploymentTest.cs | 33 ++--- .../ElasticBeanStalkDeploymentTest.cs | 29 +++-- .../ConsoleAppTests.cs | 8 +- .../Helpers/CloudFormationHelper.cs | 16 +-- .../CustomRecipeLocatorTests.cs | 121 ++++++----------- .../RecommendationTests.cs | 121 ++++++----------- .../SaveCdkDeploymentProjectTests.cs | 92 ++++--------- .../SaveCdkDeploymentProject/Utilities.cs | 11 +- .../ServerModeTests.cs | 11 +- .../InMemoryInteractiveServiceTests.cs | 8 ++ .../WebAppNoDockerFileTests.cs | 9 +- .../WebAppWithDockerFileTests.cs | 13 +- testapps/ConsoleAppService/Dockerfile | 8 +- testapps/ConsoleAppTask/Dockerfile | 8 +- .../ElasticBeanStalkConfigFile.json | 0 .../WebAppNoDockerFile.csproj | 6 + testapps/WebAppWithDockerFile/Dockerfile | 8 +- .../ECSFargateConfigFile.json | 0 .../WebAppWithDockerFile.csproj | 6 + 29 files changed, 421 insertions(+), 377 deletions(-) create mode 100644 test/AWS.Deploy.CLI.Common.UnitTests/Extensions/DirectoryCopyExtension.cs create mode 100644 test/AWS.Deploy.CLI.Common.UnitTests/IO/TestAppManager.cs rename {test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles => testapps/WebAppNoDockerFile}/ElasticBeanStalkConfigFile.json (100%) rename {test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles => testapps/WebAppWithDockerFile}/ECSFargateConfigFile.json (100%) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index de5f6e392..114359715 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -6,6 +6,8 @@ using System.CommandLine.Invocation; using System.IO; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; using Amazon; using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Utilities; @@ -33,12 +35,14 @@ public class CommandFactory : ICommandFactory private static readonly Option _optionRegion = new("--region", "AWS region to deploy the application to. For example, us-west-2."); private static readonly Option _optionProjectPath = new("--project-path", () => Directory.GetCurrentDirectory(), "Path to the project to deploy."); private static readonly Option _optionStackName = new("--stack-name", "Name the AWS stack to deploy your application to."); - private static readonly Option _optionDiagnosticLogging = new(new []{"-d", "--diagnostics"}, "Enable diagnostic output."); + private static readonly Option _optionDiagnosticLogging = new(new[] { "-d", "--diagnostics" }, "Enable diagnostic output."); private static readonly Option _optionApply = new("--apply", "Path to the deployment settings file to be applied."); - private static readonly Option _optionDisableInteractive = new(new []{"-s", "--silent" }, "Disable interactivity to deploy without any prompts for user input."); - private static readonly Option _optionOutputDirectory = new(new[]{"-o", "--output"}, "Directory path in which the CDK deployment project will be saved."); + private static readonly Option _optionDisableInteractive = new(new[] { "-s", "--silent" }, "Disable interactivity to deploy without any prompts for user input."); + private static readonly Option _optionOutputDirectory = new(new[] { "-o", "--output" }, "Directory path in which the CDK deployment project will be saved."); private static readonly Option _optionProjectDisplayName = new(new[] { "--project-display-name" }, "The name of the deployment project that will be displayed in the list of available deployment options."); private static readonly Option _optionDeploymentProject = new(new[] { "--deployment-project" }, "The absolute or relative path of the CDK project that will be used for deployment"); + private static readonly object s_root_command_lock = new(); + private static readonly object s_child_command_lock = new(); private readonly IToolInteractiveService _toolInteractiveService; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; @@ -106,35 +110,41 @@ public Command BuildRootCommand() { // Name is important to set here to show correctly in the CLI usage help. // Either dotnet-aws or dotnet aws works from the CLI. System.Commandline's help system does not like having a space with dotnet aws. - var rootCommand = new RootCommand { + var rootCommand = new RootCommand + { Name = "dotnet-aws", Description = "The AWS .NET deployment tool for deploying .NET applications on AWS." }; - rootCommand.Add(BuildDeployCommand()); - rootCommand.Add(BuildListCommand()); - rootCommand.Add(BuildDeleteCommand()); - rootCommand.Add(BuildDeploymentProjectCommand()); - rootCommand.Add(BuildServerModeCommand()); + lock(s_root_command_lock) + { + rootCommand.Add(BuildDeployCommand()); + rootCommand.Add(BuildListCommand()); + rootCommand.Add(BuildDeleteCommand()); + rootCommand.Add(BuildDeploymentProjectCommand()); + rootCommand.Add(BuildServerModeCommand()); + } return rootCommand; } private Command BuildDeployCommand() { - var deployCommand = new Command( + var deployCommand = new Command( "deploy", - "Inspect, build, and deploy the .NET project to AWS using the recommended AWS service.") + "Inspect, build, and deploy the .NET project to AWS using the recommended AWS service."); + + lock (s_child_command_lock) { - _optionProfile, - _optionRegion, - _optionProjectPath, - _optionStackName, - _optionApply, - _optionDiagnosticLogging, - _optionDisableInteractive, - _optionDeploymentProject - }; + deployCommand.Add(_optionProfile); + deployCommand.Add(_optionRegion); + deployCommand.Add(_optionProjectPath); + deployCommand.Add(_optionStackName); + deployCommand.Add(_optionApply); + deployCommand.Add(_optionDiagnosticLogging); + deployCommand.Add(_optionDisableInteractive); + deployCommand.Add(_optionDeploymentProject); + } deployCommand.Handler = CommandHandler.Create(async (DeployCommandHandlerInput input) => { @@ -142,7 +152,7 @@ private Command BuildDeployCommand() { _toolInteractiveService.Diagnostics = input.Diagnostics; _toolInteractiveService.DisableInteractive = input.Silent; - + var userDeploymentSettings = !string.IsNullOrEmpty(input.Apply) ? UserDeploymentSettings.ReadSettings(input.Apply) : null; @@ -156,7 +166,7 @@ private Command BuildDeployCommand() var systemCapabilities = _systemCapabilityEvaluator.Evaluate(); var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? ""); - + var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); var session = new OrchestratorSession( @@ -208,6 +218,7 @@ private Command BuildDeployCommand() _toolInteractiveService.WriteErrorLine(string.Empty); _toolInteractiveService.WriteErrorLine(e.Message); } + // bail out with an non-zero return code. return CommandReturnCodes.USER_ERROR; } @@ -226,14 +237,16 @@ private Command BuildDeployCommand() private Command BuildDeleteCommand() { - var deleteCommand = new Command("delete-deployment", "Delete an existing deployment.") + var deleteCommand = new Command("delete-deployment", "Delete an existing deployment."); + lock (s_child_command_lock) { - _optionProfile, - _optionRegion, - _optionProjectPath, - _optionDiagnosticLogging, - new Argument("deployment-name") - }; + deleteCommand.Add(_optionProfile); + deleteCommand.Add(_optionRegion); + deleteCommand.Add(_optionProjectPath); + deleteCommand.Add(_optionDiagnosticLogging); + deleteCommand.AddArgument(new Argument("deployment-name")); + } + deleteCommand.Handler = CommandHandler.Create(async (DeleteCommandHandlerInput input) => { try @@ -269,6 +282,7 @@ private Command BuildDeleteCommand() _toolInteractiveService.WriteErrorLine(string.Empty); _toolInteractiveService.WriteErrorLine(e.Message); } + // bail out with an non-zero return code. return CommandReturnCodes.USER_ERROR; } @@ -287,13 +301,15 @@ private Command BuildDeleteCommand() private Command BuildListCommand() { - var listCommand = new Command("list-deployments", "List existing deployments.") + var listCommand = new Command("list-deployments", "List existing deployments."); + lock (s_child_command_lock) { - _optionProfile, - _optionRegion, - _optionProjectPath, - _optionDiagnosticLogging - }; + listCommand.Add(_optionProfile); + listCommand.Add(_optionRegion); + listCommand.Add(_optionProjectPath); + listCommand.Add(_optionDiagnosticLogging); + } + listCommand.Handler = CommandHandler.Create(async (ListCommandHandlerInput input) => { try @@ -345,13 +361,15 @@ private Command BuildDeploymentProjectCommand() "Save the deployment project inside a user provided directory path."); var generateDeploymentProjectCommand = new Command("generate", - "Save the deployment project inside a user provided directory path without proceeding with a deployment") + "Save the deployment project inside a user provided directory path without proceeding with a deployment"); + + lock (s_child_command_lock) { - _optionOutputDirectory, - _optionDiagnosticLogging, - _optionProjectPath, - _optionProjectDisplayName - }; + generateDeploymentProjectCommand.Add(_optionOutputDirectory); + generateDeploymentProjectCommand.Add(_optionDiagnosticLogging); + generateDeploymentProjectCommand.Add(_optionProjectPath); + generateDeploymentProjectCommand.Add(_optionProjectDisplayName); + } generateDeploymentProjectCommand.Handler = CommandHandler.Create(async (GenerateDeploymentProjectCommandHandlerInput input) => { @@ -397,6 +415,7 @@ private Command BuildDeploymentProjectCommand() _toolInteractiveService.WriteErrorLine(string.Empty); _toolInteractiveService.WriteErrorLine(e.Message); } + // bail out with an non-zero return code. return CommandReturnCodes.USER_ERROR; } @@ -411,7 +430,11 @@ private Command BuildDeploymentProjectCommand() } }); - deploymentProjectCommand.Add(generateDeploymentProjectCommand); + lock (s_child_command_lock) + { + deploymentProjectCommand.Add(generateDeploymentProjectCommand); + } + return deploymentProjectCommand; } @@ -419,13 +442,16 @@ private Command BuildServerModeCommand() { var serverModeCommand = new Command( "server-mode", - "Launches the tool in a server mode for IDEs like Visual Studio to integrate with.") + "Launches the tool in a server mode for IDEs like Visual Studio to integrate with."); + + lock (s_child_command_lock) { - new Option(new []{"--port"}, description: "Port the server mode will listen to."), - new Option(new []{"--parent-pid"}, description: "The ID of the process that is launching server mode. Server mode will exit when the parent pid terminates."), - new Option(new []{"--encryption-keyinfo-stdin"}, description: "If set the cli reads encryption key info from stdin to use for decryption."), - _optionDiagnosticLogging - }; + serverModeCommand.Add(new Option(new[] { "--port" }, description: "Port the server mode will listen to.")); + serverModeCommand.Add(new Option(new[] { "--parent-pid" }, description: "The ID of the process that is launching server mode. Server mode will exit when the parent pid terminates.")); + serverModeCommand.Add(new Option(new[] { "--encryption-keyinfo-stdin" }, description: "If set the cli reads encryption key info from stdin to use for decryption.")); + serverModeCommand.Add(_optionDiagnosticLogging); + } + serverModeCommand.Handler = CommandHandler.Create(async (ServerModeCommandHandlerInput input) => { try diff --git a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs index 51ddc0564..a8cd92947 100644 --- a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs @@ -23,6 +23,7 @@ public class DeleteDeploymentCommand private readonly IToolInteractiveService _interactiveService; private readonly IAmazonCloudFormation _cloudFormationClient; private readonly IConsoleUtilities _consoleUtilities; + private const int MAX_RETRIES = 4; public DeleteDeploymentCommand(IAWSClientFactory awsClientFactory, IToolInteractiveService interactiveService, IConsoleUtilities consoleUtilities) { @@ -68,10 +69,6 @@ await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest await WaitForStackDelete(stackName); _interactiveService.WriteLine($"{stackName}: deleted"); } - catch (AmazonCloudFormationException) - { - throw new FailedToDeleteException($"Failed to delete {stackName} stack."); - } finally { // Stop monitoring CloudFormation stack status once the deletion operation finishes @@ -136,19 +133,52 @@ private async Task WaitForStackDelete(string stackName) private async Task GetStackAsync(string stackName) { - try + var retryCount = 0; + var shouldRetry = false; + + Stack? stack = null; + do { - var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest + var waitTime = GetWaitTime(retryCount); + try { - StackName = stackName - }); + await Task.Delay(waitTime); - return response.Stacks.Count == 0 ? null : response.Stacks[0]; - } - catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist")) - { - return null; + var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest + { + StackName = stackName + }); + + stack = response.Stacks.Count == 0 ? null : response.Stacks[0]; + shouldRetry = false; + } + catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist")) + { + shouldRetry = false; + } + catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ThrottlingException")) + { + _interactiveService.WriteDebugLine(exception.PrettyPrint()); + shouldRetry = true; + } + } while (shouldRetry && retryCount++ < MAX_RETRIES); + + return stack; + } + + /// + /// Returns the next wait interval, in milliseconds, using an exponential backoff algorithm + /// Read more here https://docs.aws.amazon.com/general/latest/gr/api-retries.html + /// + /// + /// + private static TimeSpan GetWaitTime(int retryCount) { + if (retryCount == 0) { + return TimeSpan.Zero; } + + var waitTime = Math.Pow(2, retryCount) * 5; + return TimeSpan.FromSeconds(waitTime); } } } diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs index f03151b5a..cefa81fed 100644 --- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -102,7 +102,7 @@ public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDispla } } - await _cdkProjectHandler.CreateCdkProjectForDeployment(selectedRecommendation, _session, saveCdkDirectoryPath); + _cdkProjectHandler.CreateCdkProjectForDeployment(selectedRecommendation, _session, saveCdkDirectoryPath); await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveCdkDirectoryPath, projectDisplayName); var saveCdkDirectoryFullPath = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).FullName; diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 5a0a1b67d..187f3c745 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -13,8 +13,8 @@ namespace AWS.Deploy.Orchestration { public interface ICdkProjectHandler { - public Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); - public Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); + Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); + string CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); } public class CdkProjectHandler : ICdkProjectHandler @@ -53,7 +53,7 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat { // Create a new temporary CDK project for a new deployment _interactiveService.LogMessageLine($"Generating a {recommendation.Recipe.Name} CDK Project"); - cdkProjectPath = await CreateCdkProjectForDeployment(recommendation, session); + cdkProjectPath = CreateCdkProjectForDeployment(recommendation, session); } // Write required configuration in appsettings.json @@ -82,7 +82,7 @@ await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{ throw new FailedToDeployCDKAppException("We had an issue deploying your application to AWS. Check the deployment output for more details."); } - public async Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) + public string CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) { string? assemblyName; if (string.IsNullOrEmpty(saveCdkDirectoryPath)) @@ -105,7 +105,7 @@ public async Task CreateCdkProjectForDeployment(Recommendation recommend _directoryManager.CreateDirectory(saveCdkDirectoryPath); var templateEngine = new TemplateEngine(); - await templateEngine.GenerateCDKProjectFromTemplate(recommendation, session, saveCdkDirectoryPath, assemblyName); + templateEngine.GenerateCDKProjectFromTemplate(recommendation, session, saveCdkDirectoryPath, assemblyName); return saveCdkDirectoryPath; } diff --git a/src/AWS.Deploy.Orchestration/TemplateEngine.cs b/src/AWS.Deploy.Orchestration/TemplateEngine.cs index 8fc658b02..361737624 100644 --- a/src/AWS.Deploy.Orchestration/TemplateEngine.cs +++ b/src/AWS.Deploy.Orchestration/TemplateEngine.cs @@ -25,13 +25,14 @@ public class TemplateEngine private const string HostIdentifier = "aws-net-deploy-template-generator"; private const string HostVersion = "v1.0.0"; private readonly Bootstrapper _bootstrapper; + private static readonly object s_locker = new(); public TemplateEngine() { _bootstrapper = new Bootstrapper(CreateHost(), null, virtualizeConfiguration: true); } - public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, OrchestratorSession session, string outputDirectory, string assemblyName) + public void GenerateCDKProjectFromTemplate(Recommendation recommendation, OrchestratorSession session, string outputDirectory, string assemblyName) { if (string.IsNullOrEmpty(recommendation.Recipe.CdkProjectTemplate)) { @@ -73,8 +74,11 @@ public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, try { - //Generate the CDK project using the installed template into the output directory - await _bootstrapper.CreateAsync(template, assemblyName, outputDirectory, templateParameters, false, ""); + lock (s_locker) + { + //Generate the CDK project using the installed template into the output directory + _bootstrapper.CreateAsync(template, assemblyName, outputDirectory, templateParameters, false, "").GetAwaiter().GetResult(); + } } catch { @@ -86,7 +90,10 @@ private void InstallTemplates(string folderLocation) { try { - _bootstrapper.Install(folderLocation); + lock (s_locker) + { + _bootstrapper.Install(folderLocation); + } } catch(Exception e) { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs index e5fe7f380..db0182737 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs @@ -5,12 +5,12 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using Newtonsoft.Json; using Xunit; using Should; -using AWS.Deploy.CLI.Common.UnitTests.IO; namespace AWS.Deploy.CLI.Common.UnitTests.DeploymentManifestFile { @@ -22,6 +22,7 @@ public class DeploymentManifestFileTests : IDisposable private readonly string _targetApplicationFullPath; private readonly string _targetApplicationDirectoryFullPath; private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly TestAppManager _testAppManager; private bool _isDisposed; @@ -30,7 +31,8 @@ public DeploymentManifestFileTests() _fileManager = new FileManager(); _directoryManager = new DirectoryManager(); _testDirectoryManager = new TestDirectoryManager(); - var targetApplicationPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + _testAppManager = new TestAppManager(); + var targetApplicationPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"));; _targetApplicationFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).FullName; _targetApplicationDirectoryFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).Parent.FullName; _deploymentManifestEngine = new DeploymentManifestEngine(_testDirectoryManager, _fileManager); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Extensions/DirectoryCopyExtension.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Extensions/DirectoryCopyExtension.cs new file mode 100644 index 000000000..f2788c88d --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Extensions/DirectoryCopyExtension.cs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; + +namespace AWS.Deploy.CLI.Common.UnitTests.Extensions +{ + public static class DirectoryCopyExtension + { + /// + /// + /// + public static void CopyTo(this DirectoryInfo dir, string destDirName, bool copySubDirs) + { + if (!dir.Exists) + { + throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {dir.FullName}"); + } + + var dirs = dir.GetDirectories(); + + Directory.CreateDirectory(destDirName); + + var files = dir.GetFiles(); + foreach (var file in files) + { + var tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, false); + } + + if (copySubDirs) + { + foreach (var subdir in dirs) + { + var tempPath = Path.Combine(destDirName, subdir.Name); + var subDir = new DirectoryInfo(subdir.FullName); + subDir.CopyTo(tempPath, copySubDirs); + } + } + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestAppManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestAppManager.cs new file mode 100644 index 000000000..4c2b6ed60 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestAppManager.cs @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AWS.Deploy.CLI.Common.UnitTests.Extensions; + +namespace AWS.Deploy.CLI.Common.UnitTests.IO +{ + public class TestAppManager + { + public string GetProjectPath(string path) + { + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var sourceTestAppsDir = new DirectoryInfo("testapps"); + var tempTestAppsPath = Path.Combine(tempDir, "testapps"); + Directory.CreateDirectory(tempTestAppsPath); + sourceTestAppsDir.CopyTo(tempTestAppsPath, true); + return Path.Combine(tempDir, path); + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs index 5a77b56f6..5e6663c89 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs @@ -121,6 +121,11 @@ public string ReadLine() var readLine = _stdInReader.ReadLine(); + if (readLine == null) + { + throw new InvalidOperationException(); + } + // Save the BaseStream position for future reads. _stdInReaderPosition = _stdInReader.BaseStream.Position; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs index db86378ba..37ed888d7 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -24,6 +25,7 @@ public class BlazorWasmTests : IDisposable private readonly App _app; private string _stackName; private bool _isDisposed; + private readonly TestAppManager _testAppManager; public BlazorWasmTests() { @@ -44,6 +46,8 @@ public BlazorWasmTests() _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Theory] @@ -60,7 +64,7 @@ public async Task DefaultConfigurations(params string[] components) await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var deployArgs = new[] { "deploy", "--project-path", Path.Combine(components), "--stack-name", _stackName }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -92,7 +96,7 @@ public async Task DefaultConfigurations(params string[] components) // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs index d80a1ba09..91a316beb 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -7,6 +7,7 @@ using Amazon.CloudFormation; using Amazon.ECS; using Amazon.ECS.Model; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -18,7 +19,6 @@ namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment { - [Collection("WebAppWithDockerFile")] public class ECSFargateDeploymentTest : IDisposable { private readonly HttpHelper _httpHelper; @@ -27,9 +27,9 @@ public class ECSFargateDeploymentTest : IDisposable private readonly App _app; private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; - private readonly string _stackName; - private readonly string _clusterName; - private readonly string _configFilePath; + private string _stackName; + private string _clusterName; + private readonly TestAppManager _testAppManager; public ECSFargateDeploymentTest() { @@ -48,28 +48,29 @@ public ECSFargateDeploymentTest() var serviceProvider = serviceCollection.BuildServiceProvider(); - _configFilePath = Path.Combine("ConfigFileDeployment", "TestFiles", "IntegrationTestFiles", "ECSFargateConfigFile.json"); - - ConfigFileHelper.ReplacePlaceholders(_configFilePath); - - var userDeploymentSettings = UserDeploymentSettings.ReadSettings(_configFilePath); - - _stackName = userDeploymentSettings.StackName; - _clusterName = userDeploymentSettings.LeafOptionSettingItems["ECSCluster.NewClusterName"]; - _app = serviceProvider.GetService(); Assert.NotNull(_app); _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Fact] public async Task PerformDeployment() { // Deploy - var projectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", _configFilePath, "--silent" }; + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ECSFargateConfigFile.json"); + ConfigFileHelper.ReplacePlaceholders(configFilePath); + + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(configFilePath); + + _stackName = userDeploymentSettings.StackName; + _clusterName = userDeploymentSettings.LeafOptionSettingItems["ECSCluster.NewClusterName"]; + + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent" }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -105,7 +106,7 @@ public async Task PerformDeployment() // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs index ca064ae8f..d717652f5 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -17,7 +18,6 @@ namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment { - [Collection("WebAppNoDockerFile")] public class ElasticBeanStalkDeploymentTest : IDisposable { private readonly HttpHelper _httpHelper; @@ -25,8 +25,8 @@ public class ElasticBeanStalkDeploymentTest : IDisposable private readonly App _app; private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; - private readonly string _stackName; - private readonly string _configFilePath; + private string _stackName; + private readonly TestAppManager _testAppManager; public ElasticBeanStalkDeploymentTest() { @@ -42,27 +42,28 @@ public ElasticBeanStalkDeploymentTest() var serviceProvider = serviceCollection.BuildServiceProvider(); - _configFilePath = Path.Combine("ConfigFileDeployment", "TestFiles", "IntegrationTestFiles", "ElasticBeanStalkConfigFile.json"); - - ConfigFileHelper.ReplacePlaceholders(_configFilePath); - - var userDeploymentSettings = UserDeploymentSettings.ReadSettings(_configFilePath); - - _stackName = userDeploymentSettings.StackName; - _app = serviceProvider.GetService(); Assert.NotNull(_app); _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Fact] public async Task PerformDeployment() { // Deploy - var projectPath = Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj"); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", _configFilePath, "--silent" }; + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ElasticBeanStalkConfigFile.json"); + ConfigFileHelper.ReplacePlaceholders(configFilePath); + + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(configFilePath); + + _stackName = userDeploymentSettings.StackName; + + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent" }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -94,7 +95,7 @@ public async Task PerformDeployment() // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs index f8031bbf9..8e0825164 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs @@ -8,6 +8,7 @@ using Amazon.CloudFormation; using Amazon.CloudWatchLogs; using Amazon.ECS; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -26,6 +27,7 @@ public class ConsoleAppTests : IDisposable private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; + private readonly TestAppManager _testAppManager; public ConsoleAppTests() { @@ -50,6 +52,8 @@ public ConsoleAppTests() _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Theory] @@ -65,7 +69,7 @@ public async Task DefaultConfigurations(params string[] components) await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var deployArgs = new[] { "deploy", "--project-path", Path.Combine(components), "--stack-name", _stackName }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -95,7 +99,7 @@ public async Task DefaultConfigurations(params string[] components) // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/CloudFormationHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/CloudFormationHelper.cs index 8e1474a47..4305254a8 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/CloudFormationHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/CloudFormationHelper.cs @@ -40,20 +40,12 @@ public async Task IsStackDeleted(string stackName) public async Task DeleteStack(string stackName) { - try + var request = new DeleteStackRequest() { - var request = new DeleteStackRequest() - { - StackName = stackName - }; + StackName = stackName + }; - await _cloudFormationClient.DeleteStackAsync(request); - } - catch (AmazonCloudFormationException) - { - // Don't throw an error if the stack does not exist. Most likely a test has failed before a stack was actually created. If we - // throw the exception here it will hide the original error. - } + await _cloudFormationClient.DeleteStackAsync(request); } private async Task GetStackAsync(string stackName) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs index c179eb09b..c031e7362 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs @@ -9,119 +9,78 @@ using Xunit; using Task = System.Threading.Tasks.Task; using Should; -using System; -using AWS.Deploy.Common; +using AWS.Deploy.CLI.Common.UnitTests.IO; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { - [Collection("SaveCdkDeploymentProjectTests")] - public class CustomRecipeLocatorTests : IDisposable + public class CustomRecipeLocatorTests { - private readonly string _webAppWithDockerFilePath; - private readonly string _webAppWithNoDockerFilePath; - private readonly string _webAppWithDockerCsproj; - private readonly string _webAppNoDockerCsproj; - private readonly string _testArtifactsDirectoryPath; - private readonly string _solutionDirectoryPath; - private readonly ICustomRecipeLocator _customRecipeLocator; - - private bool _isDisposed; + private readonly CommandLineWrapper _commandLineWrapper; public CustomRecipeLocatorTests() { - var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); - - _webAppWithDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); - _webAppWithNoDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppNoDockerFile"); - - _webAppWithDockerCsproj = Path.Combine(_webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); - _webAppNoDockerCsproj = Path.Combine(_webAppWithNoDockerFilePath, "WebAppNoDockerFile.csproj"); - - _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); - - var directoryManager = new DirectoryManager(); - var fileManager = new FileManager(); - var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); - var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); - var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); - var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); - _customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, commandLineWrapper, directoryManager); - - var solutionPath = Path.Combine(testAppsDirectoryPath, "..", "AWS.Deploy.sln"); - _solutionDirectoryPath = directoryManager.GetDirectoryInfo(solutionPath).Parent.FullName; + _commandLineWrapper = new CommandLineWrapper(new ConsoleOrchestratorLogger(new ConsoleInteractiveServiceImpl())); } [Fact] public async Task LocateCustomRecipePathsWithManifestFile() { + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var webAppWithDockerCsproj = Path.Combine(webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); + var solutionDirectoryPath = tempDirectoryPath; + var customRecipeLocator = BuildCustomRecipeLocator(); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot - await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp2")); + await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp1")); + await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp2")); // ACT - Fetch custom recipes corresponding to the same target application that has a deployment-manifest file. - var customRecipePaths = await _customRecipeLocator.LocateCustomRecipePaths(_webAppWithDockerCsproj, _solutionDirectoryPath); + var customRecipePaths = await customRecipeLocator.LocateCustomRecipePaths(webAppWithDockerCsproj, solutionDirectoryPath); // ASSERT - File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); + File.Exists(Path.Combine(webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); customRecipePaths.Count.ShouldEqual(2); - customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - - CleanUp(); + customRecipePaths.ShouldContain(Path.Combine(tempDirectoryPath, "MyCdkApp1")); + customRecipePaths.ShouldContain(Path.Combine(tempDirectoryPath, "MyCdkApp1")); } [Fact] public async Task LocateCustomRecipePathsWithoutManifestFile() { + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var webAppNoDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppNoDockerFile"); + var webAppWithDockerCsproj = Path.Combine(webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); + var webAppNoDockerCsproj = Path.Combine(webAppNoDockerFilePath, "WebAppNoDockerFile.csproj"); + var solutionDirectoryPath = tempDirectoryPath; + var customRecipeLocator = BuildCustomRecipeLocator(); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot - await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp2")); + await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp1")); + await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp2")); // ACT - Fetch custom recipes corresponding to a different target application (under source control) without a deployment-manifest file. - var customRecipePaths = await _customRecipeLocator.LocateCustomRecipePaths(_webAppNoDockerCsproj, _solutionDirectoryPath); + var customRecipePaths = await customRecipeLocator.LocateCustomRecipePaths(webAppNoDockerCsproj, solutionDirectoryPath); // ASSERT - File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); + File.Exists(Path.Combine(webAppNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); customRecipePaths.Count.ShouldEqual(2); - customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); - - CleanUp(); + customRecipePaths.ShouldContain(Path.Combine(tempDirectoryPath, "MyCdkApp1")); + customRecipePaths.ShouldContain(Path.Combine(tempDirectoryPath, "MyCdkApp1")); } - private void CleanUp() + private ICustomRecipeLocator BuildCustomRecipeLocator() { - if (Directory.Exists(_testArtifactsDirectoryPath)) - Directory.Delete(_testArtifactsDirectoryPath, true); - - if (File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json"))) - File.Delete(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")); - - if (File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json"))) - File.Delete(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_isDisposed) return; - - if (disposing) - { - CleanUp(); - } - - _isDisposed = true; - } - - ~CustomRecipeLocatorTests() - { - Dispose(false); + var directoryManager = new DirectoryManager(); + var fileManager = new FileManager(); + var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); + var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); + var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); + var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); + return new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, commandLineWrapper, directoryManager); } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index ef9958848..60eb06b58 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -20,32 +20,27 @@ using Should; using AWS.Deploy.Common.Recipes; using Newtonsoft.Json; +using AWS.Deploy.CLI.Common.UnitTests.IO; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { - [Collection("SaveCdkDeploymentProjectTests")] - public class RecommendationTests : IDisposable + public class RecommendationTests { - private readonly string _testArtifactsDirectoryPath; - private readonly string _webAppWithDockerFilePath; - private readonly string _webAppWithNoDockerFilePath; - private readonly string _blazorAppPath; - private bool _isDisposed; + private readonly CommandLineWrapper _commandLineWrapper; public RecommendationTests() { - var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); - _webAppWithDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); - _webAppWithNoDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppNoDockerFile"); - _blazorAppPath = Path.Combine(testAppsDirectoryPath, "BlazorWasm50"); - _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); + _commandLineWrapper = new CommandLineWrapper(new ConsoleOrchestratorLogger(new ConsoleInteractiveServiceImpl())); } [Fact] public async Task GenerateRecommendationsWithoutCustomRecipes() { // ARRANGE - var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var orchestrator = await GetOrchestrator(webAppWithDockerFilePath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); // ACT var recommendations = await orchestrator.GenerateDeploymentRecommendations(); @@ -55,27 +50,28 @@ public async Task GenerateRecommendationsWithoutCustomRecipes() recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe recommendations[1].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe - - CleanUp(); } [Fact] public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() { // ARRANGE - var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var orchestrator = await GetOrchestrator(webAppWithDockerFilePath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); - var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); - var saveDirectoryPathEbsProject = Path.Combine(_testArtifactsDirectoryPath, "EBS-CDK"); + var saveDirectoryPathEcsProject = Path.Combine(tempDirectoryPath, "ECS-CDK"); + var saveDirectoryPathEbsProject = Path.Combine(tempDirectoryPath, "EBS-CDK"); var customEcsRecipeName = "Custom ECS Fargate Recipe"; var customEbsRecipeName = "Custom Elastic Beanstalk Recipe"; // select ECS Fargate recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + await Utilities.CreateCDKDeploymentProjectWithRecipeName(webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); // select Elastic Beanstalk recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEbsRecipeName, "2", saveDirectoryPathEbsProject); + await Utilities.CreateCDKDeploymentProjectWithRecipeName(webAppWithDockerFilePath, customEbsRecipeName, "3", saveDirectoryPathEbsProject); // Get custom recipe IDs var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -100,28 +96,30 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() recommendations[0].Recipe.Id.ShouldEqual(customEcsRecipeId); recommendations[1].Recipe.Id.ShouldEqual(customEbsRecipeId); - File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); - - CleanUp(); + File.Exists(Path.Combine(webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); } [Fact] public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() { // ARRANGE - var orchestrator = await GetOrchestrator(_webAppWithNoDockerFilePath); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var webAppNoDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppNoDockerFile"); + var orchestrator = await GetOrchestrator(webAppNoDockerFilePath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); - var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); - var saveDirectoryPathEbsProject = Path.Combine(_testArtifactsDirectoryPath, "EBS-CDK"); + var saveDirectoryPathEcsProject = Path.Combine(tempDirectoryPath, "ECS-CDK"); + var saveDirectoryPathEbsProject = Path.Combine(tempDirectoryPath, "EBS-CDK"); var customEcsRecipeName = "Custom ECS Fargate Recipe"; var customEbsRecipeName = "Custom Elastic Beanstalk Recipe"; // Select ECS Fargate recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + await Utilities.CreateCDKDeploymentProjectWithRecipeName(webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); // Select Elastic Beanstalk recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEbsRecipeName, "2", saveDirectoryPathEbsProject); + await Utilities.CreateCDKDeploymentProjectWithRecipeName(webAppWithDockerFilePath, customEbsRecipeName, "3", saveDirectoryPathEbsProject); // Get custom recipe IDs var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -145,21 +143,23 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() // ASSERT - Custom recipe IDs recommendations[0].Recipe.Id.ShouldEqual(customEbsRecipeId); recommendations[1].Recipe.Id.ShouldEqual(customEcsRecipeId); - File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); - - CleanUp(); + File.Exists(Path.Combine(webAppNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); } [Fact] public async Task GenerateRecommendationsFromCompatibleDeploymentProject() { // ARRANGE - var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); - var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var orchestrator = await GetOrchestrator(webAppWithDockerFilePath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + + var saveDirectoryPathEcsProject = Path.Combine(tempDirectoryPath, "ECS-CDK"); var customEcsRecipeName = "Custom ECS Fargate Recipe"; // Select ECS Fargate recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + await Utilities.CreateCDKDeploymentProjectWithRecipeName(webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); // Get custom recipe IDs var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -172,25 +172,26 @@ public async Task GenerateRecommendationsFromCompatibleDeploymentProject() recommendations[0].Name.ShouldEqual("Custom ECS Fargate Recipe"); recommendations[0].Recipe.Id.ShouldEqual(customEcsRecipeId); recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); - - CleanUp(); } [Fact] public async Task GenerateRecommendationsFromIncompatibleDeploymentProject() { // ARRANGE - var orchestrator = await GetOrchestrator(_blazorAppPath); - var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); - await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, saveDirectoryPathEcsProject); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var blazorAppPath = Path.Combine(tempDirectoryPath, "testapps", "BlazorWasm50"); + var orchestrator = await GetOrchestrator(blazorAppPath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + + var saveDirectoryPathEcsProject = Path.Combine(tempDirectoryPath, "ECS-CDK"); + await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, saveDirectoryPathEcsProject); // ACT var recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(saveDirectoryPathEcsProject); // ASSERT recommendations.ShouldBeEmpty(); - - CleanUp(); } private async Task GetOrchestrator(string targetApplicationProjectPath) @@ -223,43 +224,5 @@ private async Task GetCustomRecipeId(string recipeFilePath) var recipe = JsonConvert.DeserializeObject(recipeBody); return recipe.Id; } - - private void CleanUp() - { - if (Directory.Exists(_testArtifactsDirectoryPath)) - Directory.Delete(_testArtifactsDirectoryPath, true); - - if (File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json"))) - File.Delete(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")); - - if (File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json"))) - File.Delete(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")); - - if (File.Exists(Path.Combine(_blazorAppPath, "aws-deployments.json"))) - File.Delete(Path.Combine(_blazorAppPath, "aws-deployments.json")); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_isDisposed) return; - - if (disposing) - { - CleanUp(); - } - - _isDisposed = true; - } - - ~RecommendationTests() - { - Dispose(false); - } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs index 397e779dd..a9327eed6 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs @@ -1,101 +1,65 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System; using System.IO; -using System.Linq; -using AWS.Deploy.CLI.IntegrationTests.Extensions; -using AWS.Deploy.CLI.IntegrationTests.Services; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.CLI.Utilities; using Xunit; using Task = System.Threading.Tasks.Task; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { - [Collection("SaveCdkDeploymentProjectTests")] - public class SaveCdkDeploymentProjectTests : IDisposable + public class SaveCdkDeploymentProjectTests { - private readonly string _targetApplicationProjectPath; - private readonly string _deploymentManifestFilePath; - private readonly string _testArtifactsDirectoryPath; - - private bool _isDisposed; - private string _saveDirectoryPath; + private readonly CommandLineWrapper _commandLineWrapper; public SaveCdkDeploymentProjectTests() { - var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); - _targetApplicationProjectPath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); - _deploymentManifestFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile", "aws-deployments.json"); - _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); + _commandLineWrapper = new CommandLineWrapper(new ConsoleOrchestratorLogger(new ConsoleInteractiveServiceImpl())); } [Fact] public async Task DefaultSaveDirectory() { - _saveDirectoryPath = _targetApplicationProjectPath + "CDK"; - await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath); - CleanUp(); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + var targetApplicationProjectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + + await Utilities.CreateCDKDeploymentProject(targetApplicationProjectPath); } [Fact] public async Task CustomSaveDirectory() { - _saveDirectoryPath = Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp"); - await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath); - CleanUp(); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + var targetApplicationProjectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + + var saveDirectoryPath = Path.Combine(tempDirectoryPath, "DeploymentProjects", "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(targetApplicationProjectPath, saveDirectoryPath); } [Fact] public async Task InvalidSaveCdkDirectoryInsideProjectDirectory() { - - _saveDirectoryPath = Path.Combine(_targetApplicationProjectPath, "MyCdkApp"); - await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath, false); - CleanUp(); + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + var targetApplicationProjectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + + var saveDirectoryPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile", "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(targetApplicationProjectPath, saveDirectoryPath, false); } [Fact] public async Task InvalidNonEmptySaveCdkDirectory() { - Directory.CreateDirectory(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp", "MyFolder")); - _saveDirectoryPath = Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp"); - await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath, false); - CleanUp(); - } - - private void CleanUp() - { - if (Directory.Exists(_testArtifactsDirectoryPath)) - Directory.Delete(_testArtifactsDirectoryPath, true); - - if (File.Exists(_deploymentManifestFilePath)) - File.Delete(_deploymentManifestFilePath); - - if (Directory.Exists(_saveDirectoryPath)) - Directory.Delete(_saveDirectoryPath, true); - } + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + var targetApplicationProjectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_isDisposed) return; - - if (disposing) - { - CleanUp(); - } - - _isDisposed = true; - } - - ~SaveCdkDeploymentProjectTests() - { - Dispose(false); + Directory.CreateDirectory(Path.Combine(tempDirectoryPath, "MyCdkApp", "MyFolder")); + var saveDirectoryPath = Path.Combine(tempDirectoryPath, "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(targetApplicationProjectPath, saveDirectoryPath, false); } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs index 2f9b9d3a8..80976f61c 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs @@ -26,6 +26,7 @@ public static async Task CreateCDKDeploymentProject(string targetApplicationPath // Arrange input for saving the CDK deployment project await interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + await interactiveService.StdInWriter.FlushAsync(); string[] deployArgs; // default save directory @@ -100,16 +101,6 @@ public static async Task CreateCDKDeploymentProjectWithRecipeName(string targetA VerifyCreatedArtifacts(targetApplicationPath, saveDirectoryPath); } - public static string ResolvePathToTestApps() - { - var testsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - while (testsPath != null && !string.Equals(new DirectoryInfo(testsPath).Name, "test", StringComparison.OrdinalIgnoreCase)) - { - testsPath = Directory.GetParent(testsPath).FullName; - } - return new DirectoryInfo(Path.Combine(testsPath, "..", "testapps")).FullName; - } - private static (App app, InMemoryInteractiveService interactiveService) GetAppServiceProvider() { var serviceCollection = new ServiceCollection(); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index 6262295e5..ef0cd9208 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -15,6 +15,7 @@ using Amazon.Runtime; using Amazon.Runtime.CredentialManagement; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -27,7 +28,6 @@ namespace AWS.Deploy.CLI.IntegrationTests { - [Collection("WebAppWithDockerFile")] public class ServerModeTests : IDisposable { private bool _isDisposed; @@ -36,6 +36,7 @@ public class ServerModeTests : IDisposable private readonly CloudFormationHelper _cloudFormationHelper; private readonly string _awsRegion; + private readonly TestAppManager _testAppManager; public ServerModeTests() { @@ -50,6 +51,8 @@ public ServerModeTests() _serviceProvider = serviceCollection.BuildServiceProvider(); _awsRegion = "us-west-2"; + + _testAppManager = new TestAppManager(); } public Task ResolveCredentials() @@ -61,7 +64,7 @@ public Task ResolveCredentials() [Fact] public async Task GetRecommendations() { - var projectPath = Path.GetFullPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4000; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); @@ -96,7 +99,7 @@ public async Task GetRecommendations() [Fact] public async Task GetRecommendationsWithEncryptedCredentials() { - var projectPath = Path.GetFullPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4000; var aes = Aes.Create(); @@ -153,7 +156,7 @@ public async Task WebFargateDeploymentNoConfigChanges() { _stackName = $"ServerModeWebFargate{Guid.NewGuid().ToString().Split('-').Last()}"; - var projectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4001; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs index 0a2347959..0bcd67878 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs @@ -3,6 +3,7 @@ using System; using Xunit; +using Should; namespace AWS.Deploy.CLI.IntegrationTests.Services { @@ -113,5 +114,12 @@ public void ReadKey() Assert.Equal(ConsoleKey.E, service.ReadKey(false).Key); Assert.Equal(ConsoleKey.F, service.ReadKey(false).Key); } + + [Fact] + public void ReadLineSetToNull() + { + var service = new InMemoryInteractiveService(); + Assert.Throws(() => service.ReadLine()); + } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index 7f238acde..f019b50d1 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -16,7 +17,6 @@ namespace AWS.Deploy.CLI.IntegrationTests { - [Collection("WebAppNoDockerFile")] public class WebAppNoDockerFileTests : IDisposable { private readonly HttpHelper _httpHelper; @@ -25,6 +25,7 @@ public class WebAppNoDockerFileTests : IDisposable private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; + private readonly TestAppManager _testAppManager; public WebAppNoDockerFileTests() { @@ -45,6 +46,8 @@ public WebAppNoDockerFileTests() _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Fact] @@ -58,7 +61,7 @@ public async Task DefaultConfigurations() await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var projectPath = Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj"); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName }; await _app.Run(deployArgs); @@ -91,7 +94,7 @@ public async Task DefaultConfigurations() // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index 77752db15..1c19f6245 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs @@ -7,6 +7,7 @@ using Amazon.CloudFormation; using Amazon.ECS; using Amazon.ECS.Model; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; @@ -17,7 +18,6 @@ namespace AWS.Deploy.CLI.IntegrationTests { - [Collection("WebAppWithDockerFile")] public class WebAppWithDockerFileTests : IDisposable { private readonly HttpHelper _httpHelper; @@ -27,6 +27,7 @@ public class WebAppWithDockerFileTests : IDisposable private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; + private readonly TestAppManager _testAppManager; public WebAppWithDockerFileTests() { @@ -50,6 +51,8 @@ public WebAppWithDockerFileTests() _interactiveService = serviceProvider.GetService(); Assert.NotNull(_interactiveService); + + _testAppManager = new TestAppManager(); } [Fact] @@ -63,7 +66,7 @@ public async Task DefaultConfigurations() await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var projectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName }; await _app.Run(deployArgs); @@ -99,7 +102,7 @@ public async Task DefaultConfigurations() // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } @@ -114,7 +117,7 @@ public async Task AppRunnerDeployment() await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var projectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName }; await _app.Run(deployArgs); @@ -148,7 +151,7 @@ public async Task AppRunnerDeployment() // Delete await _app.Run(deleteArgs); - // Verify application is delete + // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); } diff --git a/testapps/ConsoleAppService/Dockerfile b/testapps/ConsoleAppService/Dockerfile index f77725519..aac38ad70 100644 --- a/testapps/ConsoleAppService/Dockerfile +++ b/testapps/ConsoleAppService/Dockerfile @@ -7,10 +7,10 @@ WORKDIR /app FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build WORKDIR /src -COPY ["testapps/ConsoleAppService/ConsoleAppService.csproj", "testapps/ConsoleAppService/"] -RUN dotnet restore "testapps/ConsoleAppService/ConsoleAppService.csproj" -COPY "testapps/ConsoleAppService/" "testapps/ConsoleAppService/" -WORKDIR "/src/testapps/ConsoleAppService" +COPY ["ConsoleAppService.csproj", "ConsoleAppService/"] +RUN dotnet restore "ConsoleAppService/ConsoleAppService.csproj" +COPY . "ConsoleAppService/" +WORKDIR "/src/ConsoleAppService" RUN dotnet build "ConsoleAppService.csproj" -c Release -o /app/build FROM build AS publish diff --git a/testapps/ConsoleAppTask/Dockerfile b/testapps/ConsoleAppTask/Dockerfile index acce814c4..6cdc61603 100644 --- a/testapps/ConsoleAppTask/Dockerfile +++ b/testapps/ConsoleAppTask/Dockerfile @@ -7,10 +7,10 @@ WORKDIR /app FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build WORKDIR /src -COPY ["testapps/ConsoleAppTask/ConsoleAppTask.csproj", "testapps/ConsoleAppTask/"] -RUN dotnet restore "testapps/ConsoleAppTask/ConsoleAppTask.csproj" -COPY "testapps/ConsoleAppTask/" "testapps/ConsoleAppTask/" -WORKDIR "/src/testapps/ConsoleAppTask" +COPY ["ConsoleAppTask.csproj", "ConsoleAppTask/"] +RUN dotnet restore "ConsoleAppTask/ConsoleAppTask.csproj" +COPY . "ConsoleAppTask/" +WORKDIR "/src/ConsoleAppTask" RUN dotnet build "ConsoleAppTask.csproj" -c Release -o /app/build FROM build AS publish diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ElasticBeanStalkConfigFile.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json similarity index 100% rename from test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ElasticBeanStalkConfigFile.json rename to testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json diff --git a/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj b/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj index 842a7700e..72e6c6788 100644 --- a/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj +++ b/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj @@ -4,4 +4,10 @@ net5.0 + + + Always + + + diff --git a/testapps/WebAppWithDockerFile/Dockerfile b/testapps/WebAppWithDockerFile/Dockerfile index b3e4f7b01..6a33d609d 100644 --- a/testapps/WebAppWithDockerFile/Dockerfile +++ b/testapps/WebAppWithDockerFile/Dockerfile @@ -9,10 +9,10 @@ EXPOSE 443 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build WORKDIR /src -COPY ["testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj", "testapps/WebAppWithDockerFile/"] -RUN dotnet restore "testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj" -COPY "testapps/WebAppWithDockerFile/" "testapps/WebAppWithDockerFile/" -WORKDIR "/src/testapps/WebAppWithDockerFile" +COPY ["WebAppWithDockerFile.csproj", "WebAppWithDockerFile/"] +RUN dotnet restore "WebAppWithDockerFile/WebAppWithDockerFile.csproj" +COPY . "WebAppWithDockerFile/" +WORKDIR "/src/WebAppWithDockerFile" RUN dotnet build "WebAppWithDockerFile.csproj" -c Release -o /app/build FROM build AS publish diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ECSFargateConfigFile.json b/testapps/WebAppWithDockerFile/ECSFargateConfigFile.json similarity index 100% rename from test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ECSFargateConfigFile.json rename to testapps/WebAppWithDockerFile/ECSFargateConfigFile.json diff --git a/testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj b/testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj index 912711f58..c22f51a78 100644 --- a/testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj +++ b/testapps/WebAppWithDockerFile/WebAppWithDockerFile.csproj @@ -11,4 +11,10 @@ + + + Always + + + From 765bc871a316487f9c82ec74dbe0474a260669b4 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Mon, 16 Aug 2021 12:10:44 -0400 Subject: [PATCH 3/4] feat: Save last used stack and order existing deployments by MRU stack --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 40 +++- .../Commands/DeleteDeploymentCommand.cs | 21 +- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 27 +-- .../GenerateDeploymentProjectCommand.cs | 2 +- .../CustomServiceCollectionExtension.cs | 2 + .../Controllers/DeploymentController.cs | 27 ++- .../Models/ExistingDeploymentSummary.cs | 10 +- src/AWS.Deploy.Common/CloudApplication.cs | 15 +- .../DeploymentManifestEngine.cs | 13 +- .../DeploymentManifestModel.cs | 8 +- src/AWS.Deploy.Common/ProjectDefinition.cs | 13 ++ ...r.cs => SerializeModelContractResolver.cs} | 3 +- src/AWS.Deploy.Orchestration/Exceptions.cs | 17 ++ .../LocalUserSettings/LastDeployedStack.cs | 46 ++++ .../LocalUserSettings/LocalUserSettings.cs | 17 ++ .../LocalUserSettingsEngine.cs | 208 ++++++++++++++++++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 12 +- .../RecommendationEngine.cs | 2 +- .../Utilities/DeployedApplicationQueryer.cs | 83 +++++-- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 6 + .../DeploymentManifestFileTests.cs | 2 +- .../IO/TestFileManager.cs | 58 +++++ .../LocalUserSettingsTests.cs | 92 ++++++++ .../RecommendationTests.cs | 3 + .../DeployedApplicationQueryerTests.cs | 25 ++- .../TestDirectoryManager.cs | 47 ++++ 26 files changed, 733 insertions(+), 66 deletions(-) rename src/AWS.Deploy.Common/{SerializeRecipeContractResolver.cs => SerializeModelContractResolver.cs} (91%) create mode 100644 src/AWS.Deploy.Orchestration/LocalUserSettings/LastDeployedStack.cs create mode 100644 src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettings.cs create mode 100644 src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs create mode 100644 test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs create mode 100644 test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 114359715..24114702f 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -21,6 +21,7 @@ using AWS.Deploy.Common.IO; using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Orchestration.DisplayedResources; +using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.Commands { @@ -61,8 +62,11 @@ public class CommandFactory : ICommandFactory private readonly ITypeHintCommandFactory _typeHintCommandFactory; private readonly IDisplayedResourcesHandler _displayedResourceHandler; private readonly IConsoleUtilities _consoleUtilities; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; private readonly IDeploymentManifestEngine _deploymentManifestEngine; private readonly ICustomRecipeLocator _customRecipeLocator; + private readonly ILocalUserSettingsEngine _localUserSettingsEngine; public CommandFactory( IToolInteractiveService toolInteractiveService, @@ -82,8 +86,11 @@ public CommandFactory( ITypeHintCommandFactory typeHintCommandFactory, IDisplayedResourcesHandler displayedResourceHandler, IConsoleUtilities consoleUtilities, + IDirectoryManager directoryManager, + IFileManager fileManager, IDeploymentManifestEngine deploymentManifestEngine, - ICustomRecipeLocator customRecipeLocator) + ICustomRecipeLocator customRecipeLocator, + ILocalUserSettingsEngine localUserSettingsEngine) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -102,8 +109,11 @@ public CommandFactory( _typeHintCommandFactory = typeHintCommandFactory; _displayedResourceHandler = displayedResourceHandler; _consoleUtilities = consoleUtilities; + _directoryManager = directoryManager; + _fileManager = fileManager; _deploymentManifestEngine = deploymentManifestEngine; _customRecipeLocator = customRecipeLocator; + _localUserSettingsEngine = localUserSettingsEngine; } public Command BuildRootCommand() @@ -194,6 +204,7 @@ private Command BuildDeployCommand() _typeHintCommandFactory, _displayedResourceHandler, _cloudApplicationNameGenerator, + _localUserSettingsEngine, _consoleUtilities, _customRecipeLocator, session); @@ -269,7 +280,28 @@ private Command BuildDeleteCommand() return CommandReturnCodes.USER_ERROR; } - await new DeleteDeploymentCommand(_awsClientFactory, _toolInteractiveService, _consoleUtilities).ExecuteAsync(input.DeploymentName); + OrchestratorSession? session = null; + + try + { + var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? string.Empty); + + var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); + + session = new OrchestratorSession( + projectDefinition, + awsCredentials, + awsRegion, + callerIdentity.Account); + } + catch (FailedToFindDeployableTargetException) { } + + await new DeleteDeploymentCommand( + _awsClientFactory, + _toolInteractiveService, + _consoleUtilities, + _localUserSettingsEngine, + session).ExecuteAsync(input.DeploymentName); return CommandReturnCodes.SUCCESS; } @@ -396,8 +428,8 @@ private Command BuildDeploymentProjectCommand() _consoleUtilities, _cdkProjectHandler, _commandLineWrapper, - new DirectoryManager(), - new FileManager(), + _directoryManager, + _fileManager, session, _deploymentManifestEngine, targetApplicationFullPath); diff --git a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs index a8cd92947..8a7942e47 100644 --- a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs @@ -8,7 +8,9 @@ using Amazon.CloudFormation.Model; using AWS.Deploy.CLI.CloudFormation; using AWS.Deploy.Common; -using AWS.Deploy.Recipes.CDK.Common; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.Commands { @@ -23,14 +25,23 @@ public class DeleteDeploymentCommand private readonly IToolInteractiveService _interactiveService; private readonly IAmazonCloudFormation _cloudFormationClient; private readonly IConsoleUtilities _consoleUtilities; + private readonly ILocalUserSettingsEngine _localUserSettingsEngine; + private readonly OrchestratorSession? _session; private const int MAX_RETRIES = 4; - public DeleteDeploymentCommand(IAWSClientFactory awsClientFactory, IToolInteractiveService interactiveService, IConsoleUtilities consoleUtilities) + public DeleteDeploymentCommand( + IAWSClientFactory awsClientFactory, + IToolInteractiveService interactiveService, + IConsoleUtilities consoleUtilities, + ILocalUserSettingsEngine localUserSettingsEngine, + OrchestratorSession? session) { _awsClientFactory = awsClientFactory; _interactiveService = interactiveService; _consoleUtilities = consoleUtilities; _cloudFormationClient = _awsClientFactory.GetAWSClient(); + _localUserSettingsEngine = localUserSettingsEngine; + _session = session; } /// @@ -67,6 +78,12 @@ await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest var _ = monitor.StartAsync(); await WaitForStackDelete(stackName); + + if (_session != null) + { + await _localUserSettingsEngine.DeleteLastDeployedStack(stackName, _session.ProjectDefinition.ProjectName, _session.AWSAccountId, _session.AWSRegion); + } + _interactiveService.WriteLine($"{stackName}: deleted"); } finally diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 5121bd3eb..833c18a26 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -18,6 +18,8 @@ using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.Orchestration.DisplayedResources; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.Commands { @@ -35,6 +37,7 @@ public class DeployCommand private readonly ITypeHintCommandFactory _typeHintCommandFactory; private readonly IDisplayedResourcesHandler _displayedResourcesHandler; private readonly ICloudApplicationNameGenerator _cloudApplicationNameGenerator; + private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly IConsoleUtilities _consoleUtilities; private readonly ICustomRecipeLocator _customRecipeLocator; @@ -53,6 +56,7 @@ public DeployCommand( ITypeHintCommandFactory typeHintCommandFactory, IDisplayedResourcesHandler displayedResourcesHandler, ICloudApplicationNameGenerator cloudApplicationNameGenerator, + ILocalUserSettingsEngine localUserSettingsEngine, IConsoleUtilities consoleUtilities, ICustomRecipeLocator customRecipeLocator, OrchestratorSession session) @@ -68,6 +72,7 @@ public DeployCommand( _typeHintCommandFactory = typeHintCommandFactory; _displayedResourcesHandler = displayedResourcesHandler; _cloudApplicationNameGenerator = cloudApplicationNameGenerator; + _localUserSettingsEngine = localUserSettingsEngine; _consoleUtilities = consoleUtilities; _session = session; _cdkManager = cdkManager; @@ -131,6 +136,7 @@ private void DisplayOutputResources(List displayedResourc _cdkManager, _awsResourceQueryer, _deploymentBundleHandler, + _localUserSettingsEngine, _dockerEngine, _customRecipeLocator, new List { RecipeLocator.FindRecipeDefinitionsPath() }); @@ -142,7 +148,7 @@ private void DisplayOutputResources(List displayedResourc var allDeployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(); // Filter compatible applications that can be re-deployed using the current set of recommendations. - var compatibleApplications = GetCompatibleApplications(allDeployedApplications, recommendations); + var compatibleApplications = await _deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, _session); // Get Cloudformation stack name. var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, compatibleApplications); @@ -180,23 +186,6 @@ private void DisplayOutputResources(List displayedResourc return (orchestrator, selectedRecommendation, cloudApplication); } - /// - /// Filters the applications that can be re-deployed using the current set of available recommendations. - /// - /// - /// - /// A list of that are compatible for a re-deployment - private List GetCompatibleApplications(List allDeployedApplications, List recommendations) - { - var compatibleApplications = new List(); - foreach (var app in allDeployedApplications) - { - if (recommendations.Any(rec => string.Equals(rec.Recipe.Id, app.RecipeId, StringComparison.Ordinal))) - compatibleApplications.Add(app); - } - return compatibleApplications; - } - /// /// Checks if the system meets all the necessary requirements for deployment. /// @@ -574,7 +563,7 @@ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendat } _toolInteractiveService.WriteLine(string.Empty); - var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "true"); + var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "false"); if (answer == YesNo.Yes) { var dockerExecutionDirectory = diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs index cefa81fed..e0420d18a 100644 --- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -239,7 +239,7 @@ private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendatio { Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new SerializeRecipeContractResolver() + ContractResolver = new SerializeModelContractResolver() }); await _fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody); } diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index e8e95fbfb..673b738b2 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -12,6 +12,7 @@ using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.DisplayedResources; +using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -54,6 +55,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IZipFileManager), typeof(ZipFileManager), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentManifestEngine), typeof(DeploymentManifestEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICustomRecipeLocator), typeof(CustomRecipeLocator), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ILocalUserSettingsEngine), typeof(LocalUserSettingsEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 8436ccf0d..c0953e61a 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -26,6 +26,8 @@ using Amazon.Runtime; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.DisplayedResources; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.ServerMode.Controllers { @@ -242,13 +244,16 @@ public async Task GetExistingDeployments(string sessionId) var output = new GetExistingDeploymentsOutput(); var deployedApplicationQueryer = serviceProvider.GetRequiredService(); - state.ExistingDeployments = await deployedApplicationQueryer.GetExistingDeployedApplications(state.NewRecommendations); + var session = CreateOrchestratorSession(state); + state.ExistingDeployments = await deployedApplicationQueryer.GetCompatibleApplications(state.NewRecommendations.ToList(), session: session); foreach(var deployment in state.ExistingDeployments) { output.ExistingDeployments.Add(new ExistingDeploymentSummary( deployment.Name, - deployment.RecipeId)); + deployment.RecipeId, + deployment.LastUpdatedTime, + deployment.UpdatedByCurrentUser)); } return Ok(output); @@ -434,6 +439,16 @@ private IServiceProvider CreateSessionServiceProvider(string sessionId, string a return serviceProvider; } + private OrchestratorSession CreateOrchestratorSession(SessionState state, AWSCredentials? awsCredentials = null) + { + return new OrchestratorSession( + state.ProjectDefinition, + awsCredentials ?? HttpContext.User.ToAWSCredentials() ?? + throw new FailedToRetrieveAWSCredentialsException("The tool was not able to retrieve the AWS Credentials."), + state.AWSRegion, + state.AWSAccountId); + } + private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? serviceProvider = null, AWSCredentials? awsCredentials = null) { if(serviceProvider == null) @@ -441,12 +456,7 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider = CreateSessionServiceProvider(state); } - var session = new OrchestratorSession( - state.ProjectDefinition, - awsCredentials ?? HttpContext.User.ToAWSCredentials() ?? - throw new FailedToRetrieveAWSCredentialsException("The tool was not able to retrieve the AWS Credentials."), - state.AWSRegion, - state.AWSAccountId); + var session = CreateOrchestratorSession(state, awsCredentials); return new Orchestrator( session, @@ -455,6 +465,7 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), new DockerEngine.DockerEngine(session.ProjectDefinition), serviceProvider.GetRequiredService(), new List { RecipeLocator.FindRecipeDefinitionsPath() } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs index d4f938929..cf272e3ef 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs @@ -14,13 +14,21 @@ public class ExistingDeploymentSummary public string RecipeId { get; set; } + public DateTime? LastUpdatedTime { get; set; } + + public bool UpdatedByCurrentUser { get; set; } + public ExistingDeploymentSummary( string name, - string recipeId + string recipeId, + DateTime? lastUpdatedTime, + bool updatedByCurrentUser ) { Name = name; RecipeId = recipeId; + LastUpdatedTime = lastUpdatedTime; + UpdatedByCurrentUser = updatedByCurrentUser; } } } diff --git a/src/AWS.Deploy.Common/CloudApplication.cs b/src/AWS.Deploy.Common/CloudApplication.cs index 0cc55ccff..b012ca609 100644 --- a/src/AWS.Deploy.Common/CloudApplication.cs +++ b/src/AWS.Deploy.Common/CloudApplication.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; + namespace AWS.Deploy.Common { /// @@ -27,16 +29,27 @@ public class CloudApplication /// public string RecipeId { get; set; } + /// + /// Last updated time of CloudFormation stack + /// + public DateTime? LastUpdatedTime { get; set; } + + /// + /// Indicates whether the Cloud Application has been redeployed by the current user. + /// + public bool UpdatedByCurrentUser { get; set; } + /// /// Display the name of the Cloud Application /// /// public override string ToString() => Name; - public CloudApplication(string name, string recipeId) + public CloudApplication(string name, string recipeId, DateTime? lastUpdatedTime = null) { Name = name; RecipeId = recipeId; + LastUpdatedTime = lastUpdatedTime; } } } diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs index e0eaccb1e..d40284e80 100644 --- a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs @@ -57,7 +57,14 @@ public async Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, if (_fileManager.Exists(deploymentManifestFilePath)) { deploymentManifestModel = await ReadManifestFile(deploymentManifestFilePath); - deploymentManifestModel.DeploymentManifestEntries.Add(new DeploymentManifestEntry(saveCdkDirectoryRelativePath)); + if (deploymentManifestModel.DeploymentProjects == null) + { + deploymentManifestModel.DeploymentProjects = new List { new DeploymentManifestEntry(saveCdkDirectoryRelativePath) }; + } + else + { + deploymentManifestModel.DeploymentProjects.Add(new DeploymentManifestEntry(saveCdkDirectoryRelativePath)); + } } else { @@ -90,7 +97,9 @@ public async Task> GetRecipeDefinitionPaths(string targetApplicatio if (_fileManager.Exists(deploymentManifestFilePath)) { var deploymentManifestModel = await ReadManifestFile(deploymentManifestFilePath); - foreach (var entry in deploymentManifestModel.DeploymentManifestEntries) + if (deploymentManifestModel.DeploymentProjects == null) + return recipeDefinitionPaths; + foreach (var entry in deploymentManifestModel.DeploymentProjects) { var saveCdkDirectoryRelativePath = entry.SaveCdkDirectoryRelativePath; if (string.IsNullOrEmpty(saveCdkDirectoryRelativePath)) diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs index 030876ff1..b9a718c22 100644 --- a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Generic; -using Newtonsoft.Json; namespace AWS.Deploy.Common.DeploymentManifest { @@ -11,12 +10,11 @@ namespace AWS.Deploy.Common.DeploymentManifest /// public class DeploymentManifestModel { - [JsonProperty("deployment-projects")] - public List DeploymentManifestEntries { get; set; } + public List DeploymentProjects { get; set; } - public DeploymentManifestModel(List deploymentManifestEntries) + public DeploymentManifestModel(List deploymentProjects) { - DeploymentManifestEntries = deploymentManifestEntries; + DeploymentProjects = deploymentProjects; } } } diff --git a/src/AWS.Deploy.Common/ProjectDefinition.cs b/src/AWS.Deploy.Common/ProjectDefinition.cs index 67156bd5b..b7282023d 100644 --- a/src/AWS.Deploy.Common/ProjectDefinition.cs +++ b/src/AWS.Deploy.Common/ProjectDefinition.cs @@ -14,6 +14,11 @@ namespace AWS.Deploy.Common /// public class ProjectDefinition { + /// + /// The name of the project + /// + public string ProjectName => GetProjectName(); + /// /// Xml file contents of the Project file. /// @@ -83,5 +88,13 @@ private bool CheckIfDockerFileExists(string projectPath) throw new InvalidProjectPathException("The project path is invalid."), "Dockerfile"); return dir.Length == 1; } + + private string GetProjectName() + { + if (string.IsNullOrEmpty(ProjectPath)) + return string.Empty; + + return Path.GetFileNameWithoutExtension(ProjectPath); + } } } diff --git a/src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs b/src/AWS.Deploy.Common/SerializeModelContractResolver.cs similarity index 91% rename from src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs rename to src/AWS.Deploy.Common/SerializeModelContractResolver.cs index b93359986..741b9e5c0 100644 --- a/src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs +++ b/src/AWS.Deploy.Common/SerializeModelContractResolver.cs @@ -6,13 +6,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace AWS.Deploy.Common { - public class SerializeRecipeContractResolver : DefaultContractResolver + public class SerializeModelContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 2868a44b3..8bf83a0b1 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -165,4 +165,21 @@ public class AWSResourceNotFoundException : Exception { public AWSResourceNotFoundException(string message, Exception? innerException = null) : base(message, innerException) { } } + + /// + /// Exception thrown if the Local User Settings File is invalid. + /// + [AWSDeploymentExpectedException] + public class InvalidLocalUserSettingsFileException : Exception + { + public InvalidLocalUserSettingsFileException(string message, Exception? innerException = null) : base(message, innerException) { } + } + + /// Exception thrown if a failure occured while trying to update the Local User Settings file. + /// + [AWSDeploymentExpectedException] + public class FailedToUpdateLocalUserSettingsFileException : Exception + { + public FailedToUpdateLocalUserSettingsFileException(string message, Exception? innerException = null) : base(message, innerException) { } + } } diff --git a/src/AWS.Deploy.Orchestration/LocalUserSettings/LastDeployedStack.cs b/src/AWS.Deploy.Orchestration/LocalUserSettings/LastDeployedStack.cs new file mode 100644 index 000000000..d87e3844e --- /dev/null +++ b/src/AWS.Deploy.Orchestration/LocalUserSettings/LastDeployedStack.cs @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.Orchestration.LocalUserSettings +{ + public class LastDeployedStack + { + public string AWSAccountId { get; set; } + + public string AWSRegion { get; set; } + + public string ProjectName { get; set; } + + public List Stacks { get; set; } + + public LastDeployedStack(string awsAccountId, string awsRegion, string projectName, List stacks) + { + AWSAccountId = awsAccountId; + AWSRegion = awsRegion; + ProjectName = projectName; + Stacks = stacks; + } + + public bool Exists(string? awsAccountId, string? awsRegion, string? projectName) + { + if (string.IsNullOrEmpty(AWSAccountId) || + string.IsNullOrEmpty(AWSRegion) || + string.IsNullOrEmpty(ProjectName)) + return false; + + if (string.IsNullOrEmpty(awsAccountId) || + string.IsNullOrEmpty(awsRegion) || + string.IsNullOrEmpty(projectName)) + return false; + + if (AWSAccountId.Equals(awsAccountId) && + AWSRegion.Equals(awsRegion) && + ProjectName.Equals(projectName)) + return true; + + return false; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettings.cs b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettings.cs new file mode 100644 index 000000000..348dfacc7 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettings.cs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.Orchestration.LocalUserSettings +{ + public class LocalUserSettings + { + public List? LastDeployedStacks { get; set; } + + public LocalUserSettings(List lastDeployedStacks) + { + LastDeployedStacks = lastDeployedStacks; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs new file mode 100644 index 000000000..58c481add --- /dev/null +++ b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs @@ -0,0 +1,208 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; +using Newtonsoft.Json; + +namespace AWS.Deploy.Orchestration.LocalUserSettings +{ + public interface ILocalUserSettingsEngine + { + Task UpdateLastDeployedStack(string stackName, string projectName, string? awsAccountId, string? awsRegion); + Task DeleteLastDeployedStack(string stackName, string projectName, string? awsAccountId, string? awsRegion); + Task CleanOrphanStacks(List deployedStacks, string projectName, string? awsAccountId, string? awsRegion); + Task GetLocalUserSettings(); + } + + public class LocalUserSettingsEngine : ILocalUserSettingsEngine + { + private readonly IFileManager _fileManager; + private readonly IDirectoryManager _directoryManager; + + private const string LOCAL_USER_SETTINGS_FILE_NAME = "local-user-settings.json"; + + public LocalUserSettingsEngine(IFileManager fileManager, IDirectoryManager directoryManager) + { + _fileManager = fileManager; + _directoryManager = directoryManager; + } + + /// + /// This method updates the local user settings json file by adding the name of the stack that was most recently used. + /// If the file does not exists then a new file is generated. + /// + public async Task UpdateLastDeployedStack(string stackName, string projectName, string? awsAccountId, string? awsRegion) + { + try + { + if (string.IsNullOrEmpty(projectName)) + throw new FailedToUpdateLocalUserSettingsFileException("The Project Name is not defined."); + if (string.IsNullOrEmpty(awsAccountId) || string.IsNullOrEmpty(awsRegion)) + throw new FailedToUpdateLocalUserSettingsFileException("The AWS Account Id or Region is not defined."); + + var localUserSettings = await GetLocalUserSettings(); + var lastDeployedStack = localUserSettings?.LastDeployedStacks? + .FirstOrDefault(x => x.Exists(awsAccountId, awsRegion, projectName)); + + if (localUserSettings != null) + { + if (lastDeployedStack != null) + { + if (lastDeployedStack.Stacks == null) + { + lastDeployedStack.Stacks = new List { stackName }; + } + else + { + if (!lastDeployedStack.Stacks.Contains(stackName)) + lastDeployedStack.Stacks.Add(stackName); + lastDeployedStack.Stacks.Sort(); + } + } + else + { + localUserSettings.LastDeployedStacks = new List() { + new LastDeployedStack( + awsAccountId, + awsRegion, + projectName, + new List() { stackName })}; + } + } + else + { + + var lastDeployedStacks = new List { + new LastDeployedStack( + awsAccountId, + awsRegion, + projectName, + new List() { stackName }) }; + localUserSettings = new LocalUserSettings(lastDeployedStacks); + } + + await WriteLocalUserSettingsFile(localUserSettings); + } + catch (Exception ex) + { + throw new FailedToUpdateLocalUserSettingsFileException($"Failed to update the local user settings file " + + $"to include the last deployed to stack '{stackName}'.", ex); + } + } + + /// + /// This method updates the local user settings json file by deleting the stack that was most recently used. + /// + public async Task DeleteLastDeployedStack(string stackName, string projectName, string? awsAccountId, string? awsRegion) + { + try + { + if (string.IsNullOrEmpty(projectName)) + throw new FailedToUpdateLocalUserSettingsFileException("The Project Name is not defined."); + if (string.IsNullOrEmpty(awsAccountId) || string.IsNullOrEmpty(awsRegion)) + throw new FailedToUpdateLocalUserSettingsFileException("The AWS Account Id or Region is not defined."); + + var localUserSettings = await GetLocalUserSettings(); + var lastDeployedStack = localUserSettings?.LastDeployedStacks? + .FirstOrDefault(x => x.Exists(awsAccountId, awsRegion, projectName)); + + if (localUserSettings == null || lastDeployedStack == null) + return; + + lastDeployedStack.Stacks.Remove(stackName); + + await WriteLocalUserSettingsFile(localUserSettings); + } + catch (Exception ex) + { + throw new FailedToUpdateLocalUserSettingsFileException($"Failed to update the local user settings file " + + $"to delete the stack '{stackName}'.", ex); + } + } + + /// + /// This method updates the local user settings json file by deleting orphan stacks. + /// + public async Task CleanOrphanStacks(List deployedStacks, string projectName, string? awsAccountId, string? awsRegion) + { + try + { + if (string.IsNullOrEmpty(projectName)) + throw new FailedToUpdateLocalUserSettingsFileException("The Project Name is not defined."); + if (string.IsNullOrEmpty(awsAccountId) || string.IsNullOrEmpty(awsRegion)) + throw new FailedToUpdateLocalUserSettingsFileException("The AWS Account Id or Region is not defined."); + + var localUserSettings = await GetLocalUserSettings(); + var localStacks = localUserSettings?.LastDeployedStacks? + .FirstOrDefault(x => x.Exists(awsAccountId, awsRegion, projectName)); + + if (localUserSettings == null || localStacks == null) + return; + + var validStacks = deployedStacks.Intersect(localStacks.Stacks); + + localStacks.Stacks = validStacks.ToList(); + + await WriteLocalUserSettingsFile(localUserSettings); + } + catch (Exception ex) + { + throw new FailedToUpdateLocalUserSettingsFileException($"Failed to update the local user settings file " + + $"to delete orphan stacks.", ex); + } + } + + /// + /// This method parses the into a string and writes it to disk. + /// + private async Task WriteLocalUserSettingsFile(LocalUserSettings deploymentManifestModel) + { + var localUserSettingsFilePath = GetLocalUserSettingsFilePath(); + var settingsFilejsonString = JsonConvert.SerializeObject(deploymentManifestModel, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new SerializeModelContractResolver() + }); + + await _fileManager.WriteAllTextAsync(localUserSettingsFilePath, settingsFilejsonString); + return localUserSettingsFilePath; + } + + /// + /// This method parses the local user settings file into a + /// + public async Task GetLocalUserSettings() + { + try + { + var localUserSettingsFilePath = GetLocalUserSettingsFilePath(); + + if (!_fileManager.Exists(localUserSettingsFilePath)) + return null; + var settingsFilejsonString = await _fileManager.ReadAllTextAsync(localUserSettingsFilePath); + return JsonConvert.DeserializeObject(settingsFilejsonString); + } + catch (Exception ex) + { + throw new InvalidLocalUserSettingsFileException("The Local User Settings file is invalid.", ex); + } + } + + /// + /// This method returns the path at which the local user settings file will be stored. + /// + private string GetLocalUserSettingsFilePath() + { + var deployToolWorkspace = _directoryManager.GetDirectoryInfo(Constants.CDK.DeployToolWorkspaceDirectoryRoot).FullName; + var localUserSettingsFileFullPath = Path.Combine(deployToolWorkspace, LOCAL_USER_SETTINGS_FILE_NAME); + return localUserSettingsFileFullPath; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 1ab8ce10d..b3f4b0ce8 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -7,10 +7,12 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.DockerEngine; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; +using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Recipes; using Newtonsoft.Json; @@ -30,10 +32,10 @@ public class Orchestrator private readonly IOrchestratorInteractiveService? _interactiveService; private readonly IAWSResourceQueryer? _awsResourceQueryer; private readonly IDeploymentBundleHandler? _deploymentBundleHandler; + private readonly ILocalUserSettingsEngine? _localUserSettingsEngine; private readonly IDockerEngine? _dockerEngine; private readonly IList? _recipeDefinitionPaths; private readonly ICustomRecipeLocator? _customRecipeLocator; - private readonly OrchestratorSession? _session; public Orchestrator( @@ -43,6 +45,7 @@ public Orchestrator( ICDKManager cdkManager, IAWSResourceQueryer awsResourceQueryer, IDeploymentBundleHandler deploymentBundleHandler, + ILocalUserSettingsEngine localUserSettingsEngine, IDockerEngine dockerEngine, ICustomRecipeLocator customRecipeLocator, IList recipeDefinitionPaths) @@ -56,6 +59,7 @@ public Orchestrator( _dockerEngine = dockerEngine; _customRecipeLocator = customRecipeLocator; _recipeDefinitionPaths = recipeDefinitionPaths; + _localUserSettingsEngine = localUserSettingsEngine; } public Orchestrator(OrchestratorSession session, IList recipeDefinitionPaths) @@ -125,6 +129,8 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm throw new InvalidOperationException($"{nameof(_cdkManager)} is null as part of the orchestartor object"); if (_cdkProjectHandler == null) throw new InvalidOperationException($"{nameof(_cdkProjectHandler)} is null as part of the orchestartor object"); + if (_localUserSettingsEngine == null) + throw new InvalidOperationException($"{nameof(_localUserSettingsEngine)} is null as part of the orchestartor object"); if (_session == null) throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); @@ -144,8 +150,10 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm break; default: _interactiveService.LogErrorMessageLine($"Unknown deployment type {recommendation.Recipe.DeploymentType} specified in recipe."); - break; + return; } + + await _localUserSettingsEngine.UpdateLastDeployedStack(cloudApplication.StackName, _session.ProjectDefinition.ProjectName, _session.AWSAccountId, _session.AWSRegion); } public async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index 8a2d0592c..6d12b199c 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -74,7 +74,7 @@ public async Task> ComputeRecommendations(Dictionary recommendation.ComputedPriority).ToList(); + recommendations = recommendations.OrderByDescending(recommendation => recommendation.ComputedPriority).ThenBy(recommendation => recommendation.Name).ToList(); return recommendations; } diff --git a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs index d1df73310..d1b68d213 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs @@ -1,12 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.Data; +using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Recipes.CDK.Common; namespace AWS.Deploy.Orchestration.Utilities @@ -16,23 +19,32 @@ public interface IDeployedApplicationQueryer /// /// Get the list of existing deployed applications by describe the CloudFormation stacks and filtering the stacks to the /// ones that have the AWS .NET deployment tool tag and description. - /// - /// If has any values that only existing applications that were deployed with any of the recipes - /// identified by the recommendations will be returned. /// - Task> GetExistingDeployedApplications(IList? compatibleRecommendations = null); + Task> GetExistingDeployedApplications(); + + /// + /// Get the list of compatible applications based on the matching elements of the deployed stack and recommendation, such as Recipe Id. + /// + Task> GetCompatibleApplications(List recommendations, List? allDeployedApplications = null, OrchestratorSession? session = null); } public class DeployedApplicationQueryer : IDeployedApplicationQueryer { private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly ILocalUserSettingsEngine _localUserSettingsEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; - public DeployedApplicationQueryer(IAWSResourceQueryer awsResourceQueryer) + public DeployedApplicationQueryer( + IAWSResourceQueryer awsResourceQueryer, + ILocalUserSettingsEngine localUserSettingsEngine, + IOrchestratorInteractiveService orchestratorInteractiveService) { _awsResourceQueryer = awsResourceQueryer; + _localUserSettingsEngine = localUserSettingsEngine; + _orchestratorInteractiveService = orchestratorInteractiveService; } - public async Task> GetExistingDeployedApplications(IList? compatibleRecommendations = null) + public async Task> GetExistingDeployedApplications() { var stacks = await _awsResourceQueryer.GetCloudFormationStacks(); var apps = new List(); @@ -66,17 +78,62 @@ public async Task> GetExistingDeployedApplications(IList< // If a list of compatible recommendations was given then skip existing applications that were used with a // recipe that is not compatible. var recipeId = deployTag.Value; - if ( - compatibleRecommendations != null && - !compatibleRecommendations.Any(rec => string.Equals(rec.Recipe.Id, recipeId))) - { - continue; - } - apps.Add(new CloudApplication(stack.StackName, recipeId)); + apps.Add(new CloudApplication(stack.StackName, recipeId, stack.LastUpdatedTime)); } return apps; } + + /// + /// Filters the applications that can be re-deployed using the current set of available recommendations. + /// + /// + /// + /// A list of that are compatible for a re-deployment + public async Task> GetCompatibleApplications(List recommendations, List? allDeployedApplications = null, OrchestratorSession? session = null) + { + var compatibleApplications = new List(); + if (allDeployedApplications == null) + allDeployedApplications = await GetExistingDeployedApplications(); + + foreach (var app in allDeployedApplications) + { + if (recommendations.Any(rec => string.Equals(rec.Recipe.Id, app.RecipeId, StringComparison.Ordinal))) + compatibleApplications.Add(app); + } + + if (session != null) + { + try + { + await _localUserSettingsEngine.CleanOrphanStacks(allDeployedApplications.Select(x => x.StackName).ToList(), session.ProjectDefinition.ProjectName, session.AWSAccountId, session.AWSRegion); + var deploymentManifest = await _localUserSettingsEngine.GetLocalUserSettings(); + var lastDeployedStack = deploymentManifest?.LastDeployedStacks? + .FirstOrDefault(x => x.Exists(session.AWSAccountId, session.AWSRegion, session.ProjectDefinition.ProjectName)); + + return compatibleApplications + .Select(x => { + x.UpdatedByCurrentUser = lastDeployedStack?.Stacks?.Contains(x.StackName) ?? false; + return x; + }) + .OrderByDescending(x => x.UpdatedByCurrentUser) + .ThenByDescending(x => x.LastUpdatedTime) + .ToList(); + } + catch (FailedToUpdateLocalUserSettingsFileException ex) + { + _orchestratorInteractiveService.LogErrorMessageLine(ex.Message); + } + catch (InvalidLocalUserSettingsFileException ex) + { + _orchestratorInteractiveService.LogErrorMessageLine(ex.Message); + } + } + + return compatibleApplications + .OrderByDescending(x => x.LastUpdatedTime) + .ToList(); ; + } } } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index cf2b83d73..cf780c716 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1322,6 +1322,12 @@ public partial class ExistingDeploymentSummary [Newtonsoft.Json.JsonProperty("recipeId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string RecipeId { get; set; } + [Newtonsoft.Json.JsonProperty("lastUpdatedTime", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.DateTimeOffset? LastUpdatedTime { get; set; } + + [Newtonsoft.Json.JsonProperty("updatedByCurrentUser", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool UpdatedByCurrentUser { get; set; } + } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs index db0182737..2dacfa7c0 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs @@ -121,7 +121,7 @@ private async Task> GetDeploymentManifestEntries(string deploymentM var manifestFilejsonString = await _fileManager.ReadAllTextAsync(deploymentManifestFilePath); var deploymentManifestModel = JsonConvert.DeserializeObject(manifestFilejsonString); - foreach (var entry in deploymentManifestModel.DeploymentManifestEntries) + foreach (var entry in deploymentManifestModel.DeploymentProjects) { deploymentProjectPaths.Add(entry.SaveCdkDirectoryRelativePath); } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs new file mode 100644 index 000000000..8badfebdc --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.CLI.Common.UnitTests.IO +{ + public class TestFileManager : IFileManager + { + public readonly Dictionary InMemoryStore = new Dictionary(); + + public bool Exists(string path) + { + return InMemoryStore.ContainsKey(path); + } + + public Task ReadAllTextAsync(string path) + { + var text = InMemoryStore[path]; + return Task.FromResult(text); + } + + public async Task ReadAllLinesAsync(string path) + { + return (await ReadAllTextAsync(path)).Split(Environment.NewLine); + } + + public Task WriteAllTextAsync(string filePath, string contents, CancellationToken cancellationToken = default) + { + InMemoryStore[filePath] = contents; + return Task.CompletedTask; + } + } + + public static class TestFileManagerExtensions + { + /// + /// Adds a virtual csproj file with valid xml contents + /// + /// + /// Returns the correct full path for + /// + public static string AddEmptyProjectFile(this TestFileManager fileManager, string relativePath) + { + relativePath = relativePath.Replace('\\', Path.DirectorySeparatorChar); + var fullPath = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "c:\\" : "/", relativePath); + fileManager.InMemoryStore.Add(fullPath, ""); + + return fullPath; + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs new file mode 100644 index 000000000..461cddd54 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.LocalUserSettings; +using Xunit; + +namespace AWS.Deploy.CLI.Common.UnitTests.LocalUserSettings +{ + public class LocalUserSettingsTests + { + private readonly IFileManager _fileManager; + private readonly IDirectoryManager _directoryManager; + private readonly ILocalUserSettingsEngine _localUserSettingsEngine; + + public LocalUserSettingsTests() + { + _fileManager = new TestFileManager(); + _directoryManager = new DirectoryManager(); + var targetApplicationPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + _localUserSettingsEngine = new LocalUserSettingsEngine(_fileManager, _directoryManager); + } + + [Fact] + public async Task UpdateLastDeployedStackTest() + { + var stackName = "WebAppWithDockerFile"; + var awsAccountId = "1234567890"; + var awsRegion = "us-west-2"; + var settingsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aws-dotnet-deploy", "local-user-settings.json"); + + await _localUserSettingsEngine.UpdateLastDeployedStack(stackName, stackName, awsAccountId, awsRegion); + var userSettings = await _localUserSettingsEngine.GetLocalUserSettings(); + + Assert.True(_fileManager.Exists(settingsFilePath)); + Assert.NotNull(userSettings); + Assert.NotNull(userSettings.LastDeployedStacks); + Assert.Single(userSettings.LastDeployedStacks); + Assert.Equal(awsAccountId, userSettings.LastDeployedStacks[0].AWSAccountId); + Assert.Equal(awsRegion, userSettings.LastDeployedStacks[0].AWSRegion); + Assert.Single(userSettings.LastDeployedStacks[0].Stacks); + Assert.Equal(stackName, userSettings.LastDeployedStacks[0].Stacks[0]); + } + + [Fact] + public async Task DeleteLastDeployedStackTest() + { + var stackName = "WebAppWithDockerFile"; + var awsAccountId = "1234567890"; + var awsRegion = "us-west-2"; + var settingsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aws-dotnet-deploy", "local-user-settings.json"); + + await _localUserSettingsEngine.UpdateLastDeployedStack(stackName, stackName, awsAccountId, awsRegion); + await _localUserSettingsEngine.DeleteLastDeployedStack(stackName, stackName, awsAccountId, awsRegion); + var userSettings = await _localUserSettingsEngine.GetLocalUserSettings(); + + Assert.True(_fileManager.Exists(settingsFilePath)); + Assert.NotNull(userSettings); + Assert.NotNull(userSettings.LastDeployedStacks); + Assert.Single(userSettings.LastDeployedStacks); + Assert.Equal(awsAccountId, userSettings.LastDeployedStacks[0].AWSAccountId); + Assert.Equal(awsRegion, userSettings.LastDeployedStacks[0].AWSRegion); + Assert.Null(userSettings.LastDeployedStacks[0].Stacks); + } + + [Fact] + public async Task CleanOrphanStacksTest() + { + var stackName = "WebAppWithDockerFile"; + var awsAccountId = "1234567890"; + var awsRegion = "us-west-2"; + var settingsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aws-dotnet-deploy", "local-user-settings.json"); + + await _localUserSettingsEngine.UpdateLastDeployedStack(stackName, stackName, awsAccountId, awsRegion); + await _localUserSettingsEngine.CleanOrphanStacks(new List { "WebAppWithDockerFile1" }, stackName, awsAccountId, awsRegion); + var userSettings = await _localUserSettingsEngine.GetLocalUserSettings(); + + Assert.True(_fileManager.Exists(settingsFilePath)); + Assert.NotNull(userSettings); + Assert.NotNull(userSettings.LastDeployedStacks); + Assert.Single(userSettings.LastDeployedStacks); + Assert.Equal(awsAccountId, userSettings.LastDeployedStacks[0].AWSAccountId); + Assert.Equal(awsRegion, userSettings.LastDeployedStacks[0].AWSRegion); + Assert.Null(userSettings.LastDeployedStacks[0].Stacks); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 60eb06b58..64536e44b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -21,6 +21,7 @@ using AWS.Deploy.Common.Recipes; using Newtonsoft.Json; using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -199,6 +200,7 @@ private async Task GetOrchestrator(string targetApplicationProject var directoryManager = new DirectoryManager(); var fileManager = new FileManager(); var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); + var localUserSettingsEngine = new LocalUserSettingsEngine(fileManager, directoryManager); var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); @@ -213,6 +215,7 @@ private async Task GetOrchestrator(string targetApplicationProject new Mock().Object, new TestToolAWSResourceQueryer(), new Mock().Object, + localUserSettingsEngine, new Mock().Object, customRecipeLocator, new List { RecipeLocator.FindRecipeDefinitionsPath() }); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs index 86035c6b5..dc0b510e4 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs @@ -9,7 +9,9 @@ using Amazon.CloudFormation; using Amazon.CloudFormation.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.Data; +using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.Recipes.CDK.Common; using Moq; @@ -20,10 +22,16 @@ namespace AWS.Deploy.Orchestration.UnitTests public class DeployedApplicationQueryerTests { private readonly Mock _mockAWSResourceQueryer; + private readonly IDirectoryManager _directoryManager; + private readonly Mock _mockLocalUserSettingsEngine; + private readonly Mock _mockOrchestratorInteractiveService; public DeployedApplicationQueryerTests() { _mockAWSResourceQueryer = new Mock(); + _directoryManager = new TestDirectoryManager(); + _mockLocalUserSettingsEngine = new Mock(); + _mockOrchestratorInteractiveService = new Mock(); } [Fact] @@ -43,7 +51,10 @@ public async Task GetExistingDeployedApplications_ListDeploymentsCall() .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(new List() { stack })); - var deployedApplicationQueryer = new DeployedApplicationQueryer(_mockAWSResourceQueryer.Object); + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); var result = await deployedApplicationQueryer.GetExistingDeployedApplications(); Assert.Single(result); @@ -70,9 +81,12 @@ public async Task GetExistingDeployedApplications_DeployCall() .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(new List() { stack })); - var deployedApplicationQueryer = new DeployedApplicationQueryer(_mockAWSResourceQueryer.Object); + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); - var result = await deployedApplicationQueryer.GetExistingDeployedApplications(new List()); + var result = await deployedApplicationQueryer.GetCompatibleApplications(new List()); Assert.Empty(result); } @@ -103,7 +117,10 @@ public async Task GetExistingDeployedApplications_InvalidConfigurations(string r .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(new List() { stack })); - var deployedApplicationQueryer = new DeployedApplicationQueryer(_mockAWSResourceQueryer.Object); + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); var result = await deployedApplicationQueryer.GetExistingDeployedApplications(); Assert.Empty(result); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs new file mode 100644 index 000000000..2958f08d7 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class TestDirectoryManager : IDirectoryManager + { + public readonly HashSet CreatedDirectories = new(); + + public DirectoryInfo CreateDirectory(string path) + { + CreatedDirectories.Add(path); + return new DirectoryInfo(path); + } + + public DirectoryInfo GetDirectoryInfo(string path) => new DirectoryInfo(path); + + public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); + + public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); + + public void Delete(string path, bool recursive = false) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public bool Exists(string path) + { + return CreatedDirectories.Contains(path); + } + + public string[] GetDirectories(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public string[] GetFiles(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public bool IsEmpty(string path) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + } +} From 73d6fe37691ab0b0a7944b200df83ca5f9f98965 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Thu, 12 Aug 2021 15:53:38 -0400 Subject: [PATCH 4/4] feat: Add GetCapabilities API to check for NodeJS and Docker installations --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 4 +- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 34 ++--- src/AWS.Deploy.CLI/Exceptions.cs | 9 -- .../Controllers/DeploymentController.cs | 35 +++++ .../Models/GetCompatibilityOutput.cs | 12 ++ .../Models/SystemCapabilitySummary.cs | 21 +++ src/AWS.Deploy.Common/Exceptions.cs | 30 +---- src/AWS.Deploy.Orchestration/Exceptions.cs | 10 ++ .../OrchestratorSession.cs | 7 - .../SystemCapabilities.cs | 26 ++++ .../SystemCapabilityEvaluator.cs | 50 ++++++- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 123 ++++++++++++++++++ .../ApplyPreviousSettingsTests.cs | 4 - .../DeploymentBundleHandlerTests.cs | 4 - .../GetOptionSettingTests.cs | 4 - .../RecommendationTests.cs | 4 - .../SetOptionSettingTests.cs | 4 - .../DisplayedResourcesHandlerTests.cs | 4 - 18 files changed, 286 insertions(+), 99 deletions(-) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/GetCompatibilityOutput.cs create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs rename src/{AWS.Deploy.CLI => AWS.Deploy.Orchestration}/SystemCapabilityEvaluator.cs (51%) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 24114702f..cd7ece17e 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -173,8 +173,6 @@ private Command BuildDeployCommand() _commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion); _awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion); - var systemCapabilities = _systemCapabilityEvaluator.Evaluate(); - var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? ""); var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); @@ -185,7 +183,6 @@ private Command BuildDeployCommand() awsRegion, callerIdentity.Account) { - SystemCapabilities = systemCapabilities, AWSProfileName = input.Profile ?? userDeploymentSettings?.AWSProfile ?? null }; @@ -207,6 +204,7 @@ private Command BuildDeployCommand() _localUserSettingsEngine, _consoleUtilities, _customRecipeLocator, + _systemCapabilityEvaluator, session); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 833c18a26..c9d701b28 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -40,7 +40,7 @@ public class DeployCommand private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly IConsoleUtilities _consoleUtilities; private readonly ICustomRecipeLocator _customRecipeLocator; - + private readonly ISystemCapabilityEvaluator _systemCapabilityEvaluator; private readonly OrchestratorSession _session; public DeployCommand( @@ -59,6 +59,7 @@ public DeployCommand( ILocalUserSettingsEngine localUserSettingsEngine, IConsoleUtilities consoleUtilities, ICustomRecipeLocator customRecipeLocator, + ISystemCapabilityEvaluator systemCapabilityEvaluator, OrchestratorSession session) { _toolInteractiveService = toolInteractiveService; @@ -77,6 +78,7 @@ public DeployCommand( _session = session; _cdkManager = cdkManager; _customRecipeLocator = customRecipeLocator; + _systemCapabilityEvaluator = systemCapabilityEvaluator; } public async Task ExecuteAsync(string stackName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) @@ -84,7 +86,7 @@ public async Task ExecuteAsync(string stackName, string deploymentProjectPath, U var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(stackName, userDeploymentSettings, deploymentProjectPath); // Verify Docker installation and minimum NodeJS version. - await EvaluateSystemCapabilities(_session, selectedRecommendation); + await EvaluateSystemCapabilities(selectedRecommendation); // Configure option settings. await ConfigureDeployment(cloudApplication, orchestrator, selectedRecommendation, userDeploymentSettings); @@ -189,32 +191,18 @@ private void DisplayOutputResources(List displayedResourc /// /// Checks if the system meets all the necessary requirements for deployment. /// - /// Holds metadata about the deployment project and the AWS account used for deployment. /// The selected recommendation settings used for deployment. - public async Task EvaluateSystemCapabilities(OrchestratorSession session, Recommendation selectedRecommendation) + public async Task EvaluateSystemCapabilities(Recommendation selectedRecommendation) { - if (_session.SystemCapabilities == null) - throw new SystemCapabilitiesNotProvidedException("The system capabilities were not provided."); - - var systemCapabilities = await _session.SystemCapabilities; - if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.CdkProject && - !systemCapabilities.NodeJsMinVersionInstalled) + var systemCapabilities = await _systemCapabilityEvaluator.EvaluateSystemCapabilities(selectedRecommendation); + var missingCapabilitiesMessage = ""; + foreach (var capability in systemCapabilities) { - throw new MissingNodeJsException("The selected deployment uses the AWS CDK, which requires version of Node.js higher than your current installation. The latest LTS version of Node.js is recommended and can be installed from https://nodejs.org/en/download/. Specifically, AWS CDK requires 10.3+ to work properly."); + missingCapabilitiesMessage = $"{missingCapabilitiesMessage}{capability.GetMessage()}{Environment.NewLine}"; } - if (selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) - { - if (!systemCapabilities.DockerInfo.DockerInstalled) - { - throw new MissingDockerException("The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for you OS: https://docs.docker.com/engine/install/"); - } - - if (!systemCapabilities.DockerInfo.DockerContainerType.Equals("linux", StringComparison.OrdinalIgnoreCase)) - { - throw new DockerContainerTypeException("The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue."); - } - } + if (systemCapabilities.Any()) + throw new MissingSystemCapabilityException(missingCapabilitiesMessage); } /// diff --git a/src/AWS.Deploy.CLI/Exceptions.cs b/src/AWS.Deploy.CLI/Exceptions.cs index c63e3468c..16c38aa1e 100644 --- a/src/AWS.Deploy.CLI/Exceptions.cs +++ b/src/AWS.Deploy.CLI/Exceptions.cs @@ -35,15 +35,6 @@ public class FailedToFindDeployableTargetException : Exception public FailedToFindDeployableTargetException(string message, Exception? innerException = null) : base(message, innerException) { } } - /// - /// Throw if docker info failed to return output. - /// - [AWSDeploymentExpectedException] - public class DockerInfoException : Exception - { - public DockerInfoException(string message, Exception? innerException = null) : base(message, innerException) { } - } - /// /// Throw if prompting the user for a name returns a null value. /// diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index c0953e61a..62ca570d4 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -312,6 +312,41 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody return Ok(); } + /// + /// Checks the missing System Capabilities for a given session. + /// + [HttpPost("session//compatiblity")] + [SwaggerOperation(OperationId = "GetCompatibility")] + [SwaggerResponse(200, type: typeof(GetCompatibilityOutput))] + [Authorize] + public async Task GetCompatibility(string sessionId) + { + var state = _stateServer.Get(sessionId); + if (state == null) + { + return NotFound($"Session ID {sessionId} not found."); + } + + if (state.SelectedRecommendation == null) + { + return NotFound($"A deployment target is not set for Session ID {sessionId}."); + } + + var output = new GetCompatibilityOutput(); + var serviceProvider = CreateSessionServiceProvider(state); + var systemCapabilityEvaluator = serviceProvider.GetRequiredService(); + + var capabilities = await systemCapabilityEvaluator.EvaluateSystemCapabilities(state.SelectedRecommendation); + + output.Capabilities = capabilities.Select(x => new SystemCapabilitySummary(x.Name, x.Installed, x.Available) + { + InstallationUrl = x.InstallationUrl, + Message = x.Message + }).ToList(); + + return Ok(output); + } + /// /// Begin execution of the deployment. /// diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/GetCompatibilityOutput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/GetCompatibilityOutput.cs new file mode 100644 index 000000000..ffad78177 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/GetCompatibilityOutput.cs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class GetCompatibilityOutput + { + public IEnumerable Capabilities { get; set; } = new List(); + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs new file mode 100644 index 000000000..393ae86b6 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class SystemCapabilitySummary + { + public string Name { get; set; } + public bool Installed { get; set; } + public bool Available { get; set; } + public string? Message { get; set; } + public string? InstallationUrl { get; set; } + + public SystemCapabilitySummary(string name, bool installed, bool available) + { + Name = name; + Installed = installed; + Available = available; + } + } +} diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 6e3eda4c1..606423df9 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -47,36 +47,12 @@ public InvalidProjectDefinitionException(string message, Exception? innerExcepti } /// - /// Throw if the user attempts to deploy a - /// that uses - /// but NodeJs/NPM could not be detected. + /// Thrown if there is a missing System Capability. /// [AWSDeploymentExpectedException] - public class MissingNodeJsException : Exception + public class MissingSystemCapabilityException : Exception { - public MissingNodeJsException(string message, Exception? innerException = null) : base(message, innerException) { } - } - - /// - /// Throw if the user attempts to deploy a - /// that requires - /// but Docker could not be detected. - /// - [AWSDeploymentExpectedException] - public class MissingDockerException : Exception - { - public MissingDockerException(string message, Exception? innerException = null) : base(message, innerException) { } - } - - /// - /// Throw if the user attempts to deploy a - /// that requires - /// but Docker is not running in linux mode. - /// - [AWSDeploymentExpectedException] - public class DockerContainerTypeException : Exception - { - public DockerContainerTypeException(string message, Exception? innerException = null) : base(message, innerException) { } + public MissingSystemCapabilityException(string message, Exception? innerException = null) : base(message, innerException) { } } /// diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 8bf83a0b1..345990dab 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -175,6 +175,7 @@ public class InvalidLocalUserSettingsFileException : Exception public InvalidLocalUserSettingsFileException(string message, Exception? innerException = null) : base(message, innerException) { } } + /// /// Exception thrown if a failure occured while trying to update the Local User Settings file. /// [AWSDeploymentExpectedException] @@ -182,4 +183,13 @@ public class FailedToUpdateLocalUserSettingsFileException : Exception { public FailedToUpdateLocalUserSettingsFileException(string message, Exception? innerException = null) : base(message, innerException) { } } + + /// + /// Throw if docker info failed to return output. + /// + [AWSDeploymentExpectedException] + public class DockerInfoException : Exception + { + public DockerInfoException(string message, Exception? innerException = null) : base(message, innerException) { } + } } diff --git a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs index 6bd780ffc..4798af4fc 100644 --- a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs +++ b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs @@ -18,13 +18,6 @@ public class OrchestratorSession : IDeployToolValidationContext public string? AWSProfileName { get; set; } public AWSCredentials? AWSCredentials { get; set; } public string? AWSRegion { get; set; } - /// - /// Calculating the current can take several seconds - /// and is not needed immediately so it is run as a background Task. - /// - /// It's safe to repeatedly await this property; evaluation will only be done once. - /// - public Task? SystemCapabilities { get; set; } public string? AWSAccountId { get; set; } public OrchestratorSession( diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilities.cs b/src/AWS.Deploy.Orchestration/SystemCapabilities.cs index c59d67106..1d0463d67 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilities.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilities.cs @@ -27,4 +27,30 @@ public DockerInfo( DockerContainerType = dockerContainerType; } } + + public class SystemCapability + { + public string Name { get; set; } + public bool Installed { get; set; } + public bool Available { get; set; } + public string? Message { get; set; } + public string? InstallationUrl { get; set; } + + public SystemCapability(string name, bool installed, bool available) + { + Name = name; + Installed = installed; + Available = available; + } + + public string GetMessage() + { + if (!string.IsNullOrEmpty(Message)) + return Message; + + var availabilityMessage = Available ? "and available" : "but not available"; + var installationMessage = Installed ? $"installed {availabilityMessage}" : "not installed"; + return $"The system capability '{Name}' is {installationMessage}"; + } + } } diff --git a/src/AWS.Deploy.CLI/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs similarity index 51% rename from src/AWS.Deploy.CLI/SystemCapabilityEvaluator.cs rename to src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index 412c0fafe..3cca05bbf 100644 --- a/src/AWS.Deploy.CLI/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -2,21 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.IO; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading.Tasks; -using AWS.Deploy.Orchestration; -using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Utilities; -namespace AWS.Deploy.CLI +namespace AWS.Deploy.Orchestration { public interface ISystemCapabilityEvaluator { - Task Evaluate(); + Task> EvaluateSystemCapabilities(Recommendation selectedRecommendation); } - internal class SystemCapabilityEvaluator : ISystemCapabilityEvaluator + public class SystemCapabilityEvaluator : ISystemCapabilityEvaluator { private readonly ICommandLineWrapper _commandLineWrapper; @@ -77,5 +77,43 @@ private async Task HasMinVersionNodeJs() return version.Major > 10 || version.Major == 10 && version.Minor >= 3; } + + /// + /// Checks if the system meets all the necessary requirements for deployment. + /// + public async Task> EvaluateSystemCapabilities(Recommendation selectedRecommendation) + { + var capabilities = new List(); + var systemCapabilities = await Evaluate(); + if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.CdkProject && + !systemCapabilities.NodeJsMinVersionInstalled) + { + capabilities.Add(new SystemCapability("NodeJS", false, false) { + InstallationUrl = "https://nodejs.org/en/download/", + Message = "The selected deployment uses the AWS CDK, which requires version of Node.js higher than your current installation. The latest LTS version of Node.js is recommended and can be installed from https://nodejs.org/en/download/. Specifically, AWS CDK requires 10.3+ to work properly." + }); + } + + if (selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) + { + if (!systemCapabilities.DockerInfo.DockerInstalled) + { + capabilities.Add(new SystemCapability("Docker", false, false) + { + InstallationUrl = "https://docs.docker.com/engine/install/", + Message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for you OS: https://docs.docker.com/engine/install/" + }); + } + else if (!systemCapabilities.DockerInfo.DockerContainerType.Equals("linux", StringComparison.OrdinalIgnoreCase)) + { + capabilities.Add(new SystemCapability("Docker", true, false) + { + Message = "The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue." + }); + } + } + + return capabilities; + } } } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index cf780c716..576396fda 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -99,6 +99,17 @@ public partial interface IRestAPIClient /// A server side error occurred. System.Threading.Tasks.Task GetExistingDeploymentsAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + /// Checks the missing System Capabilities for a given session. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetCompatibilityAsync(string sessionId); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Checks the missing System Capabilities for a given session. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetCompatibilityAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + /// Begin execution of the deployment. /// Success /// A server side error occurred. @@ -754,6 +765,88 @@ public async System.Threading.Tasks.Task GetExisti } } + /// Checks the missing System Capabilities for a given session. + /// Success + /// A server side error occurred. + public System.Threading.Tasks.Task GetCompatibilityAsync(string sessionId) + { + return GetCompatibilityAsync(sessionId, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Checks the missing System Capabilities for a given session. + /// Success + /// A server side error occurred. + public async System.Threading.Tasks.Task GetCompatibilityAsync(string sessionId, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v1/Deployment/session//compatiblity?"); + if (sessionId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("sessionId") + "=").Append(System.Uri.EscapeDataString(ConvertToString(sessionId, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Content = new System.Net.Http.StringContent(string.Empty, System.Text.Encoding.UTF8, "application/json"); + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// Begin execution of the deployment. /// Success /// A server side error occurred. @@ -1329,6 +1422,15 @@ public partial class ExistingDeploymentSummary public bool UpdatedByCurrentUser { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class GetCompatibilityOutput + { + [Newtonsoft.Json.JsonProperty("capabilities", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection Capabilities { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] @@ -1507,6 +1609,27 @@ public partial class StartDeploymentSessionOutput public string DefaultDeploymentName { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class SystemCapabilitySummary + { + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("installed", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Installed { get; set; } + + [Newtonsoft.Json.JsonProperty("available", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Available { get; set; } + + [Newtonsoft.Json.JsonProperty("message", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Message { get; set; } + + [Newtonsoft.Json.JsonProperty("installationUrl", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string InstallationUrl { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index d1f54d916..6ffa82be8 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -28,16 +28,12 @@ private async Task BuildRecommendationEngine(string testPr var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); var session = new OrchestratorSession( await parser.Parse(fullPath), awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" }; diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 41bab1bad..8e3e442ba 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -159,16 +159,12 @@ private async Task BuildRecommendationEngine(string testPr var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); var session = new OrchestratorSession( await parser.Parse(fullPath), awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" }; diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index 897cd3320..fb770fed8 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -24,16 +24,12 @@ private async Task BuildRecommendationEngine(string testPr var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); var session = new OrchestratorSession( await parser.Parse(fullPath), awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" }; diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index f0a34d191..086659e94 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -28,16 +28,12 @@ private async Task BuildRecommendationEngine(string testPr var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); _session = new OrchestratorSession( await parser.Parse(fullPath), awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" }; diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index 1f62b44c8..82ba16307 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -28,16 +28,12 @@ public SetOptionSettingTests() var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); var session = new OrchestratorSession( parser.Parse(projectPath).Result, awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" }; diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs index d58be780b..b850d3e81 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs @@ -47,16 +47,12 @@ public DisplayedResourcesHandlerTests() var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); var awsCredentials = new Mock(); - var systemCapabilities = new Mock( - It.IsAny(), - It.IsAny()); _session = new OrchestratorSession( await parser.Parse(fullPath), awsCredentials.Object, "us-west-2", "123456789012") { - SystemCapabilities = Task.FromResult(systemCapabilities.Object), AWSProfileName = "default" };