From 6d3b8b3254cc9f6171c0094d4f835b37aa4e6656 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 27 Apr 2022 15:30:10 -0700 Subject: [PATCH 1/6] fix: Add the new ReceiveLogSectionStart callback to the IDeploymentCommunicationClient --- .../DeploymentCommunicationClient.cs | 2 ++ test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/AWS.Deploy.ServerMode.Client/DeploymentCommunicationClient.cs b/src/AWS.Deploy.ServerMode.Client/DeploymentCommunicationClient.cs index 571e0f3e8..767427664 100644 --- a/src/AWS.Deploy.ServerMode.Client/DeploymentCommunicationClient.cs +++ b/src/AWS.Deploy.ServerMode.Client/DeploymentCommunicationClient.cs @@ -18,6 +18,8 @@ public interface IDeploymentCommunicationClient : IDisposable Action? ReceiveLogInfoMessage { get; set; } + Action? ReceiveLogSectionStart { get; set; } + Task JoinSession(string sessionId); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index 06767bbcf..f7f891fac 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -311,7 +311,7 @@ public async Task ShutdownViaRestClient() } } - internal static void RegisterSignalRMessageCallbacks(DeploymentCommunicationClient signalRClient, StringBuilder logOutput) + internal static void RegisterSignalRMessageCallbacks(IDeploymentCommunicationClient signalRClient, StringBuilder logOutput) { signalRClient.ReceiveLogSectionStart = (message, description) => { From 9bf0dd98db83a6a7bd706e897d4cfc394d0f01f2 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Wed, 27 Apr 2022 13:57:07 +0000 Subject: [PATCH 2/6] build: version bump to 0.43 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2aefae18d..de76544b4 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.42", + "version": "0.43", "publicReleaseRefSpec": [ ".*" ], From 8be2026ae6d361687dd3385dd8f4d784d2bba5b2 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Sun, 1 May 2022 22:44:03 -0400 Subject: [PATCH 3/6] fix: Add new error code to indicate deployment bundle creation failure from a generate docker file --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 9 +++++++++ .../Controllers/DeploymentController.cs | 8 ++++++-- .../Tasks/DeployRecommendationTask.cs | 17 ++++++++++++++++- src/AWS.Deploy.Common/Exceptions.cs | 3 ++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index f891d0ba6..80d383752 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -706,6 +706,15 @@ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendat } else { + if (!selectedRecommendation.ProjectDefinition.HasDockerFile) + { + var projectDirectory = _directoryManager.GetDirectoryInfo(selectedRecommendation.ProjectPath).Parent.FullName; + var dockerfilePath = Path.Combine(projectDirectory, "Dockerfile"); + var errorMessage = $"Failed to create a container image from generated Docker file. " + + $"Please edit the Dockerfile at {dockerfilePath} to correct the required build steps for the project. Common errors are missing project dependencies not included in the Dockerfile."; + + throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile, errorMessage); + } throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundle, "Failed to create a deployment bundle"); } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 4ea1484d1..99240266f 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -468,13 +468,15 @@ public async Task GenerateCloudFormationTemplate(string sessionId var cdkProjectHandler = CreateCdkProjectHandler(state, serviceProvider); + var directoryManager = new DirectoryManager(); + if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); if (!state.SelectedRecommendation.Recipe.DeploymentType.Equals(Common.Recipes.DeploymentTypes.CdkProject)) throw new SelectedRecommendationIsIncompatibleException($"We cannot generate a CloudFormation template for the selected recommendation as it is not of type '{nameof(Models.DeploymentTypes.CloudFormationStack)}'."); - var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation, directoryManager); var cloudFormationTemplate = await task.GenerateCloudFormationTemplate(cdkProjectHandler); var output = new GenerateCloudFormationTemplateOutput(cloudFormationTemplate); @@ -501,6 +503,8 @@ public async Task StartDeployment(string sessionId) var orchestrator = CreateOrchestrator(state, serviceProvider); + var directoryManager = new DirectoryManager(); + if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); @@ -517,7 +521,7 @@ public async Task StartDeployment(string sessionId) if (capabilities.Any()) return Problem($"Unable to start deployment due to missing system capabilities.{Environment.NewLine}{missingCapabilitiesMessage}"); - var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation, directoryManager); state.DeploymentTask = task.Execute(); return Ok(); diff --git a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs index c4b1078bd..a15179638 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration; @@ -17,13 +19,15 @@ public class DeployRecommendationTask private readonly Orchestrator _orchestrator; private readonly OrchestratorSession _orchestratorSession; private readonly Recommendation _selectedRecommendation; + private readonly IDirectoryManager _directoryManager; - public DeployRecommendationTask(OrchestratorSession orchestratorSession, Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation) + public DeployRecommendationTask(OrchestratorSession orchestratorSession, Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation, IDirectoryManager directoryManager) { _orchestratorSession = orchestratorSession; _orchestrator = orchestrator; _cloudApplication = cloudApplication; _selectedRecommendation = selectedRecommendation; + _directoryManager = directoryManager; } public async Task Execute() @@ -60,7 +64,18 @@ private async Task CreateDeploymentBundle() { var dockerBuildDeploymentBundleResult = await _orchestrator.CreateContainerDeploymentBundle(_cloudApplication, _selectedRecommendation); if (!dockerBuildDeploymentBundleResult) + { + if (!_selectedRecommendation.ProjectDefinition.HasDockerFile) + { + var projectDirectory = _directoryManager.GetDirectoryInfo(_selectedRecommendation.ProjectPath).Parent.FullName; + var dockerfilePath = Path.Combine(projectDirectory, "Dockerfile"); + var errorMessage = $"Failed to create a container image from generated Docker file. " + + $"Please edit the Dockerfile at {dockerfilePath} to correct the required build steps for the project. Common errors are missing project dependencies not included in the Dockerfile."; + + throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile, errorMessage); + } throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundle, "Failed to create a deployment bundle"); + } } else if (_selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.DotnetPublishZipFile) { diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 7105ff6da..4bb10c5d1 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -109,7 +109,8 @@ public enum DeployToolErrorCode FailedToGetCredentialsForProfile = 10008900, FailedToRunCDKDiff = 10009000, FailedToCreateCDKProject = 10009100, - ResourceQuery = 10009200 + ResourceQuery = 10009200, + FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile = 10009300 } public class ProjectFileNotFoundException : DeployToolException From 4708e8ab16ec77b97b7f46862395e591cb91b3ac Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 28 Apr 2022 22:42:53 -0700 Subject: [PATCH 4/6] fix: Fixed issue with Beanstalk Application and Environment name not correctly modeled in recipe definition. --- .../TypeHints/BeanstalkApplicationCommand.cs | 22 +++++---- .../BeanstalkApplicationTypeHintResponse.cs | 15 ++++--- .../BeanstalkApplicationConfiguration.cs | 7 ++- .../BeanstalkEnvironmentConfiguration.cs | 3 -- .../Generated/Recipe.cs | 44 +++++++++++++----- .../ASP.NETAppElasticBeanstalk.recipe | 45 ++++++++++++++----- .../ElasticBeanStalkConfigFile.json | 1 - 7 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs index 01ba5c9b5..d3d8df716 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs @@ -11,9 +11,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; -using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; -using Newtonsoft.Json; namespace AWS.Deploy.CLI.Commands.TypeHints { @@ -48,18 +46,26 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: app => app.ApplicationName, displaySelector: app => app.ApplicationName, defaultSelector: app => app.ApplicationName.Equals(currentTypeHintResponse?.ApplicationName), - defaultNewName: currentTypeHintResponse.ApplicationName) + defaultNewName: currentTypeHintResponse.ApplicationName ?? String.Empty) { AskNewName = true, }; var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(applications, "Select Elastic Beanstalk application to deploy to:", userInputConfiguration); - return new BeanstalkApplicationTypeHintResponse( - userResponse.CreateNew, - userResponse.SelectedOption?.ApplicationName ?? userResponse.NewName - ?? throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.BeanstalkAppPromptForNameReturnedNull, "The user response for a new application name was null.") - ); + var response = new BeanstalkApplicationTypeHintResponse(userResponse.CreateNew); + if(userResponse.CreateNew) + { + response.ApplicationName = userResponse.NewName ?? + throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.BeanstalkAppPromptForNameReturnedNull, "The user response for a new application name was null."); + } + else + { + response.ExistingApplicationName = userResponse.SelectedOption?.ApplicationName ?? + throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.BeanstalkAppPromptForNameReturnedNull, "The user response existing application name was null."); + } + + return response; } } } diff --git a/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkApplicationTypeHintResponse.cs b/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkApplicationTypeHintResponse.cs index a09452f0d..df6e32992 100644 --- a/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkApplicationTypeHintResponse.cs +++ b/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkApplicationTypeHintResponse.cs @@ -12,16 +12,21 @@ namespace AWS.Deploy.CLI.TypeHintResponses public class BeanstalkApplicationTypeHintResponse : IDisplayable { public bool CreateNew { get; set; } - public string ApplicationName { get; set; } + public string? ApplicationName { get; set; } + public string? ExistingApplicationName { get; set; } public BeanstalkApplicationTypeHintResponse( - bool createNew, - string applicationName) + bool createNew) { CreateNew = createNew; - ApplicationName = applicationName; } - public string ToDisplayString() => ApplicationName; + public string ToDisplayString() + { + if (CreateNew) + return ApplicationName!; + else + return ExistingApplicationName!; + } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkApplicationConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkApplicationConfiguration.cs index b1b3d3ae5..08e9679e3 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkApplicationConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkApplicationConfiguration.cs @@ -12,7 +12,8 @@ namespace AspNetAppElasticBeanstalkLinux.Configurations public partial class BeanstalkApplicationConfiguration { public bool CreateNew { get; set; } - public string ApplicationName { get; set; } + public string? ApplicationName { get; set; } + public string? ExistingApplicationName { get; set; } /// A parameterless constructor is needed for /// or the classes will fail to initialize. @@ -25,11 +26,9 @@ public BeanstalkApplicationConfiguration() #nullable restore warnings public BeanstalkApplicationConfiguration( - bool createNew, - string applicationName) + bool createNew) { CreateNew = createNew; - ApplicationName = applicationName; } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs index 5573ff42a..55a17586d 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs @@ -11,7 +11,6 @@ namespace AspNetAppElasticBeanstalkLinux.Configurations { public partial class BeanstalkEnvironmentConfiguration { - public bool CreateNew { get; set; } public string EnvironmentName { get; set; } /// A parameterless constructor is needed for @@ -25,10 +24,8 @@ public BeanstalkEnvironmentConfiguration() #nullable restore warnings public BeanstalkEnvironmentConfiguration( - bool createNew, string environmentName) { - CreateNew = createNew; EnvironmentName = environmentName; } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs index 81e47d9cb..4a5cb3eee 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs @@ -63,8 +63,8 @@ public Recipe(Construct scope, IRecipeProps props) }); ConfigureIAM(settings); - ConfigureApplication(settings); - ConfigureBeanstalkEnvironment(settings); + var beanstalkApplicationName = ConfigureApplication(settings); + ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName); } private void ConfigureIAM(Configuration settings) @@ -121,16 +121,41 @@ private void ConfigureIAM(Configuration settings) } } - private void ConfigureApplication(Configuration settings) + private string ConfigureApplication(Configuration settings) { if (ApplicationAsset == null) throw new InvalidOperationException($"{nameof(ApplicationAsset)} has not been set."); + string beanstalkApplicationName; + if(settings.BeanstalkApplication.CreateNew) + { + if (settings.BeanstalkApplication.ApplicationName == null) + throw new InvalidOperationException($"{nameof(settings.BeanstalkApplication.ApplicationName)} has not been set."); + + beanstalkApplicationName = settings.BeanstalkApplication.ApplicationName; + } + else + { + // This check is here for deployments that were initially done with an older version of the project. + // In those deployments the existing application name was persisted in the ApplicationName property. + if (settings.BeanstalkApplication.ExistingApplicationName == null && settings.BeanstalkApplication.ApplicationName != null) + { + beanstalkApplicationName = settings.BeanstalkApplication.ApplicationName; + } + else + { + if (settings.BeanstalkApplication.ExistingApplicationName == null) + throw new InvalidOperationException($"{nameof(settings.BeanstalkApplication.ExistingApplicationName)} has not been set."); + + beanstalkApplicationName = settings.BeanstalkApplication.ExistingApplicationName; + } + } + // Create an app version from the S3 asset defined above // The S3 "putObject" will occur first before CF generates the template ApplicationVersion = new CfnApplicationVersion(this, nameof(ApplicationVersion), InvokeCustomizeCDKPropsEvent(nameof(ApplicationVersion), this, new CfnApplicationVersionProps { - ApplicationName = settings.BeanstalkApplication.ApplicationName, + ApplicationName = beanstalkApplicationName, SourceBundle = new CfnApplicationVersion.SourceBundleProperty { S3Bucket = ApplicationAsset.S3BucketName, @@ -142,14 +167,16 @@ private void ConfigureApplication(Configuration settings) { BeanstalkApplication = new CfnApplication(this, nameof(BeanstalkApplication), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkApplication), this, new CfnApplicationProps { - ApplicationName = settings.BeanstalkApplication.ApplicationName + ApplicationName = beanstalkApplicationName })); ApplicationVersion.AddDependsOn(BeanstalkApplication); } + + return beanstalkApplicationName; } - private void ConfigureBeanstalkEnvironment(Configuration settings) + private void ConfigureBeanstalkEnvironment(Configuration settings, string beanstalkApplicationName) { if (Ec2InstanceProfile == null) throw new InvalidOperationException($"{nameof(Ec2InstanceProfile)} has not been set. The {nameof(ConfigureIAM)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}"); @@ -378,13 +405,10 @@ private void ConfigureBeanstalkEnvironment(Configuration settings) } } - if (!settings.BeanstalkEnvironment.CreateNew) - throw new InvalidOrMissingConfigurationException("The ability to deploy an Elastic Beanstalk application to an existing environment via a new CloudFormation stack is not supported yet."); - BeanstalkEnvironment = new CfnEnvironment(this, nameof(BeanstalkEnvironment), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkEnvironment), this, new CfnEnvironmentProps { EnvironmentName = settings.BeanstalkEnvironment.EnvironmentName, - ApplicationName = settings.BeanstalkApplication.ApplicationName, + ApplicationName = beanstalkApplicationName, PlatformArn = settings.ElasticBeanstalkPlatformArn, OptionSettings = optionSettingProperties.ToArray(), CnamePrefix = !string.IsNullOrEmpty(settings.CNamePrefix) ? settings.CNamePrefix : null, diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index e4b50682c..bae15e6ca 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -98,15 +98,47 @@ "Name": "Application Name", "Description": "The Elastic Beanstalk application name.", "Type": "String", + "DefaultValue": "{StackName}", + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "BeanstalkApplication.CreateNew", + "Value": true + } + ], + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^[^/]{1,100}$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid Application Name. The Application name can contain up to 100 Unicode characters, not including forward slash (/)." + } + } + ] + }, + { + "Id": "ExistingApplicationName", + "Name": "Application Name", + "Description": "The Elastic Beanstalk application name.", + "Type": "String", "TypeHint": "ExistingBeanstalkApplication", "DefaultValue": "{StackName}", "AdvancedSetting": false, "Updatable": false, + "DependsOn": [ + { + "Id": "BeanstalkApplication.CreateNew", + "Value": false + } + ], "Validators": [ { "ValidatorType": "Regex", - "Configuration" : { + "Configuration": { "Regex": "^[^/]{1,100}$", + "AllowEmptyString": true, "ValidationFailedMessage": "Invalid Application Name. The Application name can contain up to 100 Unicode characters, not including forward slash (/)." } } @@ -120,26 +152,15 @@ "Name": "Environment Name", "Description": "The Elastic Beanstalk environment name.", "Type": "Object", - "TypeHint": "BeanstalkEnvironment", "AdvancedSetting": false, "Updatable": false, "ChildOptionSettings": [ - { - "Id": "CreateNew", - "Name": "Create new Elastic Beanstalk environment", - "Description": "Do you want to create a new environment?", - "Type": "Bool", - "DefaultValue": true, - "AdvancedSetting": false, - "Updatable": false - }, { "Id": "EnvironmentName", "ParentSettingId": "BeanstalkApplication.ApplicationName", "Name": "Environment Name", "Description": "The Elastic Beanstalk environment name.", "Type": "String", - "TypeHint": "BeanstalkEnvironment", "DefaultValue": "{StackName}-dev", "AdvancedSetting": false, "Updatable": false, diff --git a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json index 2e44ae519..4e66d95a1 100644 --- a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json +++ b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json @@ -7,7 +7,6 @@ "ApplicationName": "MyApplication{Suffix}" }, "BeanstalkEnvironment": { - "CreateNew": true, "EnvironmentName": "MyEnvironment{Suffix}" }, "EnvironmentType": "LoadBalanced", From bec821e82a890b9c3a5e533c55c6523852f91ba5 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 27 Apr 2022 22:50:11 -0700 Subject: [PATCH 5/6] fix: Consolidate the separate CLI and ServerMode CreateDeploymentBundle methods into the Orchestrator --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 64 +++++++------------ .../Controllers/DeploymentController.cs | 8 +-- .../Tasks/DeployRecommendationTask.cs | 38 ++--------- src/AWS.Deploy.Orchestration/Orchestrator.cs | 41 +++++++++++- .../CLITests.cs | 2 +- .../ServerModeTests.cs | 2 +- .../ServerMode/GetApplyOptionSettings.cs | 2 +- .../ServerModeTests.cs | 21 ++++-- 8 files changed, 88 insertions(+), 90 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 80d383752..7500420af 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -674,59 +674,41 @@ private bool ConfirmDeployment(Recommendation recommendation) private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendation selectedRecommendation, CloudApplication cloudApplication) { - if (selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) + try { - _orchestratorInteractiveService.LogSectionStart("Creating deployment image", - "Using the docker CLI to perform a docker build to create a container image."); - - while (!await orchestrator.CreateContainerDeploymentBundle(cloudApplication, selectedRecommendation)) + await orchestrator.CreateDeploymentBundle(cloudApplication, selectedRecommendation); + } + catch(FailedToCreateDeploymentBundleException ex) when (ex.ErrorCode == DeployToolErrorCode.FailedToCreateContainerDeploymentBundle) + { + if (_toolInteractiveService.DisableInteractive) { - if (_toolInteractiveService.DisableInteractive) - { - var errorMessage = "Failed to build Docker Image." + Environment.NewLine; - errorMessage += "Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile." + Environment.NewLine; - errorMessage += "Specify a valid Docker execution directory as part of the deployment settings file and try again."; - throw new DockerBuildFailedException(DeployToolErrorCode.DockerBuildFailed, errorMessage); - } + var errorMessage = "Failed to build Docker Image." + Environment.NewLine; + errorMessage += "Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile." + Environment.NewLine; + errorMessage += "Specify a valid Docker execution directory as part of the deployment settings file and try again."; + throw new DockerBuildFailedException(DeployToolErrorCode.DockerBuildFailed, errorMessage); + } - _toolInteractiveService.WriteLine(string.Empty); - var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "false"); - if (answer == YesNo.Yes) + _toolInteractiveService.WriteLine(string.Empty); + var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "false"); + if (answer == YesNo.Yes) + { + string dockerExecutionDirectory; + do { - var dockerExecutionDirectory = - _consoleUtilities.AskUserForValue( + dockerExecutionDirectory = _consoleUtilities.AskUserForValue( "Enter the docker execution directory where the docker build command will be executed from:", selectedRecommendation.DeploymentBundle.DockerExecutionDirectory, allowEmpty: true); if (!_directoryManager.Exists(dockerExecutionDirectory)) - continue; - - selectedRecommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; - } - else - { - if (!selectedRecommendation.ProjectDefinition.HasDockerFile) { - var projectDirectory = _directoryManager.GetDirectoryInfo(selectedRecommendation.ProjectPath).Parent.FullName; - var dockerfilePath = Path.Combine(projectDirectory, "Dockerfile"); - var errorMessage = $"Failed to create a container image from generated Docker file. " + - $"Please edit the Dockerfile at {dockerfilePath} to correct the required build steps for the project. Common errors are missing project dependencies not included in the Dockerfile."; - - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile, errorMessage); + _toolInteractiveService.WriteErrorLine($"Error, directory does not exist \"{dockerExecutionDirectory}\""); } - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundle, "Failed to create a deployment bundle"); - } - } - } - else if (selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.DotnetPublishZipFile) - { - _orchestratorInteractiveService.LogSectionStart("Creating deployment zip bundle", - "Using the dotnet CLI build the project and zip the publish artifacts."); + } while (!_directoryManager.Exists(dockerExecutionDirectory)); - var dotnetPublishDeploymentBundleResult = await orchestrator.CreateDotnetPublishDeploymentBundle(selectedRecommendation); - if (!dotnetPublishDeploymentBundleResult) - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateDotnetPublishDeploymentBundle, "Failed to create a deployment bundle"); + selectedRecommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; + await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication); + } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 99240266f..4ea1484d1 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -468,15 +468,13 @@ public async Task GenerateCloudFormationTemplate(string sessionId var cdkProjectHandler = CreateCdkProjectHandler(state, serviceProvider); - var directoryManager = new DirectoryManager(); - if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); if (!state.SelectedRecommendation.Recipe.DeploymentType.Equals(Common.Recipes.DeploymentTypes.CdkProject)) throw new SelectedRecommendationIsIncompatibleException($"We cannot generate a CloudFormation template for the selected recommendation as it is not of type '{nameof(Models.DeploymentTypes.CloudFormationStack)}'."); - var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation, directoryManager); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); var cloudFormationTemplate = await task.GenerateCloudFormationTemplate(cdkProjectHandler); var output = new GenerateCloudFormationTemplateOutput(cloudFormationTemplate); @@ -503,8 +501,6 @@ public async Task StartDeployment(string sessionId) var orchestrator = CreateOrchestrator(state, serviceProvider); - var directoryManager = new DirectoryManager(); - if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); @@ -521,7 +517,7 @@ public async Task StartDeployment(string sessionId) if (capabilities.Any()) return Problem($"Unable to start deployment due to missing system capabilities.{Environment.NewLine}{missingCapabilitiesMessage}"); - var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation, directoryManager); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); state.DeploymentTask = task.Execute(); return Ok(); diff --git a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs index a15179638..e19884b2f 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text; using System.Threading.Tasks; using AWS.Deploy.Common; -using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration; @@ -19,20 +17,18 @@ public class DeployRecommendationTask private readonly Orchestrator _orchestrator; private readonly OrchestratorSession _orchestratorSession; private readonly Recommendation _selectedRecommendation; - private readonly IDirectoryManager _directoryManager; - public DeployRecommendationTask(OrchestratorSession orchestratorSession, Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation, IDirectoryManager directoryManager) + public DeployRecommendationTask(OrchestratorSession orchestratorSession, Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation) { _orchestratorSession = orchestratorSession; _orchestrator = orchestrator; _cloudApplication = cloudApplication; _selectedRecommendation = selectedRecommendation; - _directoryManager = directoryManager; } public async Task Execute() { - await CreateDeploymentBundle(); + await _orchestrator.CreateDeploymentBundle(_cloudApplication, _selectedRecommendation); await _orchestrator.DeployRecommendation(_cloudApplication, _selectedRecommendation); } @@ -46,7 +42,8 @@ public async Task GenerateCloudFormationTemplate(CdkProjectHandler cdkPr if (cdkProjectHandler == null) throw new FailedToCreateCDKProjectException(DeployToolErrorCode.FailedToCreateCDKProject, $"We could not create a CDK deployment project due to a missing dependency '{nameof(cdkProjectHandler)}'."); - await CreateDeploymentBundle(); + await _orchestrator.CreateDeploymentBundle(_cloudApplication, _selectedRecommendation); + var cdkProject = await cdkProjectHandler.ConfigureCdkProject(_orchestratorSession, _cloudApplication, _selectedRecommendation); try { @@ -57,32 +54,5 @@ public async Task GenerateCloudFormationTemplate(CdkProjectHandler cdkPr cdkProjectHandler.DeleteTemporaryCdkProject(cdkProject); } } - - private async Task CreateDeploymentBundle() - { - if (_selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) - { - var dockerBuildDeploymentBundleResult = await _orchestrator.CreateContainerDeploymentBundle(_cloudApplication, _selectedRecommendation); - if (!dockerBuildDeploymentBundleResult) - { - if (!_selectedRecommendation.ProjectDefinition.HasDockerFile) - { - var projectDirectory = _directoryManager.GetDirectoryInfo(_selectedRecommendation.ProjectPath).Parent.FullName; - var dockerfilePath = Path.Combine(projectDirectory, "Dockerfile"); - var errorMessage = $"Failed to create a container image from generated Docker file. " + - $"Please edit the Dockerfile at {dockerfilePath} to correct the required build steps for the project. Common errors are missing project dependencies not included in the Dockerfile."; - - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile, errorMessage); - } - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundle, "Failed to create a deployment bundle"); - } - } - else if (_selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.DotnetPublishZipFile) - { - var dotnetPublishDeploymentBundleResult = await _orchestrator.CreateDotnetPublishDeploymentBundle(_selectedRecommendation); - if (!dotnetPublishDeploymentBundleResult) - throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateDotnetPublishDeploymentBundle, "Failed to create a deployment bundle"); - } - } } } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index d2fb31cb4..523a6a109 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -169,7 +169,44 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm await deploymentCommand.ExecuteAsync(this, cloudApplication, recommendation); } - public async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) + public async Task CreateDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) + { + if (_interactiveService == null) + throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); + if (_directoryManager == null) + throw new InvalidOperationException($"{nameof(_directoryManager)} is null as part of the orchestartor object"); + + if (recommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) + { + _interactiveService.LogSectionStart("Creating deployment image", + "Using the docker CLI to perform a docker build to create a container image."); + + if (!await CreateContainerDeploymentBundle(cloudApplication, recommendation)) + { + if (!recommendation.ProjectDefinition.HasDockerFile) + { + var projectDirectory = _directoryManager.GetDirectoryInfo(recommendation.ProjectPath).Parent.FullName; + var dockerfilePath = Path.Combine(projectDirectory, "Dockerfile"); + var errorMessage = $"Failed to create a container image from generated Docker file. " + + $"Please edit the Dockerfile at {dockerfilePath} to correct the required build steps for the project. Common errors are missing project dependencies not included in the Dockerfile."; + + throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundleFromGeneratedDockerFile, errorMessage); + } + throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateContainerDeploymentBundle, "Failed to create a deployment bundle"); + } + } + else if (recommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.DotnetPublishZipFile) + { + _interactiveService.LogSectionStart("Creating deployment zip bundle", + "Using the dotnet CLI build the project and zip the publish artifacts."); + + var dotnetPublishDeploymentBundleResult = await CreateDotnetPublishDeploymentBundle(recommendation); + if (!dotnetPublishDeploymentBundleResult) + throw new FailedToCreateDeploymentBundleException(DeployToolErrorCode.FailedToCreateDotnetPublishDeploymentBundle, "Failed to create a deployment bundle"); + } + } + + private async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) { if (_interactiveService == null) throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); @@ -225,7 +262,7 @@ public async Task CreateContainerDeploymentBundle(CloudApplication cloudAp return true; } - public async Task CreateDotnetPublishDeploymentBundle(Recommendation recommendation) + private async Task CreateDotnetPublishDeploymentBundle(Recommendation recommendation) { if (_deploymentBundleHandler == null) throw new InvalidOperationException($"{nameof(_deploymentBundleHandler)} is null as part of the orchestartor object"); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs index defe30858..d2a91b38f 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs @@ -24,7 +24,7 @@ public CLITests(TestContextFixture fixture) public async Task DeployToExistingBeanstalkEnvironment() { var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _fixture.App.Run(deployArgs)); var environmentDescription = await _fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(_fixture.EnvironmentName); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs index 4396788c4..ab5d6d7f9 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs @@ -30,7 +30,7 @@ public ServerModeTests(TestContextFixture fixture) public async Task DeployToExistingBeanstalkEnvironment() { var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var portNumber = 4001; + var portNumber = 4031; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); var serverCommand = new ServerModeCommand(_fixture.ToolInteractiveService, portNumber, null, true); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index ff0e2e327..8f049ac20 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -79,7 +79,7 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var portNumber = 4001; + var portNumber = 4021; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index f7f891fac..a5dd46c95 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -162,7 +162,7 @@ public async Task WebFargateDeploymentNoConfigChanges() _stackName = $"ServerModeWebFargate{Guid.NewGuid().ToString().Split('-').Last()}"; var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var portNumber = 4001; + var portNumber = 4011; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); @@ -211,6 +211,11 @@ public async Task WebFargateDeploymentNoConfigChanges() Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); Assert.True(logOutput.Length > 0); + + // Make sure section header is return to output log + Assert.Contains("Creating deployment image", logOutput.ToString()); + + // Make sure normal log messages are returned to output log Assert.Contains("Pushing container image", logOutput.ToString()); var redeploymentSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput @@ -334,13 +339,21 @@ private async Task WaitForDeployment(RestAPIClient restApiClie // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. await Task.Delay(TimeSpan.FromSeconds(3)); + GetDeploymentStatusOutput output = null; + await WaitUntilHelper.WaitUntil(async () => { - DeploymentStatus status = (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; ; - return status != DeploymentStatus.Executing; + output = (await restApiClient.GetDeploymentStatusAsync(sessionId)); + + return output.Status != DeploymentStatus.Executing; }, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(15)); - return (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; + if (output.Exception != null) + { + throw new Exception("Error waiting on stack status: " + output.Exception.Message); + } + + return output.Status; } From f3b7c28d086a906e12f33e83fbe3bb270989a147 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 2 May 2022 00:47:57 -0700 Subject: [PATCH 6/6] feat: Skip executing "cdk deploy" if CDKToolkit stack already exists --- THIRD_PARTY_LICENSES | 1 + src/AWS.Deploy.Constants/CDK.cs | 10 ++ .../AWS.Deploy.Orchestration.csproj | 1 + .../CdkProjectHandler.cs | 55 ++++++-- .../Data/AWSResourceQueryer.cs | 41 ++++++ .../Utilities/TestToolAWSResourceQueryer.cs | 2 + .../Utilities/TestToolAWSResourceQueryer.cs | 2 + .../CDK/CDKProjectHandlerTests.cs | 125 ++++++++++++++++++ 8 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index f43e085e0..6855e3b8c 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -18,6 +18,7 @@ ** AWSSDK.Extensions.NETCore.Setup; version 3.7.1 -- https://www.nuget.org/packages/AWSSDK.Extensions.NETCore.Setup ** AWSSDK.IdentityManagement; version 3.7.2.25 -- https://www.nuget.org/packages/AWSSDK.IdentityManagement ** AWSSDK.SecurityToken; version 3.7.1.35 -- https://www.nuget.org/packages/AWSSDK.SecurityToken +** AWSSDK.SimpleSystemsManagement; version 3.7.16 -- https://www.nuget.org/packages/AWSSDK.SimpleSystemsManagement ** Constructs; version 10.0.0 -- https://www.nuget.org/packages/Constructs ** Amazon.CDK.Lib; version 2.13.0 -- https://www.nuget.org/packages/Amazon.CDK.Lib/ ** Amazon.JSII.Runtime; version 1.54.0 -- https://www.nuget.org/packages/Amazon.JSII.Runtime diff --git a/src/AWS.Deploy.Constants/CDK.cs b/src/AWS.Deploy.Constants/CDK.cs index 12376c710..2d8fe7c4d 100644 --- a/src/AWS.Deploy.Constants/CDK.cs +++ b/src/AWS.Deploy.Constants/CDK.cs @@ -27,5 +27,15 @@ internal static class CDK /// The file path of the CDK bootstrap template to be used /// public static string CDKBootstrapTemplatePath => Path.Combine(DeployToolWorkspaceDirectoryRoot, "CDKBootstrapTemplate.yaml"); + + /// + /// The version number CDK bootstrap specified in CDKBootstrapTemplate.yaml + /// + public const int CDKTemplateVersion = 12; + + /// + /// The name of the CDK bootstrap CloudFormation stack + /// + public const string CDKBootstrapStackName = "CDKToolkit"; } } diff --git a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj index d335de701..569bbeb42 100644 --- a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj +++ b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj @@ -22,6 +22,7 @@ + diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 2f52364a5..03d06ad49 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -103,15 +103,22 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); - // Ensure region is bootstrapped - var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\" --template \"{Constants.CDK.CDKBootstrapTemplatePath}\"", - workingDirectory: cdkProjectPath, - needAwsCredentials: true, - redirectIO: true, - streamOutputToInteractiveService: true); - - if (cdkBootstrap.ExitCode != 0) - throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToRunCDKBootstrap, "The AWS CDK Bootstrap, which is the process of provisioning initial resources for the deployment environment, has failed. Please review the output above for additional details [and check out our troubleshooting guide for the most common failure reasons]. You can learn more about CDK bootstrapping at https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html."); + if (await DetermineIfCDKBootstrapShouldRun()) + { + // Ensure region is bootstrapped + var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\" --template \"{Constants.CDK.CDKBootstrapTemplatePath}\"", + workingDirectory: cdkProjectPath, + needAwsCredentials: true, + redirectIO: true, + streamOutputToInteractiveService: true); + + if (cdkBootstrap.ExitCode != 0) + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToRunCDKBootstrap, "The AWS CDK Bootstrap, which is the process of provisioning initial resources for the deployment environment, has failed. Please review the output above for additional details [and check out our troubleshooting guide for the most common failure reasons]. You can learn more about CDK bootstrapping at https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html."); + } + else + { + _interactiveService.LogInfoMessage("Confirmed CDK Bootstrap CloudFormation stack already exists."); + } _interactiveService.LogSectionStart("Deploying AWS CDK project", @@ -120,7 +127,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication var deploymentStartDate = DateTime.Now; // Handover to CDK command line tool // Use a CDK Context parameter to specify the settings file that has been serialized. - var cdkDeploy = await _commandLineWrapper.TryRunWithResult( $"npx cdk deploy --require-approval never -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", + var cdkDeploy = await _commandLineWrapper.TryRunWithResult($"npx cdk deploy --require-approval never -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", workingDirectory: cdkProjectPath, environmentVariables: environmentVariables, needAwsCredentials: true, @@ -133,6 +140,34 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, "We had an issue deploying your application to AWS. Check the deployment output for more details."); } + public async Task DetermineIfCDKBootstrapShouldRun() + { + var stack = await _awsResourceQueryer.GetCloudFormationStack(AWS.Deploy.Constants.CDK.CDKBootstrapStackName); + if (stack == null) + { + _interactiveService.LogDebugMessage("CDK Bootstrap stack not found."); + return true; + } + + var qualiferParameter = stack.Parameters.FirstOrDefault(x => string.Equals("Qualifier", x.ParameterKey)); + if (qualiferParameter == null || string.IsNullOrEmpty(qualiferParameter.ParameterValue)) + { + _interactiveService.LogDebugMessage("CDK Bootstrap SSM parameter store value missing."); + return true; + } + + var bootstrapVersionStr = await _awsResourceQueryer.GetParameterStoreTextValue($"/cdk-bootstrap/{qualiferParameter.ParameterValue}/version"); + if (string.IsNullOrEmpty(bootstrapVersionStr) || + !int.TryParse(bootstrapVersionStr, out var bootstrapVersion) || + bootstrapVersion < AWS.Deploy.Constants.CDK.CDKTemplateVersion) + { + _interactiveService.LogDebugMessage($"CDK Bootstrap version is out of date: \"{AWS.Deploy.Constants.CDK.CDKTemplateVersion}\" < \"{bootstrapVersionStr}\"."); + return true; + } + + return false; + } + private async Task CheckCdkDeploymentFailure(CloudApplication cloudApplication, DateTime deploymentStartDate) { try diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 0c2fea696..c2d587e52 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -35,6 +35,8 @@ using Amazon.SecurityToken.Model; using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; +using Amazon.SimpleSystemsManagement; +using Amazon.SimpleSystemsManagement.Model; using Amazon.SQS; using Amazon.SQS.Model; using AWS.Deploy.Common; @@ -67,6 +69,7 @@ public interface IAWSResourceQueryer Task> GetECRRepositories(List? repositoryNames = null); Task CreateECRRepository(string repositoryName); Task> GetCloudFormationStacks(); + Task GetCloudFormationStack(string stackName); Task GetCallerIdentity(string awsRegion); Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType); Task GetCloudFrontDistribution(string distributionId); @@ -79,6 +82,7 @@ public interface IAWSResourceQueryer Task> DescribeAppRunnerVpcConnectors(); Task> DescribeSubnets(string? vpcID = null); Task> DescribeSecurityGroups(string? vpcID = null); + Task GetParameterStoreTextValue(string parameterName); } public class AWSResourceQueryer : IAWSResourceQueryer @@ -561,6 +565,25 @@ public async Task> GetCloudFormationStacks() .Stacks.ToListAsync()); } + public async Task GetCloudFormationStack(string stackName) + { + using var cloudFormationClient = _awsClientFactory.GetAWSClient(); + return await HandleException(async () => + { + try + { + var request = new DescribeStacksRequest { StackName = stackName }; + var response = await cloudFormationClient.DescribeStacksAsync(request); + return response.Stacks.FirstOrDefault(); + } + // CloudFormation throws a BadRequest exception if the stack does not exist + catch (AmazonCloudFormationException e) when (e.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + return null; + } + }); + } + public async Task GetCallerIdentity(string awsRegion) { var request = new GetCallerIdentityRequest(); @@ -700,6 +723,24 @@ public async Task DescribeECRRepository(string respositoryName) }); } + public async Task GetParameterStoreTextValue(string parameterName) + { + var client = _awsClientFactory.GetAWSClient(); + return await HandleException(async () => + { + try + { + var request = new GetParameterRequest { Name = parameterName }; + var response = await client.GetParameterAsync(request); + return response.Parameter.Value; + } + catch (ParameterNotFoundException) + { + return null; + } + }); + } + private async Task HandleException(Func> action) { try diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index a498c91b3..590c3f910 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -36,6 +36,7 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task DescribeElasticLoadBalancer(string loadBalancerArn) => throw new NotImplementedException(); public Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn) => throw new NotImplementedException(); public Task> GetCloudFormationStacks() => throw new NotImplementedException(); + public Task GetCloudFormationStack(string stackName) => throw new NotImplementedException(); public Task> GetECRAuthorizationToken() => throw new NotImplementedException(); public Task> GetECRRepositories(List repositoryNames) => throw new NotImplementedException(); public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); @@ -63,5 +64,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> DescribeAppRunnerVpcConnectors() => throw new NotImplementedException(); public Task> DescribeSubnets(string vpcID = null) => throw new NotImplementedException(); public Task> DescribeSecurityGroups(string vpcID = null) => throw new NotImplementedException(); + public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index c62ccc05c..63b7b8e8a 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -28,6 +28,7 @@ public class TestToolAWSResourceQueryer : IAWSResourceQueryer public Task CreateEC2KeyPair(string keyName, string saveLocation) => throw new NotImplementedException(); public Task CreateECRRepository(string repositoryName) => throw new NotImplementedException(); public Task> GetCloudFormationStacks() => throw new NotImplementedException(); + public Task GetCloudFormationStack(string stackName) => throw new NotImplementedException(); public Task> GetECRAuthorizationToken() { @@ -88,5 +89,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> DescribeAppRunnerVpcConnectors() => throw new NotImplementedException(); public Task> DescribeSubnets(string vpcID = null) => throw new NotImplementedException(); public Task> DescribeSecurityGroups(string vpcID = null) => throw new NotImplementedException(); + public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs new file mode 100644 index 000000000..af797fb7b --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Threading.Tasks; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Orchestration.Data; +using AWS.Deploy.Orchestration.Utilities; +using Moq; +using Xunit; +using Amazon.CloudFormation.Model; +using System.Collections.Generic; + +namespace AWS.Deploy.Orchestration.UnitTests.CDK +{ + public class CDKProjectHandlerTests + { + [Fact] + public async Task CheckCDKBootstrap_DoesNotExist() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(null)); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + + [Fact] + public async Task CheckCDKBootstrap_NoCFParameter() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(new Stack { Parameters = new List() })); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + + [Fact] + public async Task CheckCDKBootstrap_NoSSMParameter() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( + new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + + [Fact] + public async Task CheckCDKBootstrap_SSMParameterOld() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( + new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); + + awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("1")); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + + [Fact] + public async Task CheckCDKBootstrap_SSMParameterNewer() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( + new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); + + awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("100")); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + + [Fact] + public async Task CheckCDKBootstrap_SSMParameterSame() + { + var interactiveService = new Mock(); + var commandLineWrapper = new Mock(); + var fileManager = new Mock(); + + var awsResourceQuery = new Mock(); + awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( + new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); + + awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult(AWS.Deploy.Constants.CDK.CDKTemplateVersion.ToString())); + + + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object); + + Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); + } + } +}