From 93b26bb7e0801cf2dc79dfc2cb79028a6305d8f9 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Tue, 18 Jan 2022 02:36:51 +0000 Subject: [PATCH 1/8] build: version bump to 0.33 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index d49df61a9..154c83384 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.32", + "version": "0.33", "publicReleaseRefSpec": [ ".*" ], From 1b815b809ab2a615f071580093e7f983bbc6ecf0 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Thu, 20 Jan 2022 23:59:57 -0500 Subject: [PATCH 2/8] fix: KeyValue types not working with server mode --- .../OptionSettingItem.ValueOverride.cs | 5 ++ src/AWS.Deploy.Common/Recommendation.cs | 7 +-- .../RecommendationTests.cs | 28 ++++++++++ .../SetOptionSettingTests.cs | 55 ++++++++++++++++--- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 87a0cbf76..b083f73d2 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -118,6 +118,11 @@ public void SetValueOverride(object valueOverride) { _valueOverride = valueOverride; } + else if (Type.Equals(OptionSettingValueType.KeyValue)) + { + var deserialized = JsonConvert.DeserializeObject>(valueOverride?.ToString() ?? ""); + _valueOverride = deserialized; + } else if (valueOverride is string valueOverrideString) { if (bool.TryParse(valueOverrideString, out var valueOverrideBool)) diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 98c89cf3c..51a733c6c 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -91,7 +91,6 @@ public IEnumerable GetConfigurableOptionSettingItems() /// Throws exception if there is no that matches /> /// In case an option setting of type is encountered, /// that can have the key value pair name as the leaf node with the option setting Id as the node before that. - /// In case there are multiple nodes after a , then that indicates an invalid /// /// /// Dot (.) separated key values string pointing to an option setting. @@ -118,11 +117,7 @@ public OptionSettingItem GetOptionSetting(string? jsonPath) } if (optionSetting.Type.Equals(OptionSettingValueType.KeyValue)) { - if (i + 2 == ids.Length) - return optionSetting; - else - throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"The Option Setting Item {jsonPath} does not exist as part of the" + - $" {Recipe.Name} recipe"); + return optionSetting; } } diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index 0eb82440c..8b4c8b27e 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -264,6 +264,34 @@ public async Task ApplyProjectNameToSettings() Assert.Equal("CustomAppStack-dev", beanstalkRecommendation.GetOptionSettingValue(beanstalEnvNameSetting)); } + [Fact] + public async Task GetKeyValueOptionSettingServerMode() + { + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var envVarsSetting = beanstalkRecommendation.GetOptionSetting("ElasticBeanstalkEnvironmentVariables"); + + Assert.Equal(OptionSettingValueType.KeyValue, envVarsSetting.Type); + } + + [Fact] + public async Task GetKeyValueOptionSettingConfigFile() + { + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var envVarsSetting = beanstalkRecommendation.GetOptionSetting("ElasticBeanstalkEnvironmentVariables.Key"); + + Assert.Equal(OptionSettingValueType.KeyValue, envVarsSetting.Type); + } + [Theory] [MemberData(nameof(ShouldIncludeTestCases))] public void ShouldIncludeTests(RuleEffect effect, bool testPass, bool expectedResult) diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index 82ba16307..145ecfca6 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -13,14 +13,14 @@ using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; using Moq; +using Newtonsoft.Json; using Xunit; namespace AWS.Deploy.CLI.UnitTests { public class SetOptionSettingTests { - private readonly OptionSettingItem _optionSetting; - private readonly Recommendation _recommendation; + private readonly List _recommendations; public SetOptionSettingTests() { @@ -38,10 +38,7 @@ public SetOptionSettingTests() }; var engine = new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); - var recommendations = engine.ComputeRecommendations().GetAwaiter().GetResult(); - _recommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); - - _optionSetting = _recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); + _recommendations = engine.ComputeRecommendations().GetAwaiter().GetResult(); } /// @@ -51,9 +48,12 @@ public SetOptionSettingTests() [Fact] public void SetOptionSettingTests_AllowedValues() { - _optionSetting.SetValueOverride(_optionSetting.AllowedValues.First()); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); + optionSetting.SetValueOverride(optionSetting.AllowedValues.First()); - Assert.Equal(_optionSetting.AllowedValues.First(), _recommendation.GetOptionSettingValue(_optionSetting)); + Assert.Equal(optionSetting.AllowedValues.First(), recommendation.GetOptionSettingValue(optionSetting)); } /// @@ -65,7 +65,44 @@ public void SetOptionSettingTests_AllowedValues() [Fact] public void SetOptionSettingTests_MappedValues() { - Assert.Throws(() => _optionSetting.SetValueOverride(_optionSetting.ValueMapping.Values.First())); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); + Assert.Throws(() => optionSetting.SetValueOverride(optionSetting.ValueMapping.Values.First())); + } + + [Fact] + public void SetOptionSettingTests_KeyValueType() + { + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); + var values = new Dictionary() { { "key", "value" } }; + optionSetting.SetValueOverride(values); + + Assert.Equal(values, recommendation.GetOptionSettingValue>(optionSetting)); + } + + [Fact] + public void SetOptionSettingTests_KeyValueType_String() + { + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); + var dictionary = new Dictionary() { { "key", "value" } }; + var dictionaryString = JsonConvert.SerializeObject(dictionary); + optionSetting.SetValueOverride(dictionaryString); + + Assert.Equal(dictionary, recommendation.GetOptionSettingValue>(optionSetting)); + } + + [Fact] + public void SetOptionSettingTests_KeyValueType_Error() + { + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); + Assert.Throws(() => optionSetting.SetValueOverride("string")); } } } From 44631df7f036c152b1b2a3e197def201febf0d5b Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 18 Jan 2022 12:14:46 -0500 Subject: [PATCH 3/8] feat: Improve telemetry by capturing CDK deployment failure message --- src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj | 2 +- src/AWS.Deploy.Common/Exceptions.cs | 3 +- .../AWS.Deploy.Orchestration.csproj | 2 +- .../CdkProjectHandler.cs | 41 +++++++++++++++++-- .../Data/AWSResourceQueryer.cs | 17 ++++++++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 2 +- .../Utilities/TestToolAWSResourceQueryer.cs | 1 + .../Utilities/TestToolAWSResourceQueryer.cs | 1 + 8 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj index 953f171fd..a47755778 100644 --- a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj +++ b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 86efe10da..0a73a37e9 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -90,7 +90,8 @@ public enum DeployToolErrorCode CompatibleRecommendationForRedeploymentNotFound = 10006800, InvalidSaveDirectoryForCdkProject = 10006900, FailedToFindDeploymentProjectRecipeId = 10007000, - UnexpectedError = 10007100 + UnexpectedError = 10007100, + FailedToCreateCdkStack = 10007200 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj index 5672b8d87..c504ed49d 100644 --- a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj +++ b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 6ab89ac1a..643f11567 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -2,10 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; +using Amazon.CloudFormation; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.Recipes.CDK.Common; @@ -15,7 +18,7 @@ public interface ICdkProjectHandler { Task ConfigureCdkProject(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); string CreateCdkProject(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); - Task DeployCdkProject(OrchestratorSession session, string cdkProjectPath, Recommendation recommendation); + Task DeployCdkProject(OrchestratorSession session, CloudApplication cloudApplication, string cdkProjectPath, Recommendation recommendation); void DeleteTemporaryCdkProject(OrchestratorSession session, string cdkProjectPath); } @@ -25,11 +28,16 @@ public class CdkProjectHandler : ICdkProjectHandler private readonly ICommandLineWrapper _commandLineWrapper; private readonly CdkAppSettingsSerializer _appSettingsBuilder; private readonly IDirectoryManager _directoryManager; + private readonly IAWSResourceQueryer _awsResourceQueryer; - public CdkProjectHandler(IOrchestratorInteractiveService interactiveService, ICommandLineWrapper commandLineWrapper) + public CdkProjectHandler( + IOrchestratorInteractiveService interactiveService, + ICommandLineWrapper commandLineWrapper, + IAWSResourceQueryer awsResourceQueryer) { _interactiveService = interactiveService; _commandLineWrapper = commandLineWrapper; + _awsResourceQueryer = awsResourceQueryer; _appSettingsBuilder = new CdkAppSettingsSerializer(); _directoryManager = new DirectoryManager(); } @@ -61,7 +69,7 @@ public async Task ConfigureCdkProject(OrchestratorSession session, Cloud return cdkProjectPath; } - public async Task DeployCdkProject(OrchestratorSession session, string cdkProjectPath, Recommendation recommendation) + public async Task DeployCdkProject(OrchestratorSession session, CloudApplication cloudApplication, string cdkProjectPath, Recommendation recommendation) { var recipeInfo = $"{recommendation.Recipe.Id}_{recommendation.Recipe.Version}"; var environmentVariables = new Dictionary @@ -77,6 +85,7 @@ await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{ var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); + 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}\"", @@ -86,10 +95,36 @@ await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{ redirectIO: true, streamOutputToInteractiveService: true); + await CheckCdkDeploymentFailure(cloudApplication, deploymentStartDate); + if (cdkDeploy.ExitCode != 0) throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, "We had an issue deploying your application to AWS. Check the deployment output for more details."); } + private async Task CheckCdkDeploymentFailure(CloudApplication cloudApplication, DateTime deploymentStartDate) + { + try + { + var stackEvents = await _awsResourceQueryer.GetCloudFormationStackEvents(cloudApplication.StackName); + + var failedEvents = stackEvents + .Where(x => x.Timestamp >= deploymentStartDate) + .Where(x => + x.ResourceStatus.Equals(ResourceStatus.CREATE_FAILED) || + x.ResourceStatus.Equals(ResourceStatus.UPDATE_FAILED) + ); + if (failedEvents.Any()) + { + var errors = string.Join(". ", failedEvents.Reverse().Select(x => x.ResourceStatusReason)); + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, errors); + } + } + catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack [{cloudApplication.StackName}] does not exist")) + { + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToCreateCdkStack, "A CloudFormation stack was not created. Check the deployment output for more details."); + } + } + public string CreateCdkProject(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) { string? assemblyName; diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index cd5017bd7..b5c145eea 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -39,6 +39,7 @@ namespace AWS.Deploy.Orchestration.Data { public interface IAWSResourceQueryer { + Task> GetCloudFormationStackEvents(string stackName); Task> ListOfAvailableInstanceTypes(); Task DescribeAppRunnerService(string serviceArn); Task> DescribeCloudFormationResources(string stackName); @@ -79,6 +80,22 @@ public AWSResourceQueryer(IAWSClientFactory awsClientFactory) _awsClientFactory = awsClientFactory; } + public async Task> GetCloudFormationStackEvents(string stackName) + { + var cfClient = _awsClientFactory.GetAWSClient(); + var stackEvents = new List(); + var listInstanceTypesPaginator = cfClient.Paginators.DescribeStackEvents(new DescribeStackEventsRequest { + StackName = stackName + }); + + await foreach (var response in listInstanceTypesPaginator.Responses) + { + stackEvents.AddRange(response.StackEvents); + } + + return stackEvents; + } + public async Task> ListOfAvailableInstanceTypes() { var ec2Client = _awsClientFactory.GetAWSClient(); diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 9b60caf92..0ce116ff7 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -166,7 +166,7 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm try { - await _cdkProjectHandler.DeployCdkProject(_session, cdkProject, recommendation); + await _cdkProjectHandler.DeployCdkProject(_session, cloudApplication, cdkProject, recommendation); } finally { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index 2aca9720d..b4c376b93 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -54,5 +54,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> ListOfSNSTopicArns() => throw new NotImplementedException(); public Task> ListOfS3Buckets() => throw new NotImplementedException(); public Task> ListOfAvailableInstanceTypes() => throw new NotImplementedException(); + public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index 9401b8511..873a9c0e9 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -79,5 +79,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> ListOfSNSTopicArns() => throw new NotImplementedException(); public Task> ListOfS3Buckets() => throw new NotImplementedException(); public Task> ListOfAvailableInstanceTypes() => throw new NotImplementedException(); + public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); } } From c563dac6493f025ab69e2b91c43bfd25c91ee594 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Thu, 27 Jan 2022 14:43:30 -0500 Subject: [PATCH 4/8] fix: beanstalk error at startup when in non-default regions --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 5 +-- .../Controllers/DeploymentController.cs | 8 ++-- .../ServerMode/ExtensionMethods.cs | 39 +++++++++++++++++++ src/AWS.Deploy.CLI/ServerMode/Startup.cs | 2 + src/AWS.Deploy.Common/Exceptions.cs | 3 +- src/AWS.Deploy.Common/Recommendation.cs | 37 ++++++++++++++---- src/AWS.Deploy.Constants/CLI.cs | 3 ++ .../Data/AWSResourceQueryer.cs | 2 +- src/AWS.Deploy.Orchestration/Exceptions.cs | 8 ++++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 30 +++++++------- 10 files changed, 105 insertions(+), 32 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index a830e1831..7d59625aa 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -26,8 +26,6 @@ namespace AWS.Deploy.CLI.Commands { public class DeployCommand { - public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; - private readonly IToolInteractiveService _toolInteractiveService; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; private readonly ICdkProjectHandler _cdkProjectHandler; @@ -197,8 +195,7 @@ private void DisplayOutputResources(List displayedResourc } } - // Apply the user entered stack name to the recommendation so that any default settings based on stack name are applied. - selectedRecommendation.AddReplacementToken(REPLACE_TOKEN_STACK_NAME, cloudApplicationName); + await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, cloudApplicationName); var cloudApplication = new CloudApplication(cloudApplicationName, selectedRecommendation.Recipe.Id); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 7967594a7..cbace67d9 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -344,6 +344,9 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody return NotFound($"Session ID {sessionId} not found."); } + var serviceProvider = CreateSessionServiceProvider(state); + var orchestrator = CreateOrchestrator(state, serviceProvider); + if(!string.IsNullOrEmpty(input.NewDeploymentRecipeId) && !string.IsNullOrEmpty(input.NewDeploymentName)) { @@ -355,11 +358,10 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody state.ApplicationDetails.Name = input.NewDeploymentName; state.ApplicationDetails.RecipeId = input.NewDeploymentRecipeId; - state.SelectedRecommendation.AddReplacementToken(DeployCommand.REPLACE_TOKEN_STACK_NAME, input.NewDeploymentName); + await orchestrator.ApplyAllReplacementTokens(state.SelectedRecommendation, input.NewDeploymentName); } else if(!string.IsNullOrEmpty(input.ExistingDeploymentName)) { - var serviceProvider = CreateSessionServiceProvider(state); var templateMetadataReader = serviceProvider.GetRequiredService(); var existingDeployment = state.ExistingDeployments?.FirstOrDefault(x => string.Equals(input.ExistingDeploymentName, x.Name)); @@ -379,7 +381,7 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody state.ApplicationDetails.Name = input.ExistingDeploymentName; state.ApplicationDetails.RecipeId = existingDeployment.RecipeId; - state.SelectedRecommendation.AddReplacementToken(DeployCommand.REPLACE_TOKEN_STACK_NAME, input.ExistingDeploymentName); + await orchestrator.ApplyAllReplacementTokens(state.SelectedRecommendation, input.ExistingDeploymentName); } return Ok(); diff --git a/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs b/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs index ba4673764..95eb368ea 100644 --- a/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs +++ b/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs @@ -7,6 +7,13 @@ using System.Threading.Tasks; using System.Security.Claims; using Amazon.Runtime; +using Microsoft.AspNetCore.Builder; +using System.Net; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using System.Text.Json; +using AWS.Deploy.CLI.ServerMode.Models; +using AWS.Deploy.Common; namespace AWS.Deploy.CLI.ServerMode { @@ -34,5 +41,37 @@ public static class ExtensionMethods return new BasicAWSCredentials(awsAccessKeyId, awsSecretKey); } + + public static void ConfigureExceptionHandler(this IApplicationBuilder app) + { + app.UseExceptionHandler(error => + { + error.Run(async context => + { + context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + var contextFeature = context.Features.Get(); + if (contextFeature != null) + { + var exceptionString = ""; + if (contextFeature.Error is DeployToolException deployToolException) + { + exceptionString = JsonSerializer.Serialize( + new DeployToolExceptionSummary( + deployToolException.ErrorCode.ToString(), + deployToolException.Message)); + } + else + { + exceptionString = JsonSerializer.Serialize( + new DeployToolExceptionSummary( + DeployToolErrorCode.UnexpectedError.ToString(), + contextFeature.Error?.Message ?? string.Empty)); + } + await context.Response.WriteAsync(exceptionString); + } + }); + }); + } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Startup.cs b/src/AWS.Deploy.CLI/ServerMode/Startup.cs index 1b16a3f30..51e80896e 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Startup.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Startup.cs @@ -84,6 +84,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(); + app.ConfigureExceptionHandler(); + app.UseRouting(); app.UseAuthentication(); diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 0a73a37e9..68bb60e8f 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -91,7 +91,8 @@ public enum DeployToolErrorCode InvalidSaveDirectoryForCdkProject = 10006900, FailedToFindDeploymentProjectRecipeId = 10007000, UnexpectedError = 10007100, - FailedToCreateCdkStack = 10007200 + FailedToCreateCdkStack = 10007200, + FailedToFindElasticBeanstalkSolutionStack = 10007300 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 51a733c6c..9a2f54f82 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.Recipes; @@ -31,7 +32,7 @@ public class Recommendation : IUserInputOption public readonly List DeploymentBundleSettings = new (); - private readonly Dictionary _replacementTokens = new(); + public readonly Dictionary ReplacementTokens = new(); public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefinition, List deploymentBundleSettings, int computedPriority, Dictionary additionalReplacements) { @@ -44,10 +45,30 @@ public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefiniti DeploymentBundle = new DeploymentBundle(); DeploymentBundleSettings = deploymentBundleSettings; + CollectRecommendationReplacementTokens(Recipe.OptionSettings); + foreach (var replacement in additionalReplacements) { - if (!_replacementTokens.ContainsKey(replacement.Key)) - _replacementTokens[replacement.Key] = replacement.Value; + ReplacementTokens[replacement.Key] = replacement.Value; + } + } + + private void CollectRecommendationReplacementTokens(List optionSettings) + { + foreach (var optionSetting in optionSettings) + { + string defaultValue = optionSetting.DefaultValue?.ToString() ?? ""; + Regex regex = new Regex(@"^.*\{[\w\d]+\}.*$"); + Match match = regex.Match(defaultValue); + + if (match.Success) + { + var replacement = defaultValue.Substring(defaultValue.IndexOf("{"), defaultValue.IndexOf("}") + 1); + ReplacementTokens[replacement] = ""; + } + + if (optionSetting.ChildOptionSettings.Any()) + CollectRecommendationReplacementTokens(optionSetting.ChildOptionSettings); } } @@ -62,7 +83,7 @@ public Recommendation ApplyPreviousSettings(IDictionary previous public void AddReplacementToken(string key, string value) { - _replacementTokens[key] = value; + ReplacementTokens[key] = value; } private void ApplyPreviousSettings(Recommendation recommendation, IDictionary previousSettings) @@ -134,7 +155,7 @@ public T GetOptionSettingValue(OptionSettingItem optionSetting) displayableOptionSettings.Add(childOptionSetting.Id, IsOptionSettingDisplayable(childOptionSetting)); } } - return optionSetting.GetValue(_replacementTokens, displayableOptionSettings); + return optionSetting.GetValue(ReplacementTokens, displayableOptionSettings); } public object GetOptionSettingValue(OptionSettingItem optionSetting) @@ -147,17 +168,17 @@ public object GetOptionSettingValue(OptionSettingItem optionSetting) displayableOptionSettings.Add(childOptionSetting.Id, IsOptionSettingDisplayable(childOptionSetting)); } } - return optionSetting.GetValue(_replacementTokens, displayableOptionSettings); + return optionSetting.GetValue(ReplacementTokens, displayableOptionSettings); } public T? GetOptionSettingDefaultValue(OptionSettingItem optionSetting) { - return optionSetting.GetDefaultValue(_replacementTokens); + return optionSetting.GetDefaultValue(ReplacementTokens); } public object? GetOptionSettingDefaultValue(OptionSettingItem optionSetting) { - return optionSetting.GetDefaultValue(_replacementTokens); + return optionSetting.GetDefaultValue(ReplacementTokens); } /// diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index 2644050c1..a93832010 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -14,5 +14,8 @@ internal static class CLI public const string PROMPT_CHOOSE_STACK_NAME = "Choose stack to deploy to"; public const string CLI_APP_NAME = "AWS .NET Deployment Tool"; + + public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; + public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; } } diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index b5c145eea..ffeeecb04 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -381,7 +381,7 @@ public async Task GetLatestElasticBeanstalkPlatformArn() if (!platforms.Any()) { - throw new AmazonElasticBeanstalkException(".NET Core Solution Stack doesn't exist."); + throw new FailedToFindElasticBeanstalkSolutionStackException(DeployToolErrorCode.FailedToFindElasticBeanstalkSolutionStack, "Cannot use Elastic Beanstalk deployments because we cannot find a .NET Core Solution Stack to use. One possible reason could be that Elastic Beanstalk is not enabled in your region if you are using a non-default region."); } return platforms.First(); diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index b4acc3822..51cfcf3e6 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -171,4 +171,12 @@ public class DockerInfoException : DeployToolException { public DockerInfoException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if unable to find an Elastic Beanstalk .NET solution stack. + /// + public class FailedToFindElasticBeanstalkSolutionStackException : DeployToolException + { + public FailedToFindElasticBeanstalkSolutionStackException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 0ce116ff7..e48e733e8 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -23,8 +23,6 @@ namespace AWS.Deploy.Orchestration /// public class Orchestrator { - private const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; - private readonly ICdkProjectHandler? _cdkProjectHandler; private readonly ICDKManager? _cdkManager; private readonly ICDKVersionDetector? _cdkVersionDetector; @@ -85,8 +83,7 @@ public async Task> GenerateDeploymentRecommendations() var customRecipePaths = await LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath); var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths.Union(customRecipePaths), _session); - var additionalReplacements = await GetReplacements(); - return await engine.ComputeRecommendations(additionalReplacements); + return await engine.ComputeRecommendations(); } public async Task> GenerateRecommendationsToSaveDeploymentProject() @@ -110,21 +107,24 @@ public async Task> GenerateRecommendationsFromSavedDeployme throw new InvalidCliArgumentException(DeployToolErrorCode.DeploymentProjectPathNotFound, $"The path '{deploymentProjectPath}' does not exists on the file system. Please provide a valid deployment project path and try again."); var engine = new RecommendationEngine.RecommendationEngine(new List { deploymentProjectPath }, _session); - var additionalReplacements = await GetReplacements(); - return await engine.ComputeRecommendations(additionalReplacements); + return await engine.ComputeRecommendations(); } - public async Task> GetReplacements() + public async Task ApplyAllReplacementTokens(Recommendation recommendation, string cloudApplicationName) { - var replacements = new Dictionary(); - - if (_awsResourceQueryer == null) - throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); - - var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(); - replacements[REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN] = latestPlatform.PlatformArn; + if (recommendation.ReplacementTokens.ContainsKey(Constants.CLI.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN)) + { + if (_awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); - return replacements; + var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(); + recommendation.AddReplacementToken(Constants.CLI.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN, latestPlatform.PlatformArn); + } + if (recommendation.ReplacementTokens.ContainsKey(Constants.CLI.REPLACE_TOKEN_STACK_NAME)) + { + // Apply the user entered stack name to the recommendation so that any default settings based on stack name are applied. + recommendation.AddReplacementToken(Constants.CLI.REPLACE_TOKEN_STACK_NAME, cloudApplicationName); + } } public async Task DeployRecommendation(CloudApplication cloudApplication, Recommendation recommendation) From 75c94616ebb30a96f8a7d6bee049855ed30fd0ee Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 2 Feb 2022 13:05:30 -0500 Subject: [PATCH 5/8] fix: deployment project fails if .aws-dotnet-deploy directory does not exist --- src/AWS.Deploy.Orchestration/CDK/CDKInstaller.cs | 8 +++++++- .../CDK/CDKInstallerTests.cs | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKInstaller.cs b/src/AWS.Deploy.Orchestration/CDK/CDKInstaller.cs index 65e9985ba..9f90b32ba 100644 --- a/src/AWS.Deploy.Orchestration/CDK/CDKInstaller.cs +++ b/src/AWS.Deploy.Orchestration/CDK/CDKInstaller.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.Utilities; namespace AWS.Deploy.Orchestration.CDK @@ -35,16 +36,21 @@ public interface ICDKInstaller public class CDKInstaller : ICDKInstaller { private readonly ICommandLineWrapper _commandLineWrapper; + private readonly IDirectoryManager _directoryManager; - public CDKInstaller(ICommandLineWrapper commandLineWrapper) + public CDKInstaller(ICommandLineWrapper commandLineWrapper, IDirectoryManager directoryManager) { _commandLineWrapper = commandLineWrapper; + _directoryManager = directoryManager; } public async Task> GetVersion(string workingDirectory) { const string command = "npx --no-install cdk --version"; + if (!_directoryManager.Exists(workingDirectory)) + _directoryManager.CreateDirectory(workingDirectory); + TryRunResult result; try diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKInstallerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKInstallerTests.cs index c24b723db..f79d860bc 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKInstallerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKInstallerTests.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Utilities; using Xunit; @@ -13,12 +14,14 @@ public class CDKInstallerTests { private readonly TestCommandLineWrapper _commandLineWrapper; private readonly CDKInstaller _cdkInstaller; + private readonly IDirectoryManager _directoryManager; private const string _workingDirectory = @"c:\fake\path"; public CDKInstallerTests() { _commandLineWrapper = new TestCommandLineWrapper(); - _cdkInstaller = new CDKInstaller(_commandLineWrapper); + _directoryManager = new TestDirectoryManager(); + _cdkInstaller = new CDKInstaller(_commandLineWrapper, _directoryManager); } [Fact] From b9cbdab93d979f4a6ed4287af02f643702b08b5f Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Thu, 13 Jan 2022 16:29:48 -0500 Subject: [PATCH 6/8] feat: Support Elastic Beanstalk backwards compatibility --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 15 +- .../DeployCommandHandlerInput.cs | 2 +- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 100 ++++--- .../Commands/ListDeploymentsCommand.cs | 7 +- .../CustomServiceCollectionExtension.cs | 4 + .../Controllers/DeploymentController.cs | 44 ++- src/AWS.Deploy.CLI/ServerMode/Exceptions.cs | 8 + .../ServerMode/Models/DeploymentTypes.cs | 11 + .../Models/ExistingDeploymentSummary.cs | 24 +- .../Models/RecommendationSummary.cs | 18 +- .../Models/SetDeploymentTargetInput.cs | 2 +- src/AWS.Deploy.CLI/ServerMode/SessionState.cs | 2 +- src/AWS.Deploy.Common/CloudApplication.cs | 39 ++- .../CloudApplicationResourceType.cs | 16 ++ src/AWS.Deploy.Common/Exceptions.cs | 10 +- src/AWS.Deploy.Common/IO/FileManager.cs | 9 + .../Recipes/DeploymentTypes.cs | 5 +- .../Recipes/RecipeDefinition.cs | 5 + .../UserDeploymentSettings.cs | 2 +- .../AWS.Deploy.Constants.projitems | 2 + src/AWS.Deploy.Constants/CLI.cs | 6 +- src/AWS.Deploy.Constants/ElasticBeanstalk.cs | 39 +++ src/AWS.Deploy.Constants/RecipeIdentifier.cs | 13 + .../CdkAppSettingsSerializer.cs | 2 +- .../CdkProjectHandler.cs | 4 +- .../Data/AWSResourceQueryer.cs | 50 +++- .../DeploymentBundleHandler.cs | 4 +- .../BeanstalkEnvironmentDeploymentCommand.cs | 70 +++++ .../CdkDeploymentCommand.cs | 82 ++++++ .../DeploymentCommandFactory.cs | 38 +++ .../DeploymentCommands/DeploymentStatus.cs | 11 + .../DeploymentCommands/IDeploymentCommand.cs | 18 ++ .../DisplayedResourcesHandler.cs | 34 +-- src/AWS.Deploy.Orchestration/Exceptions.cs | 25 ++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 87 ++---- .../AWSElasticBeanstalkHandler.cs | 201 +++++++++++++ .../ServiceHandlers/AWSS3Handler.cs | 80 ++++++ .../ServiceHandlers/AWSServiceHandler.cs | 27 ++ .../CloudApplicationNameGenerator.cs | 2 +- .../Utilities/DeployedApplicationQueryer.cs | 186 +++++++++--- ....NETAppExistingBeanstalkEnvironment.recipe | 88 ++++++ .../aws-deploy-recipe-schema.json | 13 +- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 26 +- .../ECSFargateDeploymentTest.cs | 2 +- .../ElasticBeanStalkDeploymentTest.cs | 2 +- .../ElasticBeanStalkKeyValueDeploymentTest.cs | 2 +- .../UnitTestFiles/ECSFargateConfigFile.json | 2 +- .../ElasticBeanStalkConfigFile.json | 2 +- .../ElasticBeanStalkKeyPairConfigFile.json | 2 +- .../IO/TestFileManager.cs | 4 + .../BlazorWasmTests.cs | 6 +- .../ECSFargateDeploymentTest.cs | 4 +- .../ElasticBeanStalkDeploymentTest.cs | 4 +- .../ConsoleAppTests.cs | 4 +- .../RecommendationTests.cs | 15 +- .../RedeploymentTests.cs | 8 +- .../ServerModeTests.cs | 40 +++ .../Utilities/TestToolAWSResourceQueryer.cs | 2 + .../WebAppNoDockerFileTests.cs | 4 +- .../WebAppWithDockerFileTests.cs | 10 +- .../DeploymentBundleHandlerTests.cs | 12 +- .../ServerModeTests.cs | 36 +++ .../Utilities/TestToolAWSResourceQueryer.cs | 2 + .../DeployedApplicationQueryerTests.cs | 264 +++++++++++++++++- .../DeploymentCommandFactoryTests.cs | 24 ++ .../DisplayedResourcesHandlerTests.cs | 2 +- .../ElasticBeanstalkHandlerTests.cs | 143 ++++++++++ .../TestFileManager.cs | 4 + .../CloudApplicationNameGeneratorTests.cs | 2 +- .../ElasticBeanStalkConfigFile.json | 2 +- .../ECSFargateConfigFile.json | 2 +- 71 files changed, 1750 insertions(+), 287 deletions(-) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/DeploymentTypes.cs create mode 100644 src/AWS.Deploy.Common/CloudApplicationResourceType.cs create mode 100644 src/AWS.Deploy.Constants/ElasticBeanstalk.cs create mode 100644 src/AWS.Deploy.Constants/RecipeIdentifier.cs create mode 100644 src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs create mode 100644 src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs create mode 100644 src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentCommandFactory.cs create mode 100644 src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentStatus.cs create mode 100644 src/AWS.Deploy.Orchestration/DeploymentCommands/IDeploymentCommand.cs create mode 100644 src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs create mode 100644 src/AWS.Deploy.Orchestration/ServiceHandlers/AWSS3Handler.cs create mode 100644 src/AWS.Deploy.Orchestration/ServiceHandlers/AWSServiceHandler.cs create mode 100644 src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/DeploymentCommandFactoryTests.cs create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 74c1df85b..e33f9bc2b 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -23,6 +23,7 @@ using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Orchestration.DisplayedResources; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.CLI.Commands { @@ -36,7 +37,7 @@ public class CommandFactory : ICommandFactory private static readonly Option _optionProfile = new("--profile", "AWS credential profile used to make calls to AWS."); 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 _optionApplicationName = new("--application-name", "Name of the cloud application. If you choose to deploy via CloudFormation, this name will be used to identify the CloudFormation stack."); 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."); @@ -69,6 +70,7 @@ public class CommandFactory : ICommandFactory private readonly ICustomRecipeLocator _customRecipeLocator; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly ICDKVersionDetector _cdkVersionDetector; + private readonly IAWSServiceHandler _awsServiceHandler; public CommandFactory( IToolInteractiveService toolInteractiveService, @@ -93,7 +95,8 @@ public CommandFactory( IDeploymentManifestEngine deploymentManifestEngine, ICustomRecipeLocator customRecipeLocator, ILocalUserSettingsEngine localUserSettingsEngine, - ICDKVersionDetector cdkVersionDetector) + ICDKVersionDetector cdkVersionDetector, + IAWSServiceHandler awsServiceHandler) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -118,6 +121,7 @@ public CommandFactory( _customRecipeLocator = customRecipeLocator; _localUserSettingsEngine = localUserSettingsEngine; _cdkVersionDetector = cdkVersionDetector; + _awsServiceHandler = awsServiceHandler; } public Command BuildRootCommand() @@ -153,7 +157,7 @@ private Command BuildDeployCommand() deployCommand.Add(_optionProfile); deployCommand.Add(_optionRegion); deployCommand.Add(_optionProjectPath); - deployCommand.Add(_optionStackName); + deployCommand.Add(_optionApplicationName); deployCommand.Add(_optionApply); deployCommand.Add(_optionDiagnosticLogging); deployCommand.Add(_optionDisableInteractive); @@ -216,7 +220,8 @@ private Command BuildDeployCommand() _systemCapabilityEvaluator, session, _directoryManager, - _fileManager); + _fileManager, + _awsServiceHandler); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; if (!string.IsNullOrEmpty(deploymentProjectPath)) @@ -224,7 +229,7 @@ private Command BuildDeployCommand() deploymentProjectPath = Path.GetFullPath(deploymentProjectPath, targetApplicationDirectoryPath); } - await deploy.ExecuteAsync(input.StackName ?? "", deploymentProjectPath, userDeploymentSettings); + await deploy.ExecuteAsync(input.ApplicationName ?? string.Empty, deploymentProjectPath, userDeploymentSettings); return CommandReturnCodes.SUCCESS; } diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs index 7e65326cd..c7a8837ba 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs @@ -13,7 +13,7 @@ public class DeployCommandHandlerInput public string? Profile { get; set; } public string? Region { get; set; } public string? ProjectPath { get; set; } - public string? StackName { get; set; } + public string? ApplicationName { get; set; } public string? Apply { get; set; } public bool Diagnostics { get; set; } public bool Silent { get; set; } diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 7d59625aa..79d498a7c 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -21,6 +21,7 @@ using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.LocalUserSettings; using Newtonsoft.Json; +using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.CLI.Commands { @@ -46,6 +47,7 @@ public class DeployCommand private readonly IDirectoryManager _directoryManager; private readonly IFileManager _fileManager; private readonly ICDKVersionDetector _cdkVersionDetector; + private readonly IAWSServiceHandler _awsServiceHandler; public DeployCommand( IToolInteractiveService toolInteractiveService, @@ -67,7 +69,8 @@ public DeployCommand( ISystemCapabilityEvaluator systemCapabilityEvaluator, OrchestratorSession session, IDirectoryManager directoryManager, - IFileManager fileManager) + IFileManager fileManager, + IAWSServiceHandler awsServiceHandler) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -89,11 +92,12 @@ public DeployCommand( _cdkManager = cdkManager; _customRecipeLocator = customRecipeLocator; _systemCapabilityEvaluator = systemCapabilityEvaluator; + _awsServiceHandler = awsServiceHandler; } - public async Task ExecuteAsync(string stackName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) + public async Task ExecuteAsync(string applicationName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) { - var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(stackName, userDeploymentSettings, deploymentProjectPath); + var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(applicationName, userDeploymentSettings, deploymentProjectPath); // Verify Docker installation and minimum NodeJS version. await EvaluateSystemCapabilities(selectedRecommendation); @@ -133,13 +137,13 @@ private void DisplayOutputResources(List displayedResourc /// /// Initiates a deployment or a re-deployment. /// If a new Cloudformation stack name is selected, then a fresh deployment is initiated with the user-selected deployment recipe. - /// If an existing Cloudformation stack name is selected, then a re-deployment is initiated with the same deployment recipe. + /// If an existing deployment target is selected, then a re-deployment is initiated with the same deployment recipe. /// - /// The stack name provided via the --stack-name CLI argument + /// The cloud application name provided via the --application-name CLI argument /// The deserialized object from the user provided config file. /// The absolute or relative path of the CDK project that will be used for deployment /// A tuple consisting of the Orchestrator object, Selected Recommendation, Cloud Application metadata. - public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string stackName, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) + public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string applicationName, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) { var orchestrator = new Orchestrator( _session, @@ -153,30 +157,32 @@ private void DisplayOutputResources(List displayedResourc _dockerEngine, _customRecipeLocator, new List { RecipeLocator.FindRecipeDefinitionsPath() }, - _directoryManager); + _fileManager, + _directoryManager, + _awsServiceHandler); // Determine what recommendations are possible for the project. var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath); // Get all existing applications that were previously deployed using our deploy tool. - var allDeployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(); + var allDeployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList()); // Filter compatible applications that can be re-deployed using the current set of recommendations. var compatibleApplications = await _deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, _session); - // Get Cloudformation stack name. - var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, compatibleApplications); + // Get CloudApplication name. + var cloudApplicationName = GetCloudApplicationName(applicationName, userDeploymentSettings, compatibleApplications); - // Find existing application with the same CloudFormation stack name. + // Find existing application with the same CloudApplication name. var deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName)); Recommendation? selectedRecommendation = null; if (deployedApplication != null) { // Verify that the target application can be deployed using the current set of recommendations - if (!compatibleApplications.Any(app => app.StackName.Equals(deployedApplication.StackName, StringComparison.Ordinal))) + if (!compatibleApplications.Any(app => app.Name.Equals(deployedApplication.Name, StringComparison.Ordinal))) { - var errorMessage = $"{deployedApplication.StackName} already exists as a Cloudformation stack but a compatible recommendation to perform a redeployment was not found"; + var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation to perform a redeployment was not found"; throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage); } @@ -191,13 +197,14 @@ private void DisplayOutputResources(List displayedResourc } else { - selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); + // Filter the recommendation list for a NEW deployment with recipes which have the DisableNewDeployments property set to false. + selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations.Where(x => !x.Recipe.DisableNewDeployments).ToList()); } } await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, cloudApplicationName); - var cloudApplication = new CloudApplication(cloudApplicationName, selectedRecommendation.Recipe.Id); + var cloudApplication = new CloudApplication(cloudApplicationName, deployedApplication?.UniqueIdentifier ?? string.Empty, deployedApplication?.ResourceType ?? CloudApplicationResourceType.CloudFormationStack, selectedRecommendation.Recipe.Id); return (orchestrator, selectedRecommendation, cloudApplication); } @@ -267,18 +274,17 @@ private async Task> GenerateDeploymentRecommendations(Orche private async Task GetSelectedRecommendationFromPreviousDeployment(List recommendations, CloudApplication deployedApplication, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) { - var existingCloudApplicationMetadata = await _templateMetadataReader.LoadCloudApplicationMetadata(deployedApplication.Name); var deploymentSettingRecipeId = userDeploymentSettings?.RecipeId; var selectedRecommendation = await GetRecommendationForRedeployment(recommendations, deployedApplication, deploymentProjectPath); if (selectedRecommendation == null) { - var errorMessage = $"{deployedApplication.StackName} already exists as a Cloudformation stack but a compatible recommendation used to perform a re-deployment was not found."; + var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation used to perform a re-deployment was not found."; throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage); } if (!string.IsNullOrEmpty(deploymentSettingRecipeId) && !string.Equals(deploymentSettingRecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase)) { - var errorMessage = $"The existing stack {deployedApplication.StackName} was created from a different deployment recommendation. " + - "Deploying to an existing stack must be performed with the original deployment recommendation to avoid unintended destructive changes to the stack."; + var errorMessage = $"The existing {deployedApplication.ResourceType} {deployedApplication.Name} was created from a different deployment recommendation. " + + "Deploying to an existing target must be performed with the original deployment recommendation to avoid unintended destructive changes to the resources."; if (_toolInteractiveService.Diagnostics) { errorMessage += Environment.NewLine + $"The original deployment recipe ID was {deployedApplication.RecipeId} and the current deployment recipe ID is {deploymentSettingRecipeId}"; @@ -286,9 +292,15 @@ private async Task GetSelectedRecommendationFromPreviousDeployme throw new InvalidUserDeploymentSettingsException(DeployToolErrorCode.StackCreatedFromDifferentDeploymentRecommendation, errorMessage.Trim()); } - selectedRecommendation = selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); + IDictionary previousSettings; + if (deployedApplication.ResourceType == CloudApplicationResourceType.CloudFormationStack) + previousSettings = (await _templateMetadataReader.LoadCloudApplicationMetadata(deployedApplication.Name)).Settings; + else + previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication); + + selectedRecommendation = selectedRecommendation.ApplyPreviousSettings(previousSettings); - var header = $"Loading {deployedApplication.Name} settings:"; + var header = $"Loading {deployedApplication.DisplayName} settings:"; _toolInteractiveService.WriteLine(header); _toolInteractiveService.WriteLine(new string('-', header.Length)); @@ -457,32 +469,32 @@ private void SetDeploymentBundleOptionSetting(Recommendation recommendation, str } } - private string GetCloudApplicationName(string? stackName, UserDeploymentSettings? userDeploymentSettings, List deployedApplications) + private string GetCloudApplicationName(string? applicationName, UserDeploymentSettings? userDeploymentSettings, List deployedApplications) { - // validate the stackName provided by the --stack-name cli argument if present. - if (!string.IsNullOrEmpty(stackName)) + // validate the applicationName provided by the --application-name cli argument if present. + if (!string.IsNullOrEmpty(applicationName)) { - if (_cloudApplicationNameGenerator.IsValidName(stackName)) - return stackName; + if (_cloudApplicationNameGenerator.IsValidName(applicationName)) + return applicationName; - PrintInvalidStackNameMessage(); + PrintInvalidApplicationNameMessage(); throw new InvalidCliArgumentException(DeployToolErrorCode.InvalidCliArguments, "Found invalid CLI arguments"); } - if (!string.IsNullOrEmpty(userDeploymentSettings?.StackName)) + if (!string.IsNullOrEmpty(userDeploymentSettings?.ApplicationName)) { - if (_cloudApplicationNameGenerator.IsValidName(userDeploymentSettings.StackName)) - return userDeploymentSettings.StackName; + if (_cloudApplicationNameGenerator.IsValidName(userDeploymentSettings.ApplicationName)) + return userDeploymentSettings.ApplicationName; - PrintInvalidStackNameMessage(); + PrintInvalidApplicationNameMessage(); throw new InvalidUserDeploymentSettingsException(DeployToolErrorCode.UserDeploymentInvalidStackName, "Please provide a valid stack name and try again."); } if (_toolInteractiveService.DisableInteractive) { - var message = "The \"--silent\" CLI argument can only be used if a CDK stack name is provided either via the CLI argument \"--stack-name\" or through a deployment-settings file. " + - "Please provide a stack name and try again"; - throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsStackNameArgument, message); + var message = "The \"--silent\" CLI argument can only be used if a cloud application name is provided either via the CLI argument \"--application-name\" or through a deployment-settings file. " + + "Please provide an application name and try again"; + throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsApplicationNameArgument, message); } return AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); } @@ -542,7 +554,7 @@ private string AskUserForCloudApplicationName(ProjectDefinition project, List x.Name), + existingApplications.Select(x => x.DisplayName), title, askNewName: true, defaultNewName: defaultName, - defaultChoosePrompt: Constants.CLI.PROMPT_CHOOSE_STACK_NAME, + defaultChoosePrompt: Constants.CLI.PROMPT_CHOOSE_DEPLOYMENT_TARGET, defaultCreateNewPrompt: Constants.CLI.PROMPT_NEW_STACK_NAME, defaultCreateNewLabel: Constants.CLI.CREATE_NEW_STACK_LABEL) ; - cloudApplicationName = userResponse.SelectedOption ?? userResponse.NewName; + // Since the selected option will be the display name, we need to extract the actual name out of it. + // Ex - DisplayName = "MyAppStack (CloudFormationStack)", ActualName = "MyAppStack" + cloudApplicationName = userResponse.SelectedOption?.Split().FirstOrDefault() ?? userResponse.NewName; } if (!string.IsNullOrEmpty(cloudApplicationName) && _cloudApplicationNameGenerator.IsValidName(cloudApplicationName)) return cloudApplicationName; - PrintInvalidStackNameMessage(); + PrintInvalidApplicationNameMessage(); } } - private void PrintInvalidStackNameMessage() + private void PrintInvalidApplicationNameMessage() { _toolInteractiveService.WriteLine(); _toolInteractiveService.WriteErrorLine( - "Invalid stack name. A stack name can contain only alphanumeric characters (case-sensitive) and hyphens. " + + "Invalid application name. The application name can contain only alphanumeric characters (case-sensitive) and hyphens. " + "It must start with an alphabetic character and can't be longer than 128 characters"); } diff --git a/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs b/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs index 98ec0f28c..65f7bc0a3 100644 --- a/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs @@ -1,7 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Utilities; namespace AWS.Deploy.CLI.Commands @@ -26,10 +28,11 @@ public async Task ExecuteAsync() _interactiveService.WriteLine("Cloud Applications:"); _interactiveService.WriteLine("-------------------"); - var existingApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(); + var deploymentTypes = new List(){ DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var existingApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); foreach (var app in existingApplications) { - _interactiveService.WriteLine(app.Name); + _interactiveService.WriteLine(app.DisplayName); } } } diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 6a13d7ef1..3cedb9c0a 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -13,6 +13,7 @@ using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.DisplayedResources; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.ServiceHandlers; using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -58,6 +59,9 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(ILocalUserSettingsEngine), typeof(LocalUserSettingsEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKVersionDetector), typeof(CDKVersionDetector), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IAWSServiceHandler), typeof(AWSServiceHandler), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IS3Handler), typeof(AWSS3Handler), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IElasticBeanstalkHandler), typeof(AWSElasticBeanstalkHandler), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IPackageJsonGenerator), (serviceProvider) => new PackageJsonGenerator(packageJsonTemplate), lifetime)); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index cbace67d9..c82223be6 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -32,6 +32,7 @@ using AWS.Deploy.CLI.Commands; using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.Common.TypeHintData; +using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.CLI.ServerMode.Controllers { @@ -92,8 +93,8 @@ await _projectParserUtility.Parse(input.ProjectPath) var recommendations = await orchestrator.GenerateDeploymentRecommendations(); state.NewRecommendations = recommendations; - // Get all existing applications that were previously deployed using our deploy tool. - var allDeployedApplications = await deployedApplicationQueryer.GetExistingDeployedApplications(); + // Get all existing CloudApplications based on the deploymentTypes filter + var allDeployedApplications = await deployedApplicationQueryer.GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList()); var existingApplications = await deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, session); state.ExistingDeployments = existingApplications; @@ -137,12 +138,16 @@ public async Task GetRecommendations(string sessionId) state.NewRecommendations ??= await orchestrator.GenerateDeploymentRecommendations(); foreach (var recommendation in state.NewRecommendations) { + if (recommendation.Recipe.DisableNewDeployments) + continue; + output.Recommendations.Add(new RecommendationSummary( recipeId: recommendation.Recipe.Id, name: recommendation.Name, shortDescription: recommendation.ShortDescription, description: recommendation.Description, - targetService: recommendation.Recipe.TargetService + targetService: recommendation.Recipe.TargetService, + deploymentType: recommendation.Recipe.DeploymentType )); } @@ -324,7 +329,9 @@ public async Task GetExistingDeployments(string sessionId) description: recommendation.Description, targetService: recommendation.Recipe.TargetService, lastUpdatedTime: deployment.LastUpdatedTime, - updatedByCurrentUser: deployment.UpdatedByCurrentUser)); + updatedByCurrentUser: deployment.UpdatedByCurrentUser, + resourceType: deployment.ResourceType, + uniqueIdentifier: deployment.UniqueIdentifier)); } return Ok(output); @@ -357,17 +364,20 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody } state.ApplicationDetails.Name = input.NewDeploymentName; + state.ApplicationDetails.UniqueIdentifier = string.Empty; + state.ApplicationDetails.ResourceType = CloudApplicationResourceType.CloudFormationStack; state.ApplicationDetails.RecipeId = input.NewDeploymentRecipeId; await orchestrator.ApplyAllReplacementTokens(state.SelectedRecommendation, input.NewDeploymentName); } - else if(!string.IsNullOrEmpty(input.ExistingDeploymentName)) + else if(!string.IsNullOrEmpty(input.ExistingDeploymentId)) { var templateMetadataReader = serviceProvider.GetRequiredService(); + var deployedApplicationQueryer = serviceProvider.GetRequiredService(); - var existingDeployment = state.ExistingDeployments?.FirstOrDefault(x => string.Equals(input.ExistingDeploymentName, x.Name)); + var existingDeployment = state.ExistingDeployments?.FirstOrDefault(x => string.Equals(input.ExistingDeploymentId, x.UniqueIdentifier)); if (existingDeployment == null) { - return NotFound($"Existing deployment {input.ExistingDeploymentName} not found."); + return NotFound($"Existing deployment {input.ExistingDeploymentId} not found."); } state.SelectedRecommendation = state.NewRecommendations?.FirstOrDefault(x => string.Equals(existingDeployment.RecipeId, x.Recipe.Id)); @@ -376,12 +386,18 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody return NotFound($"Recommendation {input.NewDeploymentRecipeId} used in existing deployment {existingDeployment.RecipeId} not found."); } - var existingCloudApplicationMetadata = await templateMetadataReader.LoadCloudApplicationMetadata(input.ExistingDeploymentName); - state.SelectedRecommendation = state.SelectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); + IDictionary previousSettings; + if (existingDeployment.ResourceType == CloudApplicationResourceType.CloudFormationStack) + previousSettings = (await templateMetadataReader.LoadCloudApplicationMetadata(existingDeployment.Name)).Settings; + else + previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment); + + state.SelectedRecommendation = state.SelectedRecommendation.ApplyPreviousSettings(previousSettings); - state.ApplicationDetails.Name = input.ExistingDeploymentName; + state.ApplicationDetails.Name = existingDeployment.Name; + state.ApplicationDetails.UniqueIdentifier = existingDeployment.UniqueIdentifier; state.ApplicationDetails.RecipeId = existingDeployment.RecipeId; - await orchestrator.ApplyAllReplacementTokens(state.SelectedRecommendation, input.ExistingDeploymentName); + await orchestrator.ApplyAllReplacementTokens(state.SelectedRecommendation, existingDeployment.Name); } return Ok(); @@ -531,7 +547,7 @@ public async Task GetDeploymentDetails(string sessionId) var displayedResources = await displayedResourcesHandler.GetDeploymentOutputs(state.ApplicationDetails, state.SelectedRecommendation); var output = new GetDeploymentDetailsOutput( - state.ApplicationDetails.StackName, + state.ApplicationDetails.Name, displayedResources .Select(x => new DisplayedResourceSummary(x.Id, x.Description, x.Type, x.Data)) .ToList()); @@ -609,7 +625,9 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService()), serviceProvider.GetRequiredService(), new List { RecipeLocator.FindRecipeDefinitionsPath() }, - serviceProvider.GetRequiredService() + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() ); } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs b/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs index 7b8b6a38a..c82b8b5a3 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs @@ -29,4 +29,12 @@ public class InvalidEncryptionKeyInfoException : Exception { public InvalidEncryptionKeyInfoException(string message, Exception? innerException = null) : base(message, innerException) { } } + + /// + /// Throw if could not find deployment targets mapping. + /// + public class FailedToFindDeploymentTargetsMappingException : Exception + { + public FailedToFindDeploymentTargetsMappingException(string message, Exception? innerException = null) : base(message, innerException) { } + } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/DeploymentTypes.cs b/src/AWS.Deploy.CLI/ServerMode/Models/DeploymentTypes.cs new file mode 100644 index 000000000..c35e3bf1c --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/DeploymentTypes.cs @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public enum DeploymentTypes + { + CloudFormationStack, + BeanstalkEnvironment + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs index 2a6ab9a9c..fb2debd5d 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs @@ -5,11 +5,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AWS.Deploy.Common; namespace AWS.Deploy.CLI.ServerMode.Models { public class ExistingDeploymentSummary { + private readonly Dictionary _deploymentTargetsMapping = new() + { + { CloudApplicationResourceType.CloudFormationStack, DeploymentTypes.CloudFormationStack }, + { CloudApplicationResourceType.BeanstalkEnvironment, DeploymentTypes.BeanstalkEnvironment } + }; + public string Name { get; set; } public string RecipeId { get; set; } @@ -26,6 +33,10 @@ public class ExistingDeploymentSummary public bool UpdatedByCurrentUser { get; set; } + public DeploymentTypes DeploymentType { get; set; } + + public string ExistingDeploymentId { get; set; } + public ExistingDeploymentSummary( string name, string recipeId, @@ -34,7 +45,9 @@ public ExistingDeploymentSummary( string description, string targetService, DateTime? lastUpdatedTime, - bool updatedByCurrentUser + bool updatedByCurrentUser, + CloudApplicationResourceType resourceType, + string uniqueIdentifier ) { Name = name; @@ -45,6 +58,15 @@ bool updatedByCurrentUser TargetService = targetService; LastUpdatedTime = lastUpdatedTime; UpdatedByCurrentUser = updatedByCurrentUser; + ExistingDeploymentId = uniqueIdentifier; + + if (!_deploymentTargetsMapping.ContainsKey(resourceType)) + { + var message = $"Failed to find a deployment target mapping for {nameof(CloudApplicationResourceType)} {resourceType}."; + throw new FailedToFindDeploymentTargetsMappingException(message); + } + + DeploymentType = _deploymentTargetsMapping[resourceType]; } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs index d232e7b38..1e2e61357 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs @@ -9,18 +9,26 @@ namespace AWS.Deploy.CLI.ServerMode.Models { public class RecommendationSummary { + private readonly Dictionary _deploymentTargetsMapping = new() + { + { Common.Recipes.DeploymentTypes.CdkProject, DeploymentTypes.CloudFormationStack }, + { Common.Recipes.DeploymentTypes.BeanstalkEnvironment, DeploymentTypes.BeanstalkEnvironment } + }; + public string RecipeId { get; set; } public string Name { get; set; } public string ShortDescription { get; set; } public string Description { get; set; } public string TargetService { get; set; } + public DeploymentTypes DeploymentType { get; set; } public RecommendationSummary( string recipeId, string name, string shortDescription, string description, - string targetService + string targetService, + Common.Recipes.DeploymentTypes deploymentType ) { RecipeId = recipeId; @@ -28,6 +36,14 @@ string targetService ShortDescription = shortDescription; Description = description; TargetService = targetService; + + if (!_deploymentTargetsMapping.ContainsKey(deploymentType)) + { + var message = $"Failed to find a deployment target mapping for {nameof(Common.Recipes.DeploymentTypes)} {deploymentType}."; + throw new FailedToFindDeploymentTargetsMappingException(message); + } + + DeploymentType = _deploymentTargetsMapping[deploymentType]; } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/SetDeploymentTargetInput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/SetDeploymentTargetInput.cs index 1b24d0cf4..13c165fbd 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/SetDeploymentTargetInput.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/SetDeploymentTargetInput.cs @@ -13,6 +13,6 @@ public class SetDeploymentTargetInput public string? NewDeploymentRecipeId { get; set; } - public string? ExistingDeploymentName { get; set; } + public string? ExistingDeploymentId { get; set; } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/SessionState.cs b/src/AWS.Deploy.CLI/ServerMode/SessionState.cs index c7296f9bf..fb3dcb2b1 100644 --- a/src/AWS.Deploy.CLI/ServerMode/SessionState.cs +++ b/src/AWS.Deploy.CLI/ServerMode/SessionState.cs @@ -28,7 +28,7 @@ public class SessionState public Recommendation? SelectedRecommendation { get; set; } - public CloudApplication ApplicationDetails { get; } = new CloudApplication(string.Empty, string.Empty); + public CloudApplication ApplicationDetails { get; } = new CloudApplication(string.Empty, string.Empty, CloudApplicationResourceType.None, string.Empty); public Task? DeploymentTask { get; set; } diff --git a/src/AWS.Deploy.Common/CloudApplication.cs b/src/AWS.Deploy.Common/CloudApplication.cs index b012ca609..9fb553d08 100644 --- a/src/AWS.Deploy.Common/CloudApplication.cs +++ b/src/AWS.Deploy.Common/CloudApplication.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; namespace AWS.Deploy.Common { @@ -10,25 +11,37 @@ namespace AWS.Deploy.Common /// public class CloudApplication { + private readonly Dictionary _resourceTypeMapping = + new() + { + { CloudApplicationResourceType.CloudFormationStack, "CloudFormation Stack" }, + { CloudApplicationResourceType.BeanstalkEnvironment, "Elastic Beanstalk Environment" } + }; + /// - /// Name of the CloudApplication - /// used to create CloudFormation stack + /// Name of the CloudApplication resource /// public string Name { get; set; } /// - /// Name of CloudFormation stack + /// The unique Id to identify the CloudApplication. + /// The ID is set to the StackId if the CloudApplication is an existing Cloudformation stack. + /// The ID is set to the EnvironmentId if the CloudApplication is an existing Elastic Beanstalk environment. + /// The ID is set to string.Empty for new CloudApplications. /// - /// - /// and are two different properties and just happens to be same value at this moment. - /// - public string StackName => Name; + public string UniqueIdentifier { get; set; } /// - /// The id of the AWS .NET deployment tool recipe used to create the cloud application. + /// The id of the AWS .NET deployment tool recipe used to create or re-deploy the cloud application. /// public string RecipeId { get; set; } + /// + /// indicates the type of the AWS resource which serves as the deployment target. + /// Current supported values are None, CloudFormationStack and BeanstalkEnvironment. + /// + public CloudApplicationResourceType ResourceType { get; set; } + /// /// Last updated time of CloudFormation stack /// @@ -39,15 +52,21 @@ public class CloudApplication /// public bool UpdatedByCurrentUser { get; set; } + /// + /// This name is shown to the user when the CloudApplication is presented as an existing re-deployment target. + /// + public string DisplayName => $"{Name} ({_resourceTypeMapping[ResourceType]})"; + /// /// Display the name of the Cloud Application /// - /// public override string ToString() => Name; - public CloudApplication(string name, string recipeId, DateTime? lastUpdatedTime = null) + public CloudApplication(string name, string uniqueIdentifier, CloudApplicationResourceType resourceType, string recipeId, DateTime? lastUpdatedTime = null) { Name = name; + UniqueIdentifier = uniqueIdentifier; + ResourceType = resourceType; RecipeId = recipeId; LastUpdatedTime = lastUpdatedTime; } diff --git a/src/AWS.Deploy.Common/CloudApplicationResourceType.cs b/src/AWS.Deploy.Common/CloudApplicationResourceType.cs new file mode 100644 index 000000000..69192f095 --- /dev/null +++ b/src/AWS.Deploy.Common/CloudApplicationResourceType.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Common +{ + public enum CloudApplicationResourceType + { + None, + CloudFormationStack, + BeanstalkEnvironment + } +} diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 68bb60e8f..ab6e04ed7 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -23,7 +23,7 @@ public enum DeployToolErrorCode ProjectPathNotFound = 10000100, ProjectParserNoSdkAttribute = 10000200, InvalidCliArguments = 10000300, - SilentArgumentNeedsStackNameArgument = 10000400, + SilentArgumentNeedsApplicationNameArgument = 10000400, SilentArgumentNeedsDeploymentRecipe = 10000500, DeploymentProjectPathNotFound = 10000600, RuleHasInvalidTestType = 10000700, @@ -92,7 +92,13 @@ public enum DeployToolErrorCode FailedToFindDeploymentProjectRecipeId = 10007000, UnexpectedError = 10007100, FailedToCreateCdkStack = 10007200, - FailedToFindElasticBeanstalkSolutionStack = 10007300 + FailedToFindElasticBeanstalkSolutionStack = 10007300, + FailedToCreateDeploymentCommandInstance = 10007400, + FailedToFindElasticBeanstalkApplication = 10007500, + FailedS3Upload = 10007600, + FailedToCreateElasticBeanstalkApplicationVersion = 10007700, + FailedToUpdateElasticBeanstalkEnvironment = 10007800, + FailedToCreateElasticBeanstalkStorageLocation = 10007900 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Common/IO/FileManager.cs b/src/AWS.Deploy.Common/IO/FileManager.cs index 5bfd88486..de4b9d0e0 100644 --- a/src/AWS.Deploy.Common/IO/FileManager.cs +++ b/src/AWS.Deploy.Common/IO/FileManager.cs @@ -15,6 +15,9 @@ public interface IFileManager Task ReadAllTextAsync(string path); Task ReadAllLinesAsync(string path); Task WriteAllTextAsync(string filePath, string contents, CancellationToken cancellationToken = default); + FileStream OpenRead(string filePath); + string GetExtension(string filePath); + long GetSizeInBytes(string filePath); } /// @@ -31,6 +34,12 @@ public class FileManager : IFileManager public Task WriteAllTextAsync(string filePath, string contents, CancellationToken cancellationToken) => File.WriteAllTextAsync(filePath, contents, cancellationToken); + public FileStream OpenRead(string filePath) => File.OpenRead(filePath); + + public string GetExtension(string filePath) => Path.GetExtension(filePath); + + public long GetSizeInBytes(string filePath) => new FileInfo(filePath).Length; + private bool IsFileValid(string filePath) { if (!PathUtilities.IsPathValid(filePath)) diff --git a/src/AWS.Deploy.Common/Recipes/DeploymentTypes.cs b/src/AWS.Deploy.Common/Recipes/DeploymentTypes.cs index 85b0e65fa..d517636fc 100644 --- a/src/AWS.Deploy.Common/Recipes/DeploymentTypes.cs +++ b/src/AWS.Deploy.Common/Recipes/DeploymentTypes.cs @@ -1,10 +1,11 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 namespace AWS.Deploy.Common.Recipes { public enum DeploymentTypes { - CdkProject + CdkProject, + BeanstalkEnvironment } } diff --git a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs index aead8a80a..4c4babd81 100644 --- a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs +++ b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs @@ -28,6 +28,11 @@ public class RecipeDefinition /// public string Name { get; set; } + /// + /// Indicates if this recipe should be presented as an option during new deployments. + /// + public bool DisableNewDeployments { get; set; } + /// /// Description of the recipe informing the user what this recipe does and why it is recommended. /// diff --git a/src/AWS.Deploy.Common/UserDeploymentSettings.cs b/src/AWS.Deploy.Common/UserDeploymentSettings.cs index 6b94aa81c..1390e740b 100644 --- a/src/AWS.Deploy.Common/UserDeploymentSettings.cs +++ b/src/AWS.Deploy.Common/UserDeploymentSettings.cs @@ -19,7 +19,7 @@ public class UserDeploymentSettings public string? AWSRegion { get; set; } - public string? StackName { get; set; } + public string? ApplicationName { get; set; } public string? RecipeId { get; set; } diff --git a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems index 8ad3508f7..6c0b2a873 100644 --- a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems +++ b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems @@ -12,5 +12,7 @@ + + \ No newline at end of file diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index a93832010..43b5be765 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -9,9 +9,9 @@ internal static class CLI public const string CREATE_NEW_LABEL = "*** Create new ***"; public const string DEFAULT_LABEL = "*** Default ***"; public const string EMPTY_LABEL = "*** Empty ***"; - public const string CREATE_NEW_STACK_LABEL = "*** Deploy to a new stack ***"; - public const string PROMPT_NEW_STACK_NAME = "Enter the name of the new stack"; - public const string PROMPT_CHOOSE_STACK_NAME = "Choose stack to deploy to"; + public const string CREATE_NEW_STACK_LABEL = "*** Deploy to a new CloudFormation stack ***"; + public const string PROMPT_NEW_STACK_NAME = "Enter the name of the new CloudFormationStack stack"; + public const string PROMPT_CHOOSE_DEPLOYMENT_TARGET = "Choose deployment target"; public const string CLI_APP_NAME = "AWS .NET Deployment Tool"; diff --git a/src/AWS.Deploy.Constants/ElasticBeanstalk.cs b/src/AWS.Deploy.Constants/ElasticBeanstalk.cs new file mode 100644 index 000000000..b0db84c39 --- /dev/null +++ b/src/AWS.Deploy.Constants/ElasticBeanstalk.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Constants +{ + internal static class ElasticBeanstalk + { + public const string EnhancedHealthReportingOptionId = "EnhancedHealthReporting"; + public const string EnhancedHealthReportingOptionNameSpace = "aws:elasticbeanstalk:healthreporting:system"; + public const string EnhancedHealthReportingOptionName = "SystemType"; + + public const string XRayTracingOptionId = "XRayTracingSupportEnabled"; + public const string XRayTracingOptionNameSpace = "aws:elasticbeanstalk:xray"; + public const string XRayTracingOptionName = "XRayEnabled"; + + public const string ProxyOptionId = "ReverseProxy"; + public const string ProxyOptionNameSpace = "aws:elasticbeanstalk:environment:proxy"; + public const string ProxyOptionName = "ProxyServer"; + + public const string HealthCheckURLOptionId = "HealthCheckURL"; + public const string HealthCheckURLOptionNameSpace = "aws:elasticbeanstalk:application"; + public const string HealthCheckURLOptionName = "Application Healthcheck URL"; + + /// + /// This list stores a named tuple of OptionSettingId, OptionSettingNameSpace and OptionSettingName. + /// OptionSettingId refers to the Id property for an option setting item in the recipe file. + /// OptionSettingNameSpace and OptionSettingName provide a way to configure the environments metadata and update its behaviour. + /// A comprehensive list of all configurable settings can be found here + /// + public static List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> OptionSettingQueryList = new() + { + new (EnhancedHealthReportingOptionId, EnhancedHealthReportingOptionNameSpace, EnhancedHealthReportingOptionName), + new (XRayTracingOptionId, XRayTracingOptionNameSpace, XRayTracingOptionName), + new (ProxyOptionId, ProxyOptionNameSpace, ProxyOptionName), + new (HealthCheckURLOptionId, HealthCheckURLOptionNameSpace, HealthCheckURLOptionName) + }; + } +} diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs new file mode 100644 index 000000000..575269471 --- /dev/null +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Constants +{ + internal static class RecipeIdentifier + { + public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; + public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; + public const string EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment"; + } +} diff --git a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs index f9e55467b..9cf06b1d2 100644 --- a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs +++ b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs @@ -19,7 +19,7 @@ public string Build(CloudApplication cloudApplication, Recommendation recommenda // General Settings var appSettingsContainer = new RecipeProps>( - cloudApplication.StackName, + cloudApplication.Name, projectPath, recommendation.Recipe.Id, recommendation.Recipe.Version, diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 643f11567..a73cfdfcd 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -105,7 +105,7 @@ private async Task CheckCdkDeploymentFailure(CloudApplication cloudApplication, { try { - var stackEvents = await _awsResourceQueryer.GetCloudFormationStackEvents(cloudApplication.StackName); + var stackEvents = await _awsResourceQueryer.GetCloudFormationStackEvents(cloudApplication.Name); var failedEvents = stackEvents .Where(x => x.Timestamp >= deploymentStartDate) @@ -119,7 +119,7 @@ private async Task CheckCdkDeploymentFailure(CloudApplication cloudApplication, throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, errors); } } - catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack [{cloudApplication.StackName}] does not exist")) + catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack [{cloudApplication.Name}] does not exist")) { throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToCreateCdkStack, "A CloudFormation stack was not created. Check the deployment output for more details."); } diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index ffeeecb04..3f2a1a54b 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -43,7 +43,7 @@ public interface IAWSResourceQueryer Task> ListOfAvailableInstanceTypes(); Task DescribeAppRunnerService(string serviceArn); Task> DescribeCloudFormationResources(string stackName); - Task DescribeElasticBeanstalkEnvironment(string environmentId); + Task DescribeElasticBeanstalkEnvironment(string environmentName); Task DescribeElasticLoadBalancer(string loadBalancerArn); Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn); Task DescribeCloudWatchRule(string ruleName); @@ -51,7 +51,8 @@ public interface IAWSResourceQueryer Task GetS3BucketWebSiteConfiguration(string bucketName); Task> ListOfECSClusters(); Task> ListOfElasticBeanstalkApplications(); - Task> ListOfElasticBeanstalkEnvironments(string? applicationName); + Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null); + Task> ListElasticBeanstalkResourceTags(string resourceArn); Task> ListOfEC2KeyPairs(); Task CreateEC2KeyPair(string keyName, string saveLocation); Task> ListOfIAMRoles(string? servicePrincipal); @@ -69,6 +70,7 @@ public interface IAWSResourceQueryer Task> ListOfSQSQueuesUrls(); Task> ListOfSNSTopicArns(); Task> ListOfS3Buckets(); + Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName); } public class AWSResourceQueryer : IAWSResourceQueryer @@ -135,17 +137,17 @@ public async Task> DescribeCloudFormationResources(string st return resources.StackResources; } - public async Task DescribeElasticBeanstalkEnvironment(string environmentId) + public async Task DescribeElasticBeanstalkEnvironment(string environmentName) { var beanstalkClient = _awsClientFactory.GetAWSClient(); var environment = await beanstalkClient.DescribeEnvironmentsAsync(new DescribeEnvironmentsRequest { - EnvironmentNames = new List { environmentId } + EnvironmentNames = new List { environmentName } }); if (!environment.Environments.Any()) { - throw new AWSResourceNotFoundException(DeployToolErrorCode.BeanstalkEnvironmentDoesNotExist, $"The elastic beanstalk environment '{environmentId}' does not exist."); + throw new AWSResourceNotFoundException(DeployToolErrorCode.BeanstalkEnvironmentDoesNotExist, $"The elastic beanstalk environment '{environmentName}' does not exist."); } return environment.Environments.First(); @@ -257,14 +259,11 @@ public async Task> ListOfElasticBeanstalkApplicatio return applications.Applications; } - public async Task> ListOfElasticBeanstalkEnvironments(string? applicationName) + public async Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null) { var beanstalkClient = _awsClientFactory.GetAWSClient(); var environments = new List(); - if (string.IsNullOrEmpty(applicationName)) - return environments; - var request = new DescribeEnvironmentsRequest { ApplicationName = applicationName @@ -282,6 +281,17 @@ public async Task> ListOfElasticBeanstalkEnvironmen return environments; } + public async Task> ListElasticBeanstalkResourceTags(string resourceArn) + { + var beanstalkClient = _awsClientFactory.GetAWSClient(); + var response = await beanstalkClient.ListTagsForResourceAsync(new Amazon.ElasticBeanstalk.Model.ListTagsForResourceRequest + { + ResourceArn = resourceArn + }); + + return response.ResourceTags; + } + public async Task> ListOfEC2KeyPairs() { var ec2Client = _awsClientFactory.GetAWSClient(); @@ -525,5 +535,27 @@ public async Task> ListOfSNSTopicArns() return buckets; } + + public async Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName) + { + var optionSetting = new List(); + var environmentDescription = await DescribeElasticBeanstalkEnvironment(environmentName); + var client = _awsClientFactory.GetAWSClient(); + var response = await client.DescribeConfigurationSettingsAsync(new DescribeConfigurationSettingsRequest + { + ApplicationName = environmentDescription.ApplicationName, + EnvironmentName = environmentName + }); + + foreach (var settingDescription in response.ConfigurationSettings) + { + foreach (var setting in settingDescription.OptionSettings) + { + optionSetting.Add(setting); + } + } + + return optionSetting; + } } } diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 18e3f88b0..f27eec6ae 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -51,7 +51,7 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Re var dockerExecutionDirectory = GetDockerExecutionDirectory(recommendation); var tagSuffix = DateTime.UtcNow.Ticks; - var imageTag = $"{cloudApplication.StackName.ToLower()}:{tagSuffix}"; + var imageTag = $"{cloudApplication.Name.ToLower()}:{tagSuffix}"; var dockerFile = GetDockerFilePath(recommendation); var buildArgs = GetDockerBuildArgs(recommendation); @@ -79,7 +79,7 @@ public async Task PushDockerImageToECR(CloudApplication cloudApplication, Recomm await InitiateDockerLogin(); var tagSuffix = sourceTag.Split(":")[1]; - var repository = await SetupECRRepository(cloudApplication.StackName.ToLower()); + var repository = await SetupECRRepository(cloudApplication.Name.ToLower()); var targetTag = $"{repository.RepositoryUri}:{tagSuffix}"; await TagDockerImage(sourceTag, targetTag); diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs new file mode 100644 index 000000000..540367c2e --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.DisplayedResources; + +namespace AWS.Deploy.Orchestration.DeploymentCommands +{ + public class BeanstalkEnvironmentDeploymentCommand : IDeploymentCommand + { + public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation recommendation) + { + if (orchestrator._interactiveService == null) + throw new InvalidOperationException($"{nameof(orchestrator._interactiveService)} is null as part of the orchestartor object"); + if (orchestrator._awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(orchestrator._awsResourceQueryer)} is null as part of the orchestartor object"); + if (orchestrator._awsServiceHandler == null) + throw new InvalidOperationException($"{nameof(orchestrator._awsServiceHandler)} is null as part of the orchestartor object"); + + var deploymentPackage = recommendation.DeploymentBundle.DotnetPublishZipPath; + var environmentName = cloudApplication.Name; + var applicationName = (await orchestrator._awsResourceQueryer.ListOfElasticBeanstalkEnvironments()) + .Where(x => string.Equals(x.EnvironmentId, cloudApplication.UniqueIdentifier)) + .FirstOrDefault()? + .ApplicationName; + + var s3Handler = orchestrator._awsServiceHandler.S3Handler; + var elasticBeanstalkHandler = orchestrator._awsServiceHandler.ElasticBeanstalkHandler; + + if (string.IsNullOrEmpty(applicationName)) + { + var message = $"Could not find any Elastic Beanstalk application that contains the following environment: {environmentName}"; + throw new AWSResourceNotFoundException(DeployToolErrorCode.FailedToFindElasticBeanstalkApplication, message); + } + + orchestrator._interactiveService.LogMessageLine($"Initiating deployment to {cloudApplication.DisplayName}..."); + + var versionLabel = $"v-{DateTime.Now.Ticks}"; + var s3location = await elasticBeanstalkHandler.CreateApplicationStorageLocationAsync(applicationName, versionLabel, deploymentPackage); + await s3Handler.UploadToS3Async(s3location.S3Bucket, s3location.S3Key, deploymentPackage); + await elasticBeanstalkHandler.CreateApplicationVersionAsync(applicationName, versionLabel, s3location); + var environmentConfigurationSettings = elasticBeanstalkHandler.GetEnvironmentConfigurationSettings(recommendation); + + var success = await elasticBeanstalkHandler.UpdateEnvironmentAsync(applicationName, environmentName, versionLabel, environmentConfigurationSettings); + + if (success) + { + orchestrator._interactiveService.LogMessageLine($"The Elastic Beanstalk Environment {environmentName} has been successfully updated to the application version {versionLabel}" + Environment.NewLine); + } + else + { + throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToUpdateElasticBeanstalkEnvironment, "Failed to update the Elastic Beanstalk environment"); + } + } + public async Task> GetDeploymentOutputsAsync(IDisplayedResourcesHandler displayedResourcesHandler, CloudApplication cloudApplication, Recommendation recommendation) + { + var displayedResources = new List(); + var environment = await displayedResourcesHandler.AwsResourceQueryer.DescribeElasticBeanstalkEnvironment(cloudApplication.Name); + var data = new Dictionary() {{ "Endpoint", $"http://{environment.CNAME}/" }}; + var resourceDescription = "An AWS Elastic Beanstalk environment is a collection of AWS resources running an application version."; + var resourceType = "Elastic Beanstalk Environment"; + displayedResources.Add(new DisplayedResourceItem(environment.EnvironmentName, resourceDescription, resourceType, data)); + return displayedResources; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs new file mode 100644 index 000000000..41291aba1 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Orchestration.DisplayedResources; + +namespace AWS.Deploy.Orchestration.DeploymentCommands +{ + public class CdkDeploymentCommand : IDeploymentCommand + { + public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation recommendation) + { + if (orchestrator._interactiveService == null) + throw new InvalidOperationException($"{nameof(orchestrator._interactiveService)} is null as part of the orchestartor object"); + if (orchestrator._cdkManager == null) + throw new InvalidOperationException($"{nameof(orchestrator._cdkManager)} is null as part of the orchestartor object"); + if (orchestrator._cdkProjectHandler == null) + throw new InvalidOperationException($"{nameof(CdkProjectHandler)} is null as part of the orchestartor object"); + if (orchestrator._localUserSettingsEngine == null) + throw new InvalidOperationException($"{nameof(orchestrator._localUserSettingsEngine)} is null as part of the orchestartor object"); + if (orchestrator._session == null) + throw new InvalidOperationException($"{nameof(orchestrator._session)} is null as part of the orchestartor object"); + if (orchestrator._cdkVersionDetector == null) + throw new InvalidOperationException($"{nameof(orchestrator._cdkVersionDetector)} must not be null."); + if (orchestrator._directoryManager == null) + throw new InvalidOperationException($"{nameof(orchestrator._directoryManager)} must not be null."); + + orchestrator._interactiveService.LogMessageLine(string.Empty); + orchestrator._interactiveService.LogMessageLine($"Initiating deployment: {recommendation.Name}"); + + orchestrator._interactiveService.LogMessageLine("Configuring AWS Cloud Development Kit (CDK)..."); + var cdkProject = await orchestrator._cdkProjectHandler.ConfigureCdkProject(orchestrator._session, cloudApplication, recommendation); + + var projFiles = orchestrator._directoryManager.GetProjFiles(cdkProject); + var cdkVersion = orchestrator._cdkVersionDetector.Detect(projFiles); + + await orchestrator._cdkManager.EnsureCompatibleCDKExists(Constants.CDK.DeployToolWorkspaceDirectoryRoot, cdkVersion); + + try + { + await orchestrator._cdkProjectHandler.DeployCdkProject(orchestrator._session, cloudApplication, cdkProject, recommendation); + } + finally + { + orchestrator._cdkProjectHandler.DeleteTemporaryCdkProject(orchestrator._session, cdkProject); + } + + await orchestrator._localUserSettingsEngine.UpdateLastDeployedStack(cloudApplication.Name, orchestrator._session.ProjectDefinition.ProjectName, orchestrator._session.AWSAccountId, orchestrator._session.AWSRegion); + } + + public async Task> GetDeploymentOutputsAsync(IDisplayedResourcesHandler displayedResourcesHandler, CloudApplication cloudApplication, Recommendation recommendation) + { + var displayedResources = new List(); + + if (recommendation.Recipe.DisplayedResources == null) + return displayedResources; + + var resources = await displayedResourcesHandler.AwsResourceQueryer.DescribeCloudFormationResources(cloudApplication.Name); + foreach (var displayedResource in recommendation.Recipe.DisplayedResources) + { + var resource = resources.FirstOrDefault(x => x.LogicalResourceId.Equals(displayedResource.LogicalId)); + if (resource == null) + continue; + + var data = new Dictionary(); + if (!string.IsNullOrEmpty(resource.ResourceType) && displayedResourcesHandler.DisplayedResourcesFactory.GetResource(resource.ResourceType) is var displayedResourceCommand && displayedResourceCommand != null) + { + data = await displayedResourceCommand.Execute(resource.PhysicalResourceId); + } + displayedResources.Add(new DisplayedResourceItem(resource.PhysicalResourceId, displayedResource.Description, resource.ResourceType, data)); + } + + return displayedResources; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentCommandFactory.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentCommandFactory.cs new file mode 100644 index 000000000..dc28b8262 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentCommandFactory.cs @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; + +namespace AWS.Deploy.Orchestration.DeploymentCommands +{ + public static class DeploymentCommandFactory + { + private static readonly Dictionary _deploymentCommandTypeMapping = new() + { + { DeploymentTypes.CdkProject, typeof(CdkDeploymentCommand) }, + { DeploymentTypes.BeanstalkEnvironment, typeof(BeanstalkEnvironmentDeploymentCommand) } + }; + + public static IDeploymentCommand BuildDeploymentCommand(DeploymentTypes deploymentType) + { + if (!_deploymentCommandTypeMapping.ContainsKey(deploymentType)) + { + var message = $"Failed to create an instance of type {nameof(IDeploymentCommand)}. {deploymentType} does not exist as a key in {_deploymentCommandTypeMapping}."; + throw new FailedToCreateDeploymentCommandInstanceException(DeployToolErrorCode.FailedToCreateDeploymentCommandInstance, message); + } + + var deploymentCommandInstance = Activator.CreateInstance(_deploymentCommandTypeMapping[deploymentType]); + if (deploymentCommandInstance == null || deploymentCommandInstance is not IDeploymentCommand) + { + var message = $"Failed to create an instance of type {_deploymentCommandTypeMapping[deploymentType]}."; + throw new FailedToCreateDeploymentCommandInstanceException(DeployToolErrorCode.FailedToCreateDeploymentCommandInstance, message); + } + + return (IDeploymentCommand)deploymentCommandInstance; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentStatus.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentStatus.cs new file mode 100644 index 000000000..fe7218188 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/DeploymentStatus.cs @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Orchestration.DeploymentCommands +{ + public enum DeploymentStatus { NotStarted = 1, Executing = 2, Error = 3, Success = 4 } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/IDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/IDeploymentCommand.cs new file mode 100644 index 000000000..0cc4d730d --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/IDeploymentCommand.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.DisplayedResources; + +namespace AWS.Deploy.Orchestration.DeploymentCommands +{ + public interface IDeploymentCommand + { + Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation recommendation); + Task> GetDeploymentOutputsAsync(IDisplayedResourcesHandler displayedResourcesHandler, CloudApplication cloudApplication, Recommendation recommendation); + } +} diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs index 67060d6ef..32ee133eb 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs @@ -6,23 +6,26 @@ using System.Threading.Tasks; using AWS.Deploy.Orchestration.Data; using System.Linq; +using AWS.Deploy.Orchestration.DeploymentCommands; namespace AWS.Deploy.Orchestration.DisplayedResources { public interface IDisplayedResourcesHandler { + IAWSResourceQueryer AwsResourceQueryer { get; } + IDisplayedResourceCommandFactory DisplayedResourcesFactory { get; } Task> GetDeploymentOutputs(CloudApplication cloudApplication, Recommendation recommendation); } public class DisplayedResourcesHandler : IDisplayedResourcesHandler { - private readonly IAWSResourceQueryer _awsResourceQueryer; - private readonly IDisplayedResourceCommandFactory _displayedResourcesFactory; + public IAWSResourceQueryer AwsResourceQueryer { get; } + public IDisplayedResourceCommandFactory DisplayedResourcesFactory { get; } public DisplayedResourcesHandler(IAWSResourceQueryer awsResourceQueryer, IDisplayedResourceCommandFactory displayedResourcesFactory) { - _awsResourceQueryer = awsResourceQueryer; - _displayedResourcesFactory = displayedResourcesFactory; + AwsResourceQueryer = awsResourceQueryer; + DisplayedResourcesFactory = displayedResourcesFactory; } /// @@ -31,27 +34,8 @@ public DisplayedResourcesHandler(IAWSResourceQueryer awsResourceQueryer, IDispla /// public async Task> GetDeploymentOutputs(CloudApplication cloudApplication, Recommendation recommendation) { - var displayedResources = new List(); - - if (recommendation.Recipe.DisplayedResources == null) - return displayedResources; - - var resources = await _awsResourceQueryer.DescribeCloudFormationResources(cloudApplication.StackName); - foreach (var displayedResource in recommendation.Recipe.DisplayedResources) - { - var resource = resources.FirstOrDefault(x => x.LogicalResourceId.Equals(displayedResource.LogicalId)); - if (resource == null) - continue; - - var data = new Dictionary(); - if (!string.IsNullOrEmpty(resource.ResourceType) && _displayedResourcesFactory.GetResource(resource.ResourceType) is var displayedResourceCommand && displayedResourceCommand != null) - { - data = await displayedResourceCommand.Execute(resource.PhysicalResourceId); - } - displayedResources.Add(new DisplayedResourceItem(resource.PhysicalResourceId, displayedResource.Description, resource.ResourceType, data)); - } - - return displayedResources; + var deploymentCommand = DeploymentCommandFactory.BuildDeploymentCommand(recommendation.Recipe.DeploymentType); + return await deploymentCommand.GetDeploymentOutputsAsync(this, cloudApplication, recommendation); } } } diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 51cfcf3e6..645fb6064 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -1,6 +1,7 @@ using System; using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.DeploymentCommands; namespace AWS.Deploy.Orchestration { @@ -179,4 +180,28 @@ public class FailedToFindElasticBeanstalkSolutionStackException : DeployToolExce { public FailedToFindElasticBeanstalkSolutionStackException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if could not create an instance of + /// + public class FailedToCreateDeploymentCommandInstanceException : DeployToolException + { + public FailedToCreateDeploymentCommandInstanceException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } + + /// + /// Throw if an error occured while calling a S3 API. + /// + public class S3Exception : DeployToolException + { + public S3Exception(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } + + /// + /// Throw if an error occured while calling an Elastic Beanstalk API. + /// + public class ElasticBeanstalkException : DeployToolException + { + public ElasticBeanstalkException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index e48e733e8..d072f39a7 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -12,7 +12,10 @@ using AWS.Deploy.DockerEngine; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; +using AWS.Deploy.Orchestration.DeploymentCommands; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Orchestration.Utilities; namespace AWS.Deploy.Orchestration { @@ -23,18 +26,20 @@ namespace AWS.Deploy.Orchestration /// public class Orchestrator { - private readonly ICdkProjectHandler? _cdkProjectHandler; - private readonly ICDKManager? _cdkManager; - private readonly ICDKVersionDetector? _cdkVersionDetector; - 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 IDirectoryManager? _directoryManager; - private readonly ICustomRecipeLocator? _customRecipeLocator; - private readonly OrchestratorSession? _session; + internal readonly ICdkProjectHandler? _cdkProjectHandler; + internal readonly ICDKManager? _cdkManager; + internal readonly ICDKVersionDetector? _cdkVersionDetector; + internal readonly IOrchestratorInteractiveService? _interactiveService; + internal readonly IAWSResourceQueryer? _awsResourceQueryer; + internal readonly IDeploymentBundleHandler? _deploymentBundleHandler; + internal readonly ILocalUserSettingsEngine? _localUserSettingsEngine; + internal readonly IDockerEngine? _dockerEngine; + internal readonly IList? _recipeDefinitionPaths; + internal readonly IFileManager? _fileManager; + internal readonly IDirectoryManager? _directoryManager; + internal readonly ICustomRecipeLocator? _customRecipeLocator; + internal readonly OrchestratorSession? _session; + internal readonly IAWSServiceHandler? _awsServiceHandler; public Orchestrator( OrchestratorSession session, @@ -48,7 +53,9 @@ public Orchestrator( IDockerEngine dockerEngine, ICustomRecipeLocator customRecipeLocator, IList recipeDefinitionPaths, - IDirectoryManager directoryManager) + IFileManager fileManager, + IDirectoryManager directoryManager, + IAWSServiceHandler awsServiceHandler) { _session = session; _interactiveService = interactiveService; @@ -61,7 +68,9 @@ public Orchestrator( _customRecipeLocator = customRecipeLocator; _recipeDefinitionPaths = recipeDefinitionPaths; _localUserSettingsEngine = localUserSettingsEngine; + _fileManager = fileManager; _directoryManager = directoryManager; + _awsServiceHandler = awsServiceHandler; } public Orchestrator(OrchestratorSession session, IList recipeDefinitionPaths) @@ -129,56 +138,8 @@ public async Task ApplyAllReplacementTokens(Recommendation recommendation, strin public async Task DeployRecommendation(CloudApplication cloudApplication, Recommendation recommendation) { - if (_interactiveService == null) - throw new InvalidOperationException($"{nameof(_interactiveService)} is null as part of the orchestartor object"); - if (_cdkManager == null) - 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"); - - _interactiveService.LogMessageLine(string.Empty); - _interactiveService.LogMessageLine($"Initiating deployment: {recommendation.Name}"); - - switch (recommendation.Recipe.DeploymentType) - { - case DeploymentTypes.CdkProject: - if (_cdkVersionDetector == null) - { - throw new InvalidOperationException($"{nameof(_cdkVersionDetector)} must not be null."); - } - - if (_directoryManager == null) - { - throw new InvalidOperationException($"{nameof(_directoryManager)} must not be null."); - } - - _interactiveService.LogMessageLine("Configuring AWS Cloud Development Kit (CDK)..."); - var cdkProject = await _cdkProjectHandler.ConfigureCdkProject(_session, cloudApplication, recommendation); - - var projFiles = _directoryManager.GetProjFiles(cdkProject); - var cdkVersion = _cdkVersionDetector.Detect(projFiles); - - await _cdkManager.EnsureCompatibleCDKExists(Constants.CDK.DeployToolWorkspaceDirectoryRoot, cdkVersion); - - try - { - await _cdkProjectHandler.DeployCdkProject(_session, cloudApplication, cdkProject, recommendation); - } - finally - { - _cdkProjectHandler.DeleteTemporaryCdkProject(_session, cdkProject); - } - break; - default: - _interactiveService.LogErrorMessageLine($"Unknown deployment type {recommendation.Recipe.DeploymentType} specified in recipe."); - return; - } - - await _localUserSettingsEngine.UpdateLastDeployedStack(cloudApplication.StackName, _session.ProjectDefinition.ProjectName, _session.AWSAccountId, _session.AWSRegion); + var deploymentCommand = DeploymentCommandFactory.BuildDeploymentCommand(recommendation.Recipe.DeploymentType); + await deploymentCommand.ExecuteAsync(this, cloudApplication, recommendation); } public async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) diff --git a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs new file mode 100644 index 000000000..a5adc08bd --- /dev/null +++ b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs @@ -0,0 +1,201 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon.ElasticBeanstalk; +using Amazon.ElasticBeanstalk.Model; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Orchestration.ServiceHandlers +{ + public interface IElasticBeanstalkHandler + { + Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage); + Task CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle); + Task UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List optionSettings); + List GetEnvironmentConfigurationSettings(Recommendation recommendation); + } + + public class AWSElasticBeanstalkHandler : IElasticBeanstalkHandler + { + private readonly IAWSClientFactory _awsClientFactory; + private readonly IOrchestratorInteractiveService _interactiveService; + private readonly IFileManager _fileManager; + + public AWSElasticBeanstalkHandler(IAWSClientFactory awsClientFactory, IOrchestratorInteractiveService interactiveService, IFileManager fileManager) + { + _awsClientFactory = awsClientFactory; + _interactiveService = interactiveService; + _fileManager = fileManager; + } + + public async Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage) + { + string bucketName; + try + { + var ebClient = _awsClientFactory.GetAWSClient(); + bucketName = (await ebClient.CreateStorageLocationAsync()).S3Bucket; + } + catch (Exception e) + { + throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToCreateElasticBeanstalkStorageLocation, "An error occured while creating the Elastic Beanstalk storage location", e); + } + + var key = string.Format("{0}/AWSDeploymentArchive_{0}_{1}{2}", + applicationName.Replace(' ', '-'), + versionLabel.Replace(' ', '-'), + _fileManager.GetExtension(deploymentPackage)); + + return new S3Location { S3Bucket = bucketName, S3Key = key }; + } + + public async Task CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle) + { + _interactiveService.LogMessageLine("Creating new application version: " + versionLabel); + + try + { + var ebClient = _awsClientFactory.GetAWSClient(); + var response = await ebClient.CreateApplicationVersionAsync(new CreateApplicationVersionRequest + { + ApplicationName = applicationName, + VersionLabel = versionLabel, + SourceBundle = sourceBundle + }); + return response; + } + catch (Exception e) + { + throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToCreateElasticBeanstalkApplicationVersion, "An error occured while creating the Elastic Beanstalk application version", e); + } + } + + public List GetEnvironmentConfigurationSettings(Recommendation recommendation) + { + var additionalSettings = new List(); + + foreach (var tuple in Constants.ElasticBeanstalk.OptionSettingQueryList) + { + var optionSetting = recommendation.GetOptionSetting(tuple.OptionSettingId); + + if (!optionSetting.Updatable) + continue; + + var optionSettingValue = optionSetting.GetValue(new Dictionary()); + + additionalSettings.Add(new ConfigurationOptionSetting + { + Namespace = tuple.OptionSettingNameSpace, + OptionName = tuple.OptionSettingName, + Value = optionSettingValue + }); + } + + return additionalSettings; + } + + public async Task UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List optionSettings) + { + _interactiveService.LogMessageLine("Getting latest environment event date before update"); + + var startingEventDate = await GetLatestEventDateAsync(applicationName, environmentName); + + _interactiveService.LogMessageLine($"Updating environment {environmentName} to new application version {versionLabel}"); + + var updateRequest = new UpdateEnvironmentRequest + { + ApplicationName = applicationName, + EnvironmentName = environmentName, + VersionLabel = versionLabel, + OptionSettings = optionSettings + }; + + try + { + var ebClient = _awsClientFactory.GetAWSClient(); + var updateEnvironmentResponse = await ebClient.UpdateEnvironmentAsync(updateRequest); + return await WaitForEnvironmentUpdateCompletion(applicationName, environmentName, startingEventDate); + } + catch (Exception e) + { + throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToUpdateElasticBeanstalkEnvironment, "An error occured while updating the Elastic Beanstalk environment", e); + } + } + + private async Task GetLatestEventDateAsync(string applicationName, string environmentName) + { + var request = new DescribeEventsRequest + { + ApplicationName = applicationName, + EnvironmentName = environmentName + }; + + var ebClient = _awsClientFactory.GetAWSClient(); + var response = await ebClient.DescribeEventsAsync(request); + if (response.Events.Count == 0) + return DateTime.Now; + + return response.Events.First().EventDate; + } + + private async Task WaitForEnvironmentUpdateCompletion(string applicationName, string environmentName, DateTime startingEventDate) + { + _interactiveService.LogMessageLine("Waiting for environment update to complete"); + + var success = true; + var environment = new EnvironmentDescription(); + var lastPrintedEventDate = startingEventDate; + var requestEvents = new DescribeEventsRequest + { + ApplicationName = applicationName, + EnvironmentName = environmentName + }; + var requestEnvironment = new DescribeEnvironmentsRequest + { + ApplicationName = applicationName, + EnvironmentNames = new List { environmentName } + }; + var ebClient = _awsClientFactory.GetAWSClient(); + + do + { + Thread.Sleep(5000); + + var responseEnvironments = await ebClient.DescribeEnvironmentsAsync(requestEnvironment); + if (responseEnvironments.Environments.Count == 0) + throw new AWSResourceNotFoundException(DeployToolErrorCode.BeanstalkEnvironmentDoesNotExist, $"Failed to find environment {environmentName} belonging to application {applicationName}"); + + environment = responseEnvironments.Environments[0]; + + requestEvents.StartTimeUtc = lastPrintedEventDate; + var responseEvents = await ebClient.DescribeEventsAsync(requestEvents); + if (responseEvents.Events.Any()) + { + for (var i = responseEvents.Events.Count - 1; i >= 0; i--) + { + var evnt = responseEvents.Events[i]; + if (evnt.EventDate <= lastPrintedEventDate) + continue; + + _interactiveService.LogMessageLine(evnt.EventDate.ToLocalTime() + " " + evnt.Severity + " " + evnt.Message); + if (evnt.Severity == EventSeverity.ERROR || evnt.Severity == EventSeverity.FATAL) + { + success = false; + } + } + + lastPrintedEventDate = responseEvents.Events[0].EventDate; + } + + } while (environment.Status == EnvironmentStatus.Launching || environment.Status == EnvironmentStatus.Updating); + + return success; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSS3Handler.cs b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSS3Handler.cs new file mode 100644 index 000000000..39dc14a41 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSS3Handler.cs @@ -0,0 +1,80 @@ +// 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.Text; +using System.Threading.Tasks; +using Amazon.S3; +using Amazon.S3.Transfer; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Orchestration.ServiceHandlers +{ + public interface IS3Handler + { + Task UploadToS3Async(string bucket, string key, string filePath); + } + + public class AWSS3Handler : IS3Handler + { + private readonly IAWSClientFactory _awsClientFactory; + private readonly IOrchestratorInteractiveService _interactiveService; + private readonly IFileManager _fileManager; + + private const int UPLOAD_PROGRESS_INCREMENT = 10; + + public AWSS3Handler(IAWSClientFactory awsClientFactory, IOrchestratorInteractiveService interactiveService, IFileManager fileManager) + { + _awsClientFactory = awsClientFactory; + _interactiveService = interactiveService; + _fileManager = fileManager; + } + + public async Task UploadToS3Async(string bucket, string key, string filePath) + { + using (var stream = _fileManager.OpenRead(filePath)) + { + _interactiveService.LogMessageLine($"Uploading to S3. (Bucket: {bucket} Key: {key} Size: {_fileManager.GetSizeInBytes(filePath)} bytes)"); + + var request = new TransferUtilityUploadRequest() + { + BucketName = bucket, + Key = key, + InputStream = stream + }; + + request.UploadProgressEvent += CreateTransferUtilityProgressHandler(); + + try + { + var s3Client = _awsClientFactory.GetAWSClient(); + await new TransferUtility(s3Client).UploadAsync(request); + } + catch (Exception e) + { + throw new S3Exception(DeployToolErrorCode.FailedS3Upload, $"Error uploading to {key} in bucket {bucket}", innerException: e); + } + } + } + + private EventHandler CreateTransferUtilityProgressHandler() + { + var percentToUpdateOn = UPLOAD_PROGRESS_INCREMENT; + EventHandler handler = ((s, e) => + { + if (e.PercentDone != percentToUpdateOn && e.PercentDone <= percentToUpdateOn) return; + + var increment = e.PercentDone % UPLOAD_PROGRESS_INCREMENT; + if (increment == 0) + increment = UPLOAD_PROGRESS_INCREMENT; + percentToUpdateOn = e.PercentDone + increment; + _interactiveService.LogMessageLine($"... Progress: {e.PercentDone}%"); + }); + + return handler; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSServiceHandler.cs b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSServiceHandler.cs new file mode 100644 index 000000000..c1fffbca3 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSServiceHandler.cs @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Orchestration.ServiceHandlers +{ + public interface IAWSServiceHandler + { + IS3Handler S3Handler { get; } + IElasticBeanstalkHandler ElasticBeanstalkHandler { get; } + } + + public class AWSServiceHandler : IAWSServiceHandler + { + public IS3Handler S3Handler { get; } + public IElasticBeanstalkHandler ElasticBeanstalkHandler { get; } + + public AWSServiceHandler(IS3Handler s3Handler, IElasticBeanstalkHandler elasticBeanstalkHandler) + { + S3Handler = s3Handler; + ElasticBeanstalkHandler = elasticBeanstalkHandler; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs index b2082b634..e8296d7ff 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs @@ -70,7 +70,7 @@ public string GenerateValidName(ProjectDefinition target, List var suffix = 1; while (suffix < 100) { - if (existingApplications.All(x => x.StackName != recommendation) && IsValidName(recommendation)) + if (existingApplications.All(x => x.Name != recommendation) && IsValidName(recommendation)) return recommendation; recommendation = $"{recommendedPrefix}{suffix++}"; diff --git a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs index 9b25bac50..c337552d1 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs @@ -6,8 +6,11 @@ using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; +using Amazon.ElasticBeanstalk; +using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Recipes.CDK.Common; @@ -17,13 +20,12 @@ namespace AWS.Deploy.Orchestration.Utilities 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. + /// Get the list of existing deployed based on the deploymentTypes filter. /// - Task> GetExistingDeployedApplications(); + Task> GetExistingDeployedApplications(List deploymentTypes); /// - /// Get the list of compatible applications based on the matching elements of the deployed stack and recommendation, such as Recipe Id. + /// Get the list of compatible applications by matching elements of the CloudApplication RecipeId and the recommendation RecipeId. /// Task> GetCompatibleApplications(List recommendations, List? allDeployedApplications = null, OrchestratorSession? session = null); @@ -31,6 +33,11 @@ public interface IDeployedApplicationQueryer /// Checks if the given recommendation can be used for a redeployment to an existing cloudformation stack. /// bool IsCompatible(CloudApplication application, Recommendation recommendation); + + /// + /// Gets the current option settings associated with the cloud application. This method is only used for non-CloudFormation based cloud applications. + /// + Task> GetPreviousSettings(CloudApplication application); } public class DeployedApplicationQueryer : IDeployedApplicationQueryer @@ -49,58 +56,28 @@ public DeployedApplicationQueryer( _orchestratorInteractiveService = orchestratorInteractiveService; } - public async Task> GetExistingDeployedApplications() + public async Task> GetExistingDeployedApplications(List deploymentTypes) { - var stacks = await _awsResourceQueryer.GetCloudFormationStacks(); - var apps = new List(); + var existingApplications = new List(); - foreach (var stack in stacks) - { - // Check to see if stack has AWS .NET deployment tool tag and the stack is not deleted or in the process of being deleted. - var deployTag = stack.Tags.FirstOrDefault(tags => string.Equals(tags.Key, Constants.CloudFormationIdentifier.STACK_TAG)); - - // Skip stacks that don't have AWS .NET deployment tool tag - if (deployTag == null || - - // Skip stacks does not have AWS .NET deployment tool description prefix. (This is filter out stacks that have the tag propagated to it like the Beanstalk stack) - (stack.Description == null || !stack.Description.StartsWith(Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX)) || + if (deploymentTypes.Contains(DeploymentTypes.CdkProject)) + existingApplications.AddRange(await GetExistingCloudFormationStacks()); - // Skip tags that are deleted or in the process of being deleted - stack.StackStatus.ToString().StartsWith("DELETE")) - { - continue; - } - - // ROLLBACK_COMPLETE occurs when a stack creation fails and successfully rollbacks with cleaning partially created resources. - // In this state, only a delete operation can be performed. (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html) - // We don't want to include ROLLBACK_COMPLETE because it never succeeded to deploy. - // However, a customer can give name of new application same as ROLLBACK_COMPLETE stack, which will trigger the re-deployment flow on the ROLLBACK_COMPLETE stack. - if (stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) - { - continue; - } + if (deploymentTypes.Contains(DeploymentTypes.BeanstalkEnvironment)) + existingApplications.AddRange(await GetExistingBeanstalkEnvironments()); - // 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; - - apps.Add(new CloudApplication(stack.StackName, recipeId, stack.LastUpdatedTime)); - } - - return apps; + return existingApplications; } /// /// 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(); + allDeployedApplications = await GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList()); foreach (var application in allDeployedApplications) { @@ -114,14 +91,14 @@ public async Task> GetCompatibleApplications(List x.StackName).ToList(), session.ProjectDefinition.ProjectName, session.AWSAccountId, session.AWSRegion); + await _localUserSettingsEngine.CleanOrphanStacks(allDeployedApplications.Select(x => x.Name).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; + x.UpdatedByCurrentUser = lastDeployedStack?.Stacks?.Contains(x.Name) ?? false; return x; }) .OrderByDescending(x => x.UpdatedByCurrentUser) @@ -158,5 +135,126 @@ public bool IsCompatible(CloudApplication application, Recommendation recommenda } return string.Equals(recommendation.Recipe.Id, application.RecipeId, StringComparison.Ordinal); } + + /// + /// Gets the current option settings associated with the cloud application.This method is only used for non-CloudFormation based cloud applications. + /// + public async Task> GetPreviousSettings(CloudApplication application) + { + IDictionary previousSettings; + switch (application.ResourceType) + { + case CloudApplicationResourceType.BeanstalkEnvironment: + previousSettings = await GetBeanstalkEnvironmentConfigurationSettings(application.Name); + break; + default: + throw new InvalidOperationException($"Cannot fetch existing option settings for the following {nameof(CloudApplicationResourceType)}: {application.ResourceType}"); + } + return previousSettings; + } + + /// + /// Fetches existing CloudFormation stacks created by the AWS .NET deployment tool + /// + /// A list of + private async Task> GetExistingCloudFormationStacks() + { + var stacks = await _awsResourceQueryer.GetCloudFormationStacks(); + var apps = new List(); + + foreach (var stack in stacks) + { + // Check to see if stack has AWS .NET deployment tool tag and the stack is not deleted or in the process of being deleted. + var deployTag = stack.Tags.FirstOrDefault(tags => string.Equals(tags.Key, Constants.CloudFormationIdentifier.STACK_TAG)); + + // Skip stacks that don't have AWS .NET deployment tool tag + if (deployTag == null || + + // Skip stacks does not have AWS .NET deployment tool description prefix. (This is filter out stacks that have the tag propagated to it like the Beanstalk stack) + (stack.Description == null || !stack.Description.StartsWith(Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX)) || + + // Skip tags that are deleted or in the process of being deleted + stack.StackStatus.ToString().StartsWith("DELETE")) + { + continue; + } + + // ROLLBACK_COMPLETE occurs when a stack creation fails and successfully rollbacks with cleaning partially created resources. + // In this state, only a delete operation can be performed. (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html) + // We don't want to include ROLLBACK_COMPLETE because it never succeeded to deploy. + // However, a customer can give name of new application same as ROLLBACK_COMPLETE stack, which will trigger the re-deployment flow on the ROLLBACK_COMPLETE stack. + if (stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + continue; + } + + // 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; + + apps.Add(new CloudApplication(stack.StackName, stack.StackId, CloudApplicationResourceType.CloudFormationStack, recipeId, stack.LastUpdatedTime)); + } + + return apps; + } + + /// + /// Fetches existing Elastic Beanstalk environments that can serve as a deployment target. + /// These environments must have a valid dotnet specific platform arn. + /// Any environment that was created via the AWS .NET deployment tool as part of a CloudFormation stack is not included. + /// + /// A list of + private async Task> GetExistingBeanstalkEnvironments() + { + var validEnvironments = new List(); + var environments = await _awsResourceQueryer.ListOfElasticBeanstalkEnvironments(); + + if (!environments.Any()) + return validEnvironments; + + var dotnetPlatformArns = (await _awsResourceQueryer.GetElasticBeanstalkPlatformArns()).Select(x => x.PlatformArn).ToList(); + + // only select environments that have a dotnet specific platform ARN. + environments = environments.Where(x => x.Status == EnvironmentStatus.Ready && dotnetPlatformArns.Contains(x.PlatformArn)).ToList(); + + foreach (var env in environments) + { + var tags = await _awsResourceQueryer.ListElasticBeanstalkResourceTags(env.EnvironmentArn); + + // skips all environments that were created via the deploy tool. + if (tags.Any(x => string.Equals(x.Key, Constants.CloudFormationIdentifier.STACK_TAG))) + continue; + + validEnvironments.Add(new CloudApplication(env.EnvironmentName, env.EnvironmentId, CloudApplicationResourceType.BeanstalkEnvironment, Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID, env.DateUpdated)); + } + + return validEnvironments; + } + + private async Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName) + { + IDictionary optionSettings = new Dictionary(); + var configurationSettings = await _awsResourceQueryer.GetBeanstalkEnvironmentConfigurationSettings(environmentName); + + foreach (var tuple in Constants.ElasticBeanstalk.OptionSettingQueryList) + { + var configurationSetting = GetBeanstalkEnvironmentConfigurationSetting(configurationSettings, tuple.OptionSettingNameSpace, tuple.OptionSettingName); + + if (string.IsNullOrEmpty(configurationSetting?.Value)) + continue; + + optionSettings[tuple.OptionSettingId] = configurationSetting.Value; + } + + return optionSettings; + } + + private ConfigurationOptionSetting GetBeanstalkEnvironmentConfigurationSetting(List configurationSettings, string optionNameSpace, string optionName) + { + var configurationSetting = configurationSettings + .FirstOrDefault(x => string.Equals(optionNameSpace, x.Namespace) && string.Equals(optionName, x.OptionName)); + + return configurationSetting; + } } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe new file mode 100644 index 000000000..5d1fa5f69 --- /dev/null +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe @@ -0,0 +1,88 @@ +{ + "$schema": "./aws-deploy-recipe-schema.json", + "Id": "AspNetAppExistingBeanstalkEnvironment", + "Version": "0.1.0", + "Name": "ASP.NET Core App to Existing AWS Elastic Beanstalk Environment", + "DisableNewDeployments": true, + "DeploymentType": "BeanstalkEnvironment", + "DeploymentBundle": "DotnetPublishZipFile", + "Description": "This ASP.NET Core application will be built and deployed to existing AWS Elastic Beanstalk environment. Recommended if you do not want to deploy your application as a container image.", + "ShortDescription": "ASP.NET Core application deployed to AWS Elastic Beanstalk on Linux.", + "TargetService": "AWS Elastic Beanstalk", + + "RecipePriority": 0, + "RecommendationRules": [ + { + "Tests": [ + { + "Type": "MSProjectSdkAttribute", + "Condition": { + "Value": "Microsoft.NET.Sdk.Web" + } + }, + { + "Type": "MSProperty", + "Condition": { + "PropertyName": "TargetFramework", + "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0" ] + } + } + ], + "Effect": { + "Pass": { "Include": true }, + "Fail": {"Include": false} + } + } + ], + + "OptionSettings": [ + { + "Id": "EnhancedHealthReporting", + "Name": "Enhanced Health Reporting", + "Description": "Enhanced health reporting provides free real-time application and operating system monitoring of the instances and other resources in your environment.", + "Type": "String", + "DefaultValue": "enhanced", + "AllowedValues": [ + "enhanced", + "basic" + ], + "ValueMapping": { + "enhanced": "Enhanced", + "basic": "Basic" + }, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "XRayTracingSupportEnabled", + "Name": "Enable AWS X-Ray Tracing Support", + "Description": "AWS X-Ray is a service that collects data about requests that your application serves, and provides tools you can use to view, filter, and gain insights into that data to identify issues and opportunities for optimization. Do you want to enable AWS X-Ray tracing support?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "ReverseProxy", + "Name": "Reverse Proxy", + "Description": "By default Nginx is used as a reverse proxy in front of the .NET Core web server Kestrel. To use Kestrel as the front facing web server then select `none` as the reverse proxy.", + "Type": "String", + "DefaultValue": "nginx", + "AllowedValues": [ + "nginx", + "none" + ], + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "HealthCheckURL", + "Name": "Health Check URL", + "Description": "Customize the load balancer health check to ensure that your application, and not just the web server, is in a good state.", + "Type": "String", + "DefaultValue": "/", + "AdvancedSetting": false, + "Updatable": true + } + ] +} diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json index 9a0e38f4e..07773e51f 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -33,17 +33,28 @@ "description": "The unique id for the recipe. This value should never been change once the recipe is released because it will be stored in user config files.", "minLength": 1 }, + "Version": { + "type": "string", + "title": "Version string for the recipe", + "description": "Version string for the recipe. Its value will be incremented when the recipe is updated.", + "minLength": 1 + }, "Name": { "type": "string", "title": "Name", "description": "The name that will be showed to the user when choosing which recipe to choose for deployment.", "minLength": 1 }, + "DisableNewDeployments": { + "type": "boolean", + "title": "DisableNewDeployments", + "description": "A boolean value that indicates if this recipe should be presented as an option during new deployments." + }, "DeploymentType": { "type": "string", "title": "Deployment type", "description": "The technology used to deploy the project.", - "enum": [ "CdkProject" ] + "enum": [ "CdkProject", "BeanstalkEnvironment"] }, "DeploymentBundle": { "type": "string", diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index 1be411a07..13903c04e 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1476,6 +1476,17 @@ public enum DeploymentStatus } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public enum DeploymentTypes + { + [System.Runtime.Serialization.EnumMember(Value = @"CloudFormationStack")] + CloudFormationStack = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"BeanstalkEnvironment")] + BeanstalkEnvironment = 1, + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] public partial class DeployToolExceptionSummary { @@ -1537,6 +1548,13 @@ public partial class ExistingDeploymentSummary [Newtonsoft.Json.JsonProperty("updatedByCurrentUser", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool UpdatedByCurrentUser { get; set; } + [Newtonsoft.Json.JsonProperty("deploymentType", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public DeploymentTypes DeploymentType { get; set; } + + [Newtonsoft.Json.JsonProperty("existingDeploymentId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string ExistingDeploymentId { get; set; } + } @@ -1715,6 +1733,10 @@ public partial class RecommendationSummary [Newtonsoft.Json.JsonProperty("targetService", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string TargetService { get; set; } + [Newtonsoft.Json.JsonProperty("deploymentType", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public DeploymentTypes DeploymentType { get; set; } + } @@ -1727,8 +1749,8 @@ public partial class SetDeploymentTargetInput [Newtonsoft.Json.JsonProperty("newDeploymentRecipeId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string NewDeploymentRecipeId { get; set; } - [Newtonsoft.Json.JsonProperty("existingDeploymentName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public string ExistingDeploymentName { get; set; } + [Newtonsoft.Json.JsonProperty("existingDeploymentId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string ExistingDeploymentId { get; set; } } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs index 1f9d84058..64a1e1c98 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -25,7 +25,7 @@ public void VerifyJsonParsing() { Assert.Equal("default", _userDeploymentSettings.AWSProfile); Assert.Equal("us-west-2", _userDeploymentSettings.AWSRegion); - Assert.Equal("MyAppStack", _userDeploymentSettings.StackName); + Assert.Equal("MyAppStack", _userDeploymentSettings.ApplicationName); Assert.Equal("AspNetAppEcsFargate", _userDeploymentSettings.RecipeId); var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs index 303e7d65e..db299265e 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -25,7 +25,7 @@ public void VerifyJsonParsing() { Assert.Equal("default", _userDeploymentSettings.AWSProfile); Assert.Equal("us-west-2", _userDeploymentSettings.AWSRegion); - Assert.Equal("MyAppStack", _userDeploymentSettings.StackName); + Assert.Equal("MyAppStack", _userDeploymentSettings.ApplicationName); Assert.Equal("AspNetAppElasticBeanstalkLinux", _userDeploymentSettings.RecipeId); var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs index 66e025f9f..6de7cf9df 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs @@ -25,7 +25,7 @@ public void VerifyJsonParsing() { Assert.Equal("default", _userDeploymentSettings.AWSProfile); Assert.Equal("us-west-2", _userDeploymentSettings.AWSRegion); - Assert.Equal("MyAppStack", _userDeploymentSettings.StackName); + Assert.Equal("MyAppStack", _userDeploymentSettings.ApplicationName); Assert.Equal("AspNetAppElasticBeanstalkLinux", _userDeploymentSettings.RecipeId); var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json index f4eecf4f6..6a0384d2e 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json @@ -1,7 +1,7 @@ { "AWSProfile": "default", "AWSRegion": "us-west-2", - "StackName": "MyAppStack", + "ApplicationName": "MyAppStack", "RecipeId": "AspNetAppEcsFargate", "OptionSettingsConfig":{ "ECSCluster": diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json index 2b996a868..eb95c9f27 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json @@ -1,7 +1,7 @@ { "AWSProfile": "default", "AWSRegion": "us-west-2", - "StackName": "MyAppStack", + "ApplicationName": "MyAppStack", "RecipeId": "AspNetAppElasticBeanstalkLinux", "OptionSettingsConfig": { "BeanstalkApplication": { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json index fbd27c8c2..1ef8f1199 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json @@ -1,7 +1,7 @@ { "AWSProfile": "default", "AWSRegion": "us-west-2", - "StackName": "MyAppStack", + "ApplicationName": "MyAppStack", "RecipeId": "AspNetAppElasticBeanstalkLinux", "OptionSettingsConfig": { "BeanstalkApplication": { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs index 8badfebdc..87fd484a1 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs @@ -36,6 +36,10 @@ public Task WriteAllTextAsync(string filePath, string contents, CancellationToke InMemoryStore[filePath] = contents; return Task.CompletedTask; } + + public FileStream OpenRead(string filePath) => throw new NotImplementedException(); + public string GetExtension(string filePath) => throw new NotImplementedException(); + public long GetSizeInBytes(string filePath) => throw new NotImplementedException(); } public static class TestFileManagerExtensions diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs index 183d58361..3943bba86 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs @@ -65,7 +65,7 @@ public async Task DefaultConfigurations(params string[] components) await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running @@ -95,7 +95,7 @@ public async Task DefaultConfigurations(params string[] components) Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); // Setup for redeployment turning on access logging via settings file. @@ -103,7 +103,7 @@ public async Task DefaultConfigurations(params string[] components) await _interactiveService.StdInWriter.FlushAsync(); var applyLoggingSettingsFile = Path.Combine(Directory.GetParent(_testAppManager.GetProjectPath(Path.Combine(components))).FullName, "apply-settings.json"); - deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName, "--diagnostics", "--apply", applyLoggingSettingsFile }; + deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics", "--apply", applyLoggingSettingsFile }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs index 2b6207f6c..12a36c4c9 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -67,7 +67,7 @@ public async Task PerformDeployment() var userDeploymentSettings = UserDeploymentSettings.ReadSettings(configFilePath); - _stackName = userDeploymentSettings.StackName; + _stackName = userDeploymentSettings.ApplicationName; _clusterName = userDeploymentSettings.LeafOptionSettingItems["ECSCluster.NewClusterName"]; var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; @@ -94,7 +94,7 @@ public async Task PerformDeployment() Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for delete diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs index 056ea3210..8e73fe806 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -61,7 +61,7 @@ public async Task PerformDeployment() var userDeploymentSettings = UserDeploymentSettings.ReadSettings(configFilePath); - _stackName = userDeploymentSettings.StackName; + _stackName = userDeploymentSettings.ApplicationName; var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); @@ -84,7 +84,7 @@ public async Task PerformDeployment() Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for delete diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs index fc3f4fc6c..a7c8192dd 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs @@ -69,7 +69,7 @@ public async Task DefaultConfigurations(params string[] components) await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running @@ -94,7 +94,7 @@ public async Task DefaultConfigurations(params string[] components) Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for delete diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 93c4afc96..79c8ffcca 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -23,6 +23,8 @@ using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.IntegrationTests.Services; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.Utilities; +using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -50,10 +52,11 @@ public async Task GenerateRecommendationsWithoutCustomRecipes() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - recommendations.Count.ShouldEqual(3); + recommendations.Count.ShouldEqual(4); 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 + recommendations[3].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe } [Fact] @@ -85,12 +88,13 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(5); + recommendations.Count.ShouldEqual(6); recommendations[0].Name.ShouldEqual(customEcsRecipeName); // custom recipe recommendations[1].Name.ShouldEqual(customEbsRecipeName); // custom recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe + recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -133,12 +137,13 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(5); + recommendations.Count.ShouldEqual(6); recommendations[0].Name.ShouldEqual(customEbsRecipeName); recommendations[1].Name.ShouldEqual(customEcsRecipeName); recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); recommendations[3].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); + recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); @@ -221,7 +226,9 @@ private async Task GetOrchestrator(string targetApplicationProject new Mock().Object, customRecipeLocator, new List { RecipeLocator.FindRecipeDefinitionsPath() }, - directoryManager); + fileManager, + directoryManager, + new Mock().Object); } private async Task GetCustomRecipeId(string recipeFilePath) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs index e33e2b4dd..1e299dfcb 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs @@ -70,14 +70,14 @@ public async Task AttemptWorkFlow() await Utilities.CreateCDKDeploymentProjectWithRecipeName(projectPath, "Custom App Runner Recipe", "2", incompatibleDeploymentProjectPath, underSourceControl: false); // attempt re-deployment using incompatible CDK project - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", incompatibleDeploymentProjectPath, "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", incompatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; var returnCode = await _app.Run(deployArgs); Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); // attempt re-deployment using compatible CDK project await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings await _interactiveService.StdInWriter.FlushAsync(); - deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", compatibleDeploymentProjectPath, "--stack-name", _stackName, "--diagnostics" }; + deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", compatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; returnCode = await _app.Run(deployArgs); Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); @@ -96,7 +96,7 @@ private async Task PerformInitialDeployment(string projectPath) await _interactiveService.StdInWriter.FlushAsync(); // Deploy - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; var returnCode = await _app.Run(deployArgs); Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); @@ -121,7 +121,7 @@ private async Task PerformInitialDeployment(string projectPath) Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index ef5b35d65..c2fecbce7 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -244,6 +244,46 @@ public async Task WebFargateDeploymentNoConfigChanges() } } + [Fact] + public async Task RecommendationsForNewDeployments_DoesNotIncludeExistingBeanstalkEnvironmentRecipe() + { + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var portNumber = 4002; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var cancelSource = new CancellationTokenSource(); + + var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + + try + { + var baseUrl = $"http://localhost:{portNumber}/"; + var restClient = new RestAPIClient(baseUrl, httpClient); + + await WaitTillServerModeReady(restClient); + + var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput + { + AwsRegion = _awsRegion, + ProjectPath = projectPath + }); + + var sessionId = startSessionOutput.SessionId; + Assert.NotNull(sessionId); + + var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); + Assert.NotEmpty(getRecommendationOutput.Recommendations); + + var recommendations = getRecommendationOutput.Recommendations; + Assert.DoesNotContain(recommendations, x => x.DeploymentType == DeploymentTypes.BeanstalkEnvironment); + } + finally + { + cancelSource.Cancel(); + } + } + private async Task WaitForDeployment(RestAPIClient restApiClient, string sessionId) { // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index b4c376b93..c77a90e43 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -55,5 +55,7 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> ListOfS3Buckets() => throw new NotImplementedException(); public Task> ListOfAvailableInstanceTypes() => throw new NotImplementedException(); public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); + public Task> ListElasticBeanstalkResourceTags(string resourceArn) => throw new NotImplementedException(); + public Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentId) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index a54dd7dc8..b866d4e50 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -62,7 +62,7 @@ public async Task DefaultConfigurations() // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running @@ -87,7 +87,7 @@ public async Task DefaultConfigurations() Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for delete diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index dd72995b0..ad7c90360 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs @@ -67,7 +67,7 @@ public async Task DefaultConfigurations() // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running @@ -94,7 +94,7 @@ public async Task DefaultConfigurations() Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for re-deployment @@ -102,7 +102,7 @@ public async Task DefaultConfigurations() await _interactiveService.StdInWriter.FlushAsync(); // Perform re-deployment - deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; + deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); Assert.Equal("ACTIVE", cluster.Status); @@ -131,7 +131,7 @@ public async Task AppRunnerDeployment() // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running @@ -153,7 +153,7 @@ public async Task AppRunnerDeployment() Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); // Arrange input for delete diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 8e3e442ba..5d6b5907b 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -61,7 +61,7 @@ public async Task BuildDockerImage_DockerExecutionDirectoryNotSet() var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); - var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty); + var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var result = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); var dockerFile = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(recommendation.ProjectPath)), "Dockerfile"); @@ -82,7 +82,7 @@ public async Task BuildDockerImage_DockerExecutionDirectorySet() recommendation.DeploymentBundle.DockerExecutionDirectory = projectPath; - var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty); + var cloudApplication = new CloudApplication("ConsoleAppTask", string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var result = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); var dockerFile = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(recommendation.ProjectPath)), "Dockerfile"); @@ -100,10 +100,10 @@ public async Task PushDockerImage_RepositoryNameCheck() var project = await _projectDefinitionParser.Parse(projectPath); var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); - var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty); + var cloudApplication = new CloudApplication("ConsoleAppTask", string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); await _deploymentBundleHandler.PushDockerImageToECR(cloudApplication, recommendation, "ConsoleAppTask:latest"); - Assert.Equal(cloudApplication.StackName.ToLower(), recommendation.DeploymentBundle.ECRRepositoryName); + Assert.Equal(cloudApplication.Name.ToLower(), recommendation.DeploymentBundle.ECRRepositoryName); } [Fact] @@ -180,7 +180,7 @@ public async Task DockerExecutionDirectory_SolutionLevel() var recommendations = await engine.ComputeRecommendations(); var recommendation = recommendations.FirstOrDefault(x => x.Recipe.DeploymentBundle.Equals(DeploymentBundleTypes.Container)); - var cloudApplication = new CloudApplication("WebAppWithSolutionParentLevel", String.Empty); + var cloudApplication = new CloudApplication("WebAppWithSolutionParentLevel", string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var result = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); Assert.Equal(Directory.GetParent(SystemIOUtilities.ResolvePath(projectPath)).FullName, recommendation.DeploymentBundle.DockerExecutionDirectory); @@ -195,7 +195,7 @@ public async Task DockerExecutionDirectory_DockerfileLevel() var recommendations = await engine.ComputeRecommendations(); var recommendation = recommendations.FirstOrDefault(x => x.Recipe.DeploymentBundle.Equals(DeploymentBundleTypes.Container)); - var cloudApplication = new CloudApplication("WebAppNoSolution", String.Empty); + var cloudApplication = new CloudApplication("WebAppNoSolution", string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var result = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); Assert.Equal(Path.GetFullPath(SystemIOUtilities.ResolvePath(projectPath)), recommendation.DeploymentBundle.DockerExecutionDirectory); diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index 4065ff0c5..620b01eb0 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -126,5 +126,41 @@ public async Task RecipeController_GetRecipe_WithProjectPath() var resultRecipe = Assert.IsType(result.Value); Assert.Equal(recipe.Id, resultRecipe.Id); } + + [Theory] + [InlineData(CloudApplicationResourceType.CloudFormationStack, DeploymentTypes.CloudFormationStack)] + [InlineData(CloudApplicationResourceType.BeanstalkEnvironment, DeploymentTypes.BeanstalkEnvironment)] + public void ExistingDeploymentSummary_ContainsCorrectDeploymentType(CloudApplicationResourceType resourceType, DeploymentTypes expectedDeploymentType) + { + var existingDeploymentSummary = new ExistingDeploymentSummary( + "name", + "recipeId", + "recipeName", + "shortDescription", + "description", + "targetService", + System.DateTime.Now, + true, + resourceType, + "uniqueId"); + + Assert.Equal(expectedDeploymentType, existingDeploymentSummary.DeploymentType); + } + + [Theory] + [InlineData(Deploy.Common.Recipes.DeploymentTypes.CdkProject, DeploymentTypes.CloudFormationStack)] + [InlineData(Deploy.Common.Recipes.DeploymentTypes.BeanstalkEnvironment, DeploymentTypes.BeanstalkEnvironment)] + public void RecommendationSummary_ContainsCorrectDeploymentType(Deploy.Common.Recipes.DeploymentTypes deploymentType, DeploymentTypes expectedDeploymentType) + { + var recommendationSummary = new RecommendationSummary( + "recipeId", + "name", + "shortDescription", + "description", + "targetService", + deploymentType); + + Assert.Equal(expectedDeploymentType, recommendationSummary.DeploymentType); + } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index 873a9c0e9..d4b97b7c2 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -80,5 +80,7 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> ListOfS3Buckets() => throw new NotImplementedException(); public Task> ListOfAvailableInstanceTypes() => throw new NotImplementedException(); public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); + public Task> ListElasticBeanstalkResourceTags(string resourceArn) => throw new NotImplementedException(); + public Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentId) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs index 5be78afc3..391621464 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; +using Amazon.ElasticBeanstalk; +using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; @@ -39,7 +41,7 @@ public DeployedApplicationQueryerTests() public async Task GetExistingDeployedApplications_ListDeploymentsCall() { var stack = new Stack { - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "AspNetAppEcsFargate" } }, @@ -52,16 +54,21 @@ public async Task GetExistingDeployedApplications_ListDeploymentsCall() .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(new List() { stack })); + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(new List())); + var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, _mockOrchestratorInteractiveService.Object); - var result = await deployedApplicationQueryer.GetExistingDeployedApplications(); + var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); Assert.Single(result); var expectedStack = result.First(); - Assert.Equal("Stack1", expectedStack.StackName); + Assert.Equal("Stack1", expectedStack.Name); } [Fact] @@ -69,7 +76,7 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() { var stacks = new List { new Stack{ - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "AspNetAppEcsFargate" } }, @@ -78,7 +85,7 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() StackName = "WebApp" }, new Stack{ - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "ConsoleAppEcsFargateService" } }, @@ -92,6 +99,10 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(stacks)); + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(new List())); + var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, @@ -117,7 +128,7 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() var stacks = new List { // Existing stack from the base recipe which should be valid new Stack{ - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "AspNetAppEcsFargate" } }, @@ -127,7 +138,7 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() }, // Existing stack that was deployed custom deployment project. Should be valid. new Stack{ - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "AspNetAppEcsFargate-Custom" } }, @@ -137,7 +148,7 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() }, // Stack created from a different recipe and should not be valid. new Stack{ - Tags = new List() { new Tag { + Tags = new List() { new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "ConsoleAppEcsFargateService" } }, @@ -151,6 +162,10 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(stacks)); + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(new List())); + var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, @@ -184,9 +199,9 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() [InlineData("AspNetAppEcsFargate", Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX, "ROLLBACK_COMPLETE")] public async Task GetExistingDeployedApplications_InvalidConfigurations(string recipeId, string stackDecription, string deploymentStatus) { - var tags = new List(); + var tags = new List(); if (!string.IsNullOrEmpty(recipeId)) - tags.Add(new Tag + tags.Add(new Amazon.CloudFormation.Model.Tag { Key = Constants.CloudFormationIdentifier.STACK_TAG, Value = "AspNetAppEcsFargate" @@ -204,13 +219,240 @@ public async Task GetExistingDeployedApplications_InvalidConfigurations(string r .Setup(x => x.GetCloudFormationStacks()) .Returns(Task.FromResult(new List() { stack })); + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(new List())); + var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, _mockOrchestratorInteractiveService.Object); - var result = await deployedApplicationQueryer.GetExistingDeployedApplications(); + var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); Assert.Empty(result); } + + [Fact] + public async Task GetExistingDeployedApplications_ContainsValidBeanstalkEnvironments() + { + var environments = new List + { + new EnvironmentDescription + { + EnvironmentName = "env-1", + PlatformArn = "dotnet-platform-arn1", + EnvironmentArn = "env-arn-1", + Status = EnvironmentStatus.Ready + }, + new EnvironmentDescription + { + EnvironmentName = "env-2", + PlatformArn = "dotnet-platform-arn1", + EnvironmentArn = "env-arn-2", + Status = EnvironmentStatus.Ready + } + }; + + var platforms = new List + { + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn1" + }, + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn2" + } + }; + + _mockAWSResourceQueryer + .Setup(x => x.GetCloudFormationStacks()) + .Returns(Task.FromResult(new List())); + + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(environments)); + + _mockAWSResourceQueryer + .Setup(x => x.GetElasticBeanstalkPlatformArns()) + .Returns(Task.FromResult(platforms)); + + _mockAWSResourceQueryer + .Setup(x => x.ListElasticBeanstalkResourceTags(It.IsAny())) + .Returns(Task.FromResult(new List())); + + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); + + var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); + Assert.Contains(result, x => string.Equals("env-1", x.Name)); + Assert.Contains(result, x => string.Equals("env-2", x.Name)); + } + + [Fact] + public async Task GetExistingDeployedApplication_SkipsEnvironmentsWithIncompatiblePlatformArns() + { + var environments = new List + { + new EnvironmentDescription + { + EnvironmentName = "env", + PlatformArn = "incompatible-platform-arn", + EnvironmentArn = "env-arn", + Status = EnvironmentStatus.Ready + } + }; + + var platforms = new List + { + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn1" + }, + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn2" + } + }; + + _mockAWSResourceQueryer + .Setup(x => x.GetCloudFormationStacks()) + .Returns(Task.FromResult(new List())); + + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(environments)); + + _mockAWSResourceQueryer + .Setup(x => x.GetElasticBeanstalkPlatformArns()) + .Returns(Task.FromResult(platforms)); + + _mockAWSResourceQueryer + .Setup(x => x.ListElasticBeanstalkResourceTags(It.IsAny())) + .Returns(Task.FromResult(new List())); + + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); + + var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); + Assert.Empty(result); + } + + [Fact] + public async Task GetExistingDeployedApplication_SkipsEnvironmentsCreatedFromTheDeployTool() + { + var environments = new List + { + new EnvironmentDescription + { + EnvironmentName = "env", + PlatformArn = "dotnet-platform-arn1", + EnvironmentArn = "env-arn", + Status = EnvironmentStatus.Ready + } + }; + + var platforms = new List + { + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn1" + }, + new PlatformSummary + { + PlatformArn = "dotnet-platform-arn2" + } + }; + + var tags = new List + { + new Amazon.ElasticBeanstalk.Model.Tag + { + Key = Constants.CloudFormationIdentifier.STACK_TAG, + Value = "RecipeId" + } + }; + + _mockAWSResourceQueryer + .Setup(x => x.GetCloudFormationStacks()) + .Returns(Task.FromResult(new List())); + + _mockAWSResourceQueryer + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Returns(Task.FromResult(environments)); + + _mockAWSResourceQueryer + .Setup(x => x.GetElasticBeanstalkPlatformArns()) + .Returns(Task.FromResult(platforms)); + + _mockAWSResourceQueryer + .Setup(x => x.ListElasticBeanstalkResourceTags("env-arn")) + .Returns(Task.FromResult(tags)); + + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); + + var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; + var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); + Assert.Empty(result); + } + + [Fact] + public async Task GetPreviousSettings_BeanstalkEnvironment() + { + var application = new CloudApplication("name", "Id", CloudApplicationResourceType.BeanstalkEnvironment, "recipe"); + var configurationSettings = new List + { + new ConfigurationOptionSetting + { + Namespace = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionNameSpace, + OptionName = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionName, + Value = "enhanced" + }, + new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.HealthCheckURLOptionName, + Namespace = Constants.ElasticBeanstalk.HealthCheckURLOptionNameSpace, + Value = "/" + }, + new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.ProxyOptionName, + Namespace = Constants.ElasticBeanstalk.ProxyOptionNameSpace, + Value = "nginx" + }, + new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.XRayTracingOptionName, + Namespace = Constants.ElasticBeanstalk.XRayTracingOptionNameSpace, + Value = "false" + } + }; + + _mockAWSResourceQueryer + .Setup(x => x.GetBeanstalkEnvironmentConfigurationSettings(It.IsAny())) + .Returns(Task.FromResult(configurationSettings)); + + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object); + + var optionSettings = await deployedApplicationQueryer.GetPreviousSettings(application); + + Assert.Equal("enhanced", optionSettings[Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId]); + Assert.Equal("/", optionSettings[Constants.ElasticBeanstalk.HealthCheckURLOptionId]); + Assert.Equal("nginx", optionSettings[Constants.ElasticBeanstalk.ProxyOptionId]); + Assert.Equal("false", optionSettings[Constants.ElasticBeanstalk.XRayTracingOptionId]); + } } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeploymentCommandFactoryTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeploymentCommandFactoryTests.cs new file mode 100644 index 000000000..09c4a440a --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeploymentCommandFactoryTests.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.Text; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Orchestration.DeploymentCommands; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class DeploymentCommandFactoryTests + { + [Theory] + [InlineData(DeploymentTypes.CdkProject, typeof(CdkDeploymentCommand))] + [InlineData(DeploymentTypes.BeanstalkEnvironment, typeof(BeanstalkEnvironmentDeploymentCommand))] + public void BuildsValidDeploymentCommand(DeploymentTypes deploymentType, Type expectedDeploymentCommandType) + { + var command = DeploymentCommandFactory.BuildDeploymentCommand(deploymentType); + Assert.True(command.GetType() == expectedDeploymentCommandType); + } + } +} diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs index fea1ac46a..2f3cb4ced 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs @@ -33,7 +33,7 @@ public class DisplayedResourcesHandlerTests public DisplayedResourcesHandlerTests() { _mockAWSResourceQueryer = new Mock(); - _cloudApplication = new CloudApplication("StackName", "RecipeId"); + _cloudApplication = new CloudApplication("StackName", "UniqueId", CloudApplicationResourceType.CloudFormationStack, "RecipeId"); _displayedResourcesFactory = new DisplayedResourceCommandFactory(_mockAWSResourceQueryer.Object); _stackResource = new StackResource(); _stackResources = new List() { _stackResource }; diff --git a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs new file mode 100644 index 000000000..0741dfeb0 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs @@ -0,0 +1,143 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Amazon.ElasticBeanstalk.Model; +using Amazon.Runtime; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Orchestration.UnitTests.Utilities; +using AWS.Deploy.Recipes; +using Moq; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class ElasticBeanstalkHandlerTests + { + [Fact] + public async Task GetAdditionSettingsTest_DefaultValues() + { + // ARRANGE + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + var recommendations = await engine.ComputeRecommendations(); + var recommendation = recommendations.First(r => r.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID)); + + var elasticBeanstalkHandler = new AWSElasticBeanstalkHandler(new Mock().Object, + new Mock().Object, + new Mock().Object); + + // ACT + var optionSettings = elasticBeanstalkHandler.GetEnvironmentConfigurationSettings(recommendation); + + // ASSERT + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionName, + Namespace = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionNameSpace, + Value = "enhanced" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.HealthCheckURLOptionName, + Namespace = Constants.ElasticBeanstalk.HealthCheckURLOptionNameSpace, + Value = "/" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.ProxyOptionName, + Namespace = Constants.ElasticBeanstalk.ProxyOptionNameSpace, + Value = "nginx" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.XRayTracingOptionName, + Namespace = Constants.ElasticBeanstalk.XRayTracingOptionNameSpace, + Value = "false" + }, x)); + } + + [Fact] + public async Task GetAdditionSettingsTest_CustomValues() + { + // ARRANGE + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + var recommendations = await engine.ComputeRecommendations(); + var recommendation = recommendations.First(r => r.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID)); + + var elasticBeanstalkHandler = new AWSElasticBeanstalkHandler(new Mock().Object, + new Mock().Object, + new Mock().Object); + + recommendation.GetOptionSetting(Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId).SetValueOverride("basic"); + recommendation.GetOptionSetting(Constants.ElasticBeanstalk.HealthCheckURLOptionId).SetValueOverride("/url"); + recommendation.GetOptionSetting(Constants.ElasticBeanstalk.ProxyOptionId).SetValueOverride("none"); + recommendation.GetOptionSetting(Constants.ElasticBeanstalk.XRayTracingOptionId).SetValueOverride("true"); + + // ACT + var optionSettings = elasticBeanstalkHandler.GetEnvironmentConfigurationSettings(recommendation); + + // ASSERT + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionName, + Namespace = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionNameSpace, + Value = "basic" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.HealthCheckURLOptionName, + Namespace = Constants.ElasticBeanstalk.HealthCheckURLOptionNameSpace, + Value = "/url" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.ProxyOptionName, + Namespace = Constants.ElasticBeanstalk.ProxyOptionNameSpace, + Value = "none" + }, x)); + + Assert.Contains(optionSettings, x => IsEqual(new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.XRayTracingOptionName, + Namespace = Constants.ElasticBeanstalk.XRayTracingOptionNameSpace, + Value = "true" + }, x)); + } + + private async Task BuildRecommendationEngine(string testProjectName) + { + var fullPath = SystemIOUtilities.ResolvePath(testProjectName); + + var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); + var awsCredentials = new Mock(); + var session = new OrchestratorSession( + await parser.Parse(fullPath), + awsCredentials.Object, + "us-west-2", + "123456789012") + { + AWSProfileName = "default" + }; + + return new RecommendationEngine.RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + } + + private bool IsEqual(ConfigurationOptionSetting expected, ConfigurationOptionSetting actual) + { + return string.Equals(expected.OptionName, actual.OptionName) + && string.Equals(expected.Namespace, actual.Namespace) + && string.Equals(expected.Value, actual.Value); + } + } +} diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs index 90c45520b..9bd034fb5 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs @@ -36,6 +36,10 @@ public Task WriteAllTextAsync(string filePath, string contents, CancellationToke InMemoryStore[filePath] = contents; return Task.CompletedTask; } + + public FileStream OpenRead(string filePath) => throw new NotImplementedException(); + public string GetExtension(string filePath) => throw new NotImplementedException(); + public long GetSizeInBytes(string filePath) => throw new NotImplementedException(); } public static class TestFileManagerExtensions diff --git a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs index 16ea5fe45..ecc7707e9 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs @@ -96,7 +96,7 @@ public async Task SuggestsValidNameAndRespectsExistingApplications() var existingApplication = new List { - new CloudApplication(projectFile, string.Empty) + new CloudApplication(projectFile, string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty) }; // ACT diff --git a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json index 68d6abac4..2e44ae519 100644 --- a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json +++ b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json @@ -1,5 +1,5 @@ { - "StackName": "ElasticBeanStalk{Suffix}", + "ApplicationName": "ElasticBeanStalk{Suffix}", "RecipeId": "AspNetAppElasticBeanstalkLinux", "OptionSettingsConfig": { "BeanstalkApplication": { diff --git a/testapps/WebAppWithDockerFile/ECSFargateConfigFile.json b/testapps/WebAppWithDockerFile/ECSFargateConfigFile.json index 929eaaa8f..68365aa5e 100644 --- a/testapps/WebAppWithDockerFile/ECSFargateConfigFile.json +++ b/testapps/WebAppWithDockerFile/ECSFargateConfigFile.json @@ -1,5 +1,5 @@ { - "StackName": "EcsFargate{Suffix}", + "ApplicationName": "EcsFargate{Suffix}", "RecipeId": "AspNetAppEcsFargate", "OptionSettingsConfig":{ "ECSCluster": From 69dd0f979c323c62ff59e540c58f77ec90aca73c Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 2 Feb 2022 11:00:09 -0500 Subject: [PATCH 7/8] feat: add check for opt-in regions --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 6 +- .../Controllers/DeploymentController.cs | 2 +- .../DefaultAWSClientFactory.cs | 5 +- src/AWS.Deploy.Common/Exceptions.cs | 4 +- src/AWS.Deploy.Common/IAWSClientFactory.cs | 2 +- src/AWS.Deploy.Constants/CLI.cs | 4 + .../Data/AWSResourceQueryer.cs | 43 ++++++++++- src/AWS.Deploy.Orchestration/Exceptions.cs | 8 ++ .../Utilities/TestToolAWSResourceQueryer.cs | 3 +- .../TestAWSClientFactory.cs | 2 +- .../AWS.Deploy.CLI.UnitTests/TypeHintTests.cs | 8 +- .../Utilities/TestToolAWSResourceQueryer.cs | 3 +- .../AWSResourceQueryerTests.cs | 73 +++++++++++++++++++ 13 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index e33f9bc2b..5e9925285 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -187,7 +187,7 @@ private Command BuildDeployCommand() _commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion); _awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion); - var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); + var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(awsRegion); var session = new OrchestratorSession( projectDefinition, @@ -299,7 +299,7 @@ private Command BuildDeleteCommand() { var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? string.Empty); - var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); + var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(awsRegion); session = new OrchestratorSession( projectDefinition, @@ -370,6 +370,8 @@ private Command BuildListCommand() awsOptions.Region = RegionEndpoint.GetBySystemName(awsRegion); }); + await _awsResourceQueryer.GetCallerIdentity(awsRegion); + var listDeploymentsCommand = new ListDeploymentsCommand(_toolInteractiveService, _deployedApplicationQueryer); await listDeploymentsCommand.ExecuteAsync(); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index c82223be6..47c2977b8 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -79,7 +79,7 @@ public async Task StartDeploymentSession(StartDeploymentSessionIn output.SessionId, input.ProjectPath, input.AWSRegion, - (await awsResourceQueryer.GetCallerIdentity()).Account, + (await awsResourceQueryer.GetCallerIdentity(input.AWSRegion)).Account, await _projectParserUtility.Parse(input.ProjectPath) ); diff --git a/src/AWS.Deploy.Common/DefaultAWSClientFactory.cs b/src/AWS.Deploy.Common/DefaultAWSClientFactory.cs index 4312363e5..f00fd05bf 100644 --- a/src/AWS.Deploy.Common/DefaultAWSClientFactory.cs +++ b/src/AWS.Deploy.Common/DefaultAWSClientFactory.cs @@ -17,12 +17,15 @@ public void ConfigureAWSOptions(Action awsOptionsAction) _awsOptionsAction = awsOptionsAction; } - public T GetAWSClient() where T : IAmazonService + public T GetAWSClient(string? awsRegion = null) where T : IAmazonService { var awsOptions = new AWSOptions(); _awsOptionsAction?.Invoke(awsOptions); + if (!string.IsNullOrEmpty(awsRegion)) + awsOptions.Region = RegionEndpoint.GetBySystemName(awsRegion); + return awsOptions.CreateServiceClient(); } } diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index ab6e04ed7..afb00a8a6 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -98,7 +98,9 @@ public enum DeployToolErrorCode FailedS3Upload = 10007600, FailedToCreateElasticBeanstalkApplicationVersion = 10007700, FailedToUpdateElasticBeanstalkEnvironment = 10007800, - FailedToCreateElasticBeanstalkStorageLocation = 10007900 + FailedToCreateElasticBeanstalkStorageLocation = 10007900, + UnableToAccessAWSRegion = 10008000, + OptInRegionDisabled = 10008100, } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Common/IAWSClientFactory.cs b/src/AWS.Deploy.Common/IAWSClientFactory.cs index 822eadd88..80b1434fa 100644 --- a/src/AWS.Deploy.Common/IAWSClientFactory.cs +++ b/src/AWS.Deploy.Common/IAWSClientFactory.cs @@ -9,7 +9,7 @@ namespace AWS.Deploy.Common { public interface IAWSClientFactory { - T GetAWSClient() where T : IAmazonService; + T GetAWSClient(string? awsRegion = null) where T : IAmazonService; void ConfigureAWSOptions(Action awsOptionsAction); } } diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index 43b5be765..9f6ebcd3b 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -6,6 +6,10 @@ namespace AWS.Deploy.Constants { internal static class CLI { + // Represents the default STS AWS region that is used for the purposes of + // retrieving the caller identity and determining if a user is in an opt-in region. + public const string DEFAULT_STS_AWS_REGION = "us-east-1"; + public const string CREATE_NEW_LABEL = "*** Create new ***"; public const string DEFAULT_LABEL = "*** Default ***"; public const string EMPTY_LABEL = "*** Empty ***"; diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 3f2a1a54b..841b8d58d 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Amazon; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; using Amazon.CloudFront; @@ -63,7 +64,7 @@ public interface IAWSResourceQueryer Task> GetECRRepositories(List repositoryNames); Task CreateECRRepository(string repositoryName); Task> GetCloudFormationStacks(); - Task GetCallerIdentity(); + Task GetCallerIdentity(string awsRegion); Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType); Task GetCloudFrontDistribution(string distributionId); Task> ListOfDyanmoDBTables(); @@ -448,10 +449,44 @@ public async Task> GetCloudFormationStacks() return await cloudFormationClient.Paginators.DescribeStacks(new DescribeStacksRequest()).Stacks.ToListAsync(); } - public async Task GetCallerIdentity() + public async Task GetCallerIdentity(string awsRegion) { - using var stsClient = _awsClientFactory.GetAWSClient(); - return await stsClient.GetCallerIdentityAsync(new GetCallerIdentityRequest()); + var request = new GetCallerIdentityRequest(); + + try + { + using var stsClient = _awsClientFactory.GetAWSClient(awsRegion); + return await stsClient.GetCallerIdentityAsync(request); + } + catch (Exception ex) + { + var regionEndpointPartition = RegionEndpoint.GetBySystemName(awsRegion).PartitionName ?? String.Empty; + if (regionEndpointPartition.Equals("aws") && !awsRegion.Equals(Constants.CLI.DEFAULT_STS_AWS_REGION)) + { + try + { + using var stsClient = _awsClientFactory.GetAWSClient(Constants.CLI.DEFAULT_STS_AWS_REGION); + await stsClient.GetCallerIdentityAsync(request); + } + catch (Exception e) + { + throw new UnableToAccessAWSRegionException( + DeployToolErrorCode.UnableToAccessAWSRegion, + $"We were unable to access the AWS region '{awsRegion}'. Make sure you have correct permissions for that region and the region is accessible.", + e); + } + + throw new UnableToAccessAWSRegionException( + DeployToolErrorCode.OptInRegionDisabled, + $"We were unable to access the Opt-In region '{awsRegion}'. Please enable the AWS Region '{awsRegion}' and try again. Additional details could be found at https://docs.aws.amazon.com/general/latest/gr/rande-manage.html", + ex); + } + + throw new UnableToAccessAWSRegionException( + DeployToolErrorCode.UnableToAccessAWSRegion, + $"We were unable to access the AWS region '{awsRegion}'. Make sure you have correct permissions for that region and the region is accessible.", + ex); + } } public async Task> ListOfLoadBalancers(Amazon.ElasticLoadBalancingV2.LoadBalancerTypeEnum loadBalancerType) diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 645fb6064..2fa0df672 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -204,4 +204,12 @@ public class ElasticBeanstalkException : DeployToolException { public ElasticBeanstalkException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if unable to access the specified AWS Region. + /// + public class UnableToAccessAWSRegionException : DeployToolException + { + public UnableToAccessAWSRegionException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index c77a90e43..6fb0c1aa6 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -14,6 +14,7 @@ using Amazon.ElasticBeanstalk.Model; using Amazon.ElasticLoadBalancingV2; using Amazon.IdentityManagement.Model; +using Amazon.Runtime; using Amazon.SecurityToken.Model; using AWS.Deploy.Orchestration.Data; @@ -33,7 +34,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task DescribeElasticBeanstalkEnvironment(string environmentId) => throw new NotImplementedException(); public Task DescribeElasticLoadBalancer(string loadBalancerArn) => throw new NotImplementedException(); public Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn) => throw new NotImplementedException(); - public Task GetCallerIdentity() => throw new NotImplementedException(); public Task> GetCloudFormationStacks() => throw new NotImplementedException(); public Task> GetECRAuthorizationToken() => throw new NotImplementedException(); public Task> GetECRRepositories(List repositoryNames) => throw new NotImplementedException(); @@ -57,5 +57,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); public Task> ListElasticBeanstalkResourceTags(string resourceArn) => throw new NotImplementedException(); public Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentId) => throw new NotImplementedException(); + public Task GetCallerIdentity(string awsRegion) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.UnitTests/TestAWSClientFactory.cs b/test/AWS.Deploy.CLI.UnitTests/TestAWSClientFactory.cs index ccd7cf5ed..455a81913 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TestAWSClientFactory.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TestAWSClientFactory.cs @@ -22,7 +22,7 @@ public TestAWSClientFactory(params IAmazonService[] clientMocks) _clients = clientMocks ?? new IAmazonService[0]; } - public T GetAWSClient() where T : IAmazonService + public T GetAWSClient(string? awsRegion = null) where T : IAmazonService { var match = _clients.OfType().FirstOrDefault(); diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs index 3aacec32b..346d9fba7 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs @@ -44,7 +44,7 @@ public async Task TestDynamoDBTableNameTypeHint() .Returns(ddbPaginators.Object); var awsClientFactory = new Mock(); - awsClientFactory.Setup(x => x.GetAWSClient()) + awsClientFactory.Setup(x => x.GetAWSClient(It.IsAny())) .Returns(ddbClient.Object); var awsResourceQueryer = new AWSResourceQueryer(awsClientFactory.Object); @@ -74,7 +74,7 @@ public async Task TestSQSQueueUrlTypeHint() .Returns(sqsPaginators.Object); var awsClientFactory = new Mock(); - awsClientFactory.Setup(x => x.GetAWSClient()) + awsClientFactory.Setup(x => x.GetAWSClient(It.IsAny())) .Returns(sqsClient.Object); var awsResourceQueryer = new AWSResourceQueryer(awsClientFactory.Object); @@ -104,7 +104,7 @@ public async Task TestSNSTopicArnTypeHint() .Returns(snsPaginators.Object); var awsClientFactory = new Mock(); - awsClientFactory.Setup(x => x.GetAWSClient()) + awsClientFactory.Setup(x => x.GetAWSClient(It.IsAny())) .Returns(snsClient.Object); var awsResourceQueryer = new AWSResourceQueryer(awsClientFactory.Object); @@ -126,7 +126,7 @@ public async Task TestS3BucketNameTypeHint() .Returns(Task.FromResult(new ListBucketsResponse { Buckets = new List { new S3Bucket {BucketName = "Bucket1" }, new S3Bucket { BucketName = "Bucket2" } } })); var awsClientFactory = new Mock(); - awsClientFactory.Setup(x => x.GetAWSClient()) + awsClientFactory.Setup(x => x.GetAWSClient(It.IsAny())) .Returns(s3Client.Object); var awsResourceQueryer = new AWSResourceQueryer(awsClientFactory.Object); diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index d4b97b7c2..3f209ba9e 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -12,6 +12,7 @@ using Amazon.ElasticBeanstalk.Model; using Amazon.ElasticLoadBalancingV2; using Amazon.IdentityManagement.Model; +using Amazon.Runtime; using Amazon.S3; using Amazon.SecurityToken.Model; using AWS.Deploy.Orchestration; @@ -26,7 +27,6 @@ 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 GetCallerIdentity() => throw new NotImplementedException(); public Task> GetECRAuthorizationToken() { @@ -82,5 +82,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> GetCloudFormationStackEvents(string stackName) => throw new NotImplementedException(); public Task> ListElasticBeanstalkResourceTags(string resourceArn) => throw new NotImplementedException(); public Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentId) => throw new NotImplementedException(); + public Task GetCallerIdentity(string awsRegion) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs new file mode 100644 index 000000000..a50107c13 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; +using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.Data; +using Moq; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class AWSResourceQueryerTests + { + private readonly Mock _mockAWSClientFactory; + private readonly Mock _mockSTSClient; + private readonly Mock _mockSTSClientDefaultRegion; + + public AWSResourceQueryerTests() + { + _mockAWSClientFactory = new Mock(); + _mockSTSClient = new Mock(); + _mockSTSClientDefaultRegion = new Mock(); + } + + [Fact] + public async Task GetCallerIdentity_HasRegionAccess() + { + var awsResourceQueryer = new AWSResourceQueryer(_mockAWSClientFactory.Object); + var stsResponse = new GetCallerIdentityResponse(); + + _mockAWSClientFactory.Setup(x => x.GetAWSClient("ap-southeast-3")).Returns(_mockSTSClient.Object); + _mockSTSClient.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(stsResponse); + + await awsResourceQueryer.GetCallerIdentity("ap-southeast-3"); + } + + [Fact] + public async Task GetCallerIdentity_OptInRegion() + { + var awsResourceQueryer = new AWSResourceQueryer(_mockAWSClientFactory.Object); + var stsResponse = new GetCallerIdentityResponse(); + + _mockAWSClientFactory.Setup(x => x.GetAWSClient("ap-southeast-3")).Returns(_mockSTSClient.Object); + _mockSTSClient.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Invalid token")); + + _mockAWSClientFactory.Setup(x => x.GetAWSClient("us-east-1")).Returns(_mockSTSClientDefaultRegion.Object); + _mockSTSClientDefaultRegion.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), It.IsAny())).ReturnsAsync(stsResponse); + + var exceptionThrown = await Assert.ThrowsAsync(() => awsResourceQueryer.GetCallerIdentity("ap-southeast-3")); + Assert.Equal(DeployToolErrorCode.OptInRegionDisabled, exceptionThrown.ErrorCode); + } + + [Fact] + public async Task GetCallerIdentity_BadConnection() + { + var awsResourceQueryer = new AWSResourceQueryer(_mockAWSClientFactory.Object); + var stsResponse = new GetCallerIdentityResponse(); + + _mockAWSClientFactory.Setup(x => x.GetAWSClient("ap-southeast-3")).Returns(_mockSTSClient.Object); + _mockSTSClient.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Invalid token")); + + _mockAWSClientFactory.Setup(x => x.GetAWSClient("us-east-1")).Returns(_mockSTSClientDefaultRegion.Object); + _mockSTSClientDefaultRegion.Setup(x => x.GetCallerIdentityAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Invalid token")); + + var exceptionThrown = await Assert.ThrowsAsync(() => awsResourceQueryer.GetCallerIdentity("ap-southeast-3")); + Assert.Equal(DeployToolErrorCode.UnableToAccessAWSRegion, exceptionThrown.ErrorCode); + } + } +} From ba361b626ffcab66e0bd94ca007b047128e08167 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 1 Feb 2022 13:07:09 -0500 Subject: [PATCH 8/8] feat: capture standard error in server mode session --- .../CommandLineWrapper.cs | 54 ++++++++++++++++++- .../ServerModeSession.cs | 7 ++- .../Utilities/CappedStringBuilder.cs | 48 +++++++++++++++++ .../CappedStringBuilderTests.cs | 41 ++++++++++++++ .../ServerModeSessionTests.cs | 38 ++++++++----- 5 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs create mode 100644 test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs diff --git a/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs b/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs index d5ce6629c..a803804d1 100644 --- a/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs +++ b/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; +using AWS.Deploy.ServerMode.Client.Utilities; namespace AWS.Deploy.ServerMode.Client { @@ -19,16 +21,21 @@ public CommandLineWrapper(bool diagnosticLoggingEnabled) _diagnosticLoggingEnabled = diagnosticLoggingEnabled; } - public virtual async Task Run(string command, params string[] stdIn) + public virtual async Task Run(string command, params string[] stdIn) { var arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c {command}" : $"-c \"{command}\""; + var strOutput = new CappedStringBuilder(100); + var strError = new CappedStringBuilder(50); + var processStartInfo = new ProcessStartInfo { FileName = GetSystemShell(), Arguments = arguments, UseShellExecute = false, // UseShellExecute must be false in allow redirection of StdIn. RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, CreateNoWindow = !_diagnosticLoggingEnabled, // It allows displaying stdout and stderr on the screen. }; @@ -43,9 +50,28 @@ public virtual async Task Run(string command, params string[] stdIn) await process.StandardInput.WriteLineAsync(line).ConfigureAwait(false); } + process.OutputDataReceived += (sender, e) => + { + strOutput.AppendLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + strError.AppendLine(e.Data); + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(-1); - return await Task.FromResult(process.ExitCode).ConfigureAwait(false); + var result = new RunResult + { + ExitCode = process.ExitCode, + StandardError = strError.ToString(), + StandardOut = strOutput.GetLastLines(5), + }; + + return await Task.FromResult(result).ConfigureAwait(false); } private string GetSystemShell() @@ -70,4 +96,28 @@ private bool TryGetEnvironmentVariable(string variable, out string? value) return !string.IsNullOrEmpty(value); } } + + public class RunResult + { + /// + /// Indicates if this command was run successfully. This checks that + /// is empty. + /// + public bool Success => string.IsNullOrWhiteSpace(StandardError); + + /// + /// Fully read + /// + public string StandardOut { get; set; } = string.Empty; + + /// + /// Fully read + /// + public string StandardError { get; set; } = string.Empty; + + /// + /// Fully read + /// + public int ExitCode { get; set; } + } } diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 84667a9c9..1f220ef9f 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -164,9 +164,12 @@ public async Task Start(CancellationToken cancellationToken) // For -100 errors, we want to check all the ports in the configured port range // If the error code other than -100, this is an unexpected exit code. - if (startServerTask.Result != TCP_PORT_ERROR) + if (startServerTask.Result.ExitCode != TCP_PORT_ERROR) { - throw new InternalServerModeException($"\"{command}\" failed for unknown reason."); + throw new InternalServerModeException( + string.IsNullOrEmpty(startServerTask.Result.StandardError) ? + $"\"{command}\" failed for unknown reason." : + startServerTask.Result.StandardError); } } diff --git a/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs b/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs new file mode 100644 index 000000000..954757744 --- /dev/null +++ b/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AWS.Deploy.ServerMode.Client.Utilities +{ + public class CappedStringBuilder + { + public int LineLimit { get; } + public int LineCount { + get + { + return _lines?.Count ?? 0; + } + } + + private readonly Queue _lines; + + public CappedStringBuilder(int lineLimit) + { + _lines = new Queue(lineLimit); + LineLimit = lineLimit; + } + + public void AppendLine(string value) + { + if (LineCount >= LineLimit) + { + _lines.Dequeue(); + } + + _lines.Enqueue(value); + } + + public string GetLastLines(int lineCount) + { + return _lines.Reverse().Take(lineCount).Reverse().Aggregate((x, y) => x + Environment.NewLine + y); + } + + public override string ToString() + { + return _lines.Aggregate((x, y) => x + Environment.NewLine + y); + } + } +} diff --git a/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs b/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs new file mode 100644 index 000000000..7fc1e34d8 --- /dev/null +++ b/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using AWS.Deploy.ServerMode.Client.Utilities; +using Xunit; + +namespace AWS.Deploy.ServerMode.Client.UnitTests +{ + public class CappedStringBuilderTests + { + private readonly CappedStringBuilder _cappedStringBuilder; + + public CappedStringBuilderTests() + { + _cappedStringBuilder = new CappedStringBuilder(5); + } + + [Fact] + public void AppendLineTest() + { + _cappedStringBuilder.AppendLine("test1"); + _cappedStringBuilder.AppendLine("test2"); + _cappedStringBuilder.AppendLine("test3"); + + Assert.Equal(3, _cappedStringBuilder.LineCount); + Assert.Equal($"test1{Environment.NewLine}test2{Environment.NewLine}test3", _cappedStringBuilder.ToString()); + } + + [Fact] + public void GetLastLinesTest() + { + _cappedStringBuilder.AppendLine("test1"); + _cappedStringBuilder.AppendLine("test2"); + + Assert.Equal(2, _cappedStringBuilder.LineCount); + Assert.Equal("test2", _cappedStringBuilder.GetLastLines(1)); + Assert.Equal($"test1{Environment.NewLine}test2", _cappedStringBuilder.GetLastLines(2)); + } + } +} diff --git a/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs b/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs index 5e2317a3b..d9444c71d 100644 --- a/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs +++ b/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs @@ -27,7 +27,8 @@ public ServerModeSessionTests() public async Task Start() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); // Act @@ -44,7 +45,8 @@ public async Task Start() public async Task Start_PortUnavailable() { // Arrange - MockCommandLineWrapperRun(-100); + var runResult = new RunResult { ExitCode = -100 }; + MockCommandLineWrapperRun(runResult); MockHttpGet(HttpStatusCode.NotFound, TimeSpan.FromSeconds(5)); // Act & Assert @@ -58,7 +60,8 @@ await Assert.ThrowsAsync(async () => public async Task Start_HttpGetThrows() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGetThrows(); // Act & Assert @@ -72,7 +75,8 @@ await Assert.ThrowsAsync(async () => public async Task Start_HttpGetForbidden() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.Forbidden); // Act & Assert @@ -96,7 +100,8 @@ public async Task IsAlive_BaseUrlNotInitialized() public async Task IsAlive_GetAsyncThrows() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -113,7 +118,8 @@ public async Task IsAlive_GetAsyncThrows() public async Task IsAlive_HttpResponseSuccess() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -128,7 +134,8 @@ public async Task IsAlive_HttpResponseSuccess() public async Task IsAlive_HttpResponseFailure() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -145,7 +152,8 @@ public async Task IsAlive_HttpResponseFailure() public async Task TryGetRestAPIClient() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -161,7 +169,8 @@ public async Task TryGetRestAPIClient() public void TryGetRestAPIClient_WithoutStart() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); // Act @@ -176,7 +185,8 @@ public void TryGetRestAPIClient_WithoutStart() public async Task TryGetDeploymentCommunicationClient() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -228,15 +238,15 @@ private void MockHttpGetThrows() => ItExpr.IsAny()) .Throws(new Exception()); - private void MockCommandLineWrapperRun(int statusCode) => + private void MockCommandLineWrapperRun(RunResult runResult) => _commandLineWrapper .Setup(wrapper => wrapper.Run(It.IsAny(), It.IsAny())) - .ReturnsAsync(statusCode); + .ReturnsAsync(runResult); - private void MockCommandLineWrapperRun(int statusCode, TimeSpan delay) => + private void MockCommandLineWrapperRun(RunResult runResult, TimeSpan delay) => _commandLineWrapper .Setup(wrapper => wrapper.Run(It.IsAny(), It.IsAny())) - .ReturnsAsync(statusCode, delay); + .ReturnsAsync(runResult, delay); private Task CredentialGenerator() => throw new NotImplementedException(); }