From fc31455874f72a6a81d5b5fc02b4f8155bb122d3 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Mon, 16 Aug 2021 14:51:13 -0400 Subject: [PATCH 01/13] fix: Correctly resolve the solution path while parsing the project definition on non-windows targets. --- .../ProjectDefinitionParser.cs | 11 +++- .../DeploymentBundleHandler.cs | 50 +------------------ .../ProjectDefinitionParserTest.cs | 4 ++ .../Utilities/System.IO.Utilties.cs | 11 ++++ 4 files changed, 26 insertions(+), 50 deletions(-) diff --git a/src/AWS.Deploy.Common/ProjectDefinitionParser.cs b/src/AWS.Deploy.Common/ProjectDefinitionParser.cs index 2d57257f5..31885fd80 100644 --- a/src/AWS.Deploy.Common/ProjectDefinitionParser.cs +++ b/src/AWS.Deploy.Common/ProjectDefinitionParser.cs @@ -126,7 +126,16 @@ private async Task ValidateProjectInSolution(string projectPath, string so var lines = await _fileManager.ReadAllLinesAsync(solutionFile); var projectLines = lines.Where(x => x.StartsWith("Project")); - var projectPaths = projectLines.Select(x => x.Split(',')[1].Replace('\"', ' ').Trim()).ToList(); + var projectPaths = + projectLines + .Select(x => x.Split(',')) + .Where(x => x.Length > 1) + .Select(x => + x[1] + .Replace('\"', ' ') + .Trim()) + .Select(x => x.Replace('\\', Path.DirectorySeparatorChar)) + .ToList(); //Validate project exists in solution return projectPaths.Select(x => Path.GetFileName(x)).Any(x => x.Equals(projectFileName)); diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 49bf6bbeb..eb96e213e 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -144,7 +144,7 @@ private string GetDockerExecutionDirectory(Recommendation recommendation) var dockerFileDirectory = new FileInfo(recommendation.ProjectPath).Directory?.FullName; if (dockerFileDirectory == null) throw new InvalidProjectPathException("The project path is invalid."); - var projectSolutionPath = GetProjectSolutionFile(recommendation.ProjectPath); + var projectSolutionPath = recommendation.ProjectDefinition.ProjectSolutionPath; if (string.IsNullOrEmpty(dockerExecutionDirectory)) { @@ -171,54 +171,6 @@ private string GetDockerFilePath(Recommendation recommendation) return Path.Combine(dockerFileDirectory, "Dockerfile"); } - private string GetProjectSolutionFile(string projectPath) - { - var projectDirectory = Directory.GetParent(projectPath); - var solutionExists = false; - - while (solutionExists == false && projectDirectory != null) - { - var files = projectDirectory.GetFiles("*.sln"); - foreach (var solutionFile in files) - { - if (ValidateProjectInSolution(projectPath, solutionFile.FullName)) - { - return solutionFile.FullName; - } - } - projectDirectory = projectDirectory.Parent; - } - - return string.Empty; - } - - private bool ValidateProjectInSolution(string projectPath, string solutionFile) - { - var projectFileName = Path.GetFileName(projectPath); - - if (string.IsNullOrWhiteSpace(solutionFile) || - string.IsNullOrWhiteSpace(projectFileName)) - { - return false; - } - - var lines = File.ReadAllLines(solutionFile); - var projectLines = lines.Where(x => x.StartsWith("Project")); - var projectPaths = - projectLines - .Select(x => x.Split(',')) - .Where(x => x.Length > 1) - .Select(x => - x[1] - .Replace('\"', ' ') - .Trim()) - .Select(x => x.Replace('\\', Path.DirectorySeparatorChar)) - .ToList(); - - //Validate project exists in solution - return projectPaths.Any(x => Path.GetFileName(x).Equals(projectFileName)); - } - private string GetDockerBuildArgs(Recommendation recommendation) { var buildArgs = string.Empty; diff --git a/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs b/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs index a1f34a8eb..13f50faa3 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs @@ -35,6 +35,7 @@ public async Task ParseProjectDefinitionWithRelativeProjectPath(string projectNa var absoluteProjectDirectoryPath = new DirectoryInfo(projectDirectoryPath).FullName; var absoluteProjectPath = Path.Combine(absoluteProjectDirectoryPath, csprojName); var relativeProjectDirectoryPath = Path.GetRelativePath(currrentWorkingDirectory, absoluteProjectDirectoryPath); + var projectSolutionPath = SystemIOUtilities.ResolvePathToSolution(); // Act var projectDefinition = await new ProjectDefinitionParser(new FileManager(), new DirectoryManager()).Parse(relativeProjectDirectoryPath); @@ -42,6 +43,7 @@ public async Task ParseProjectDefinitionWithRelativeProjectPath(string projectNa // Assert projectDefinition.ShouldNotBeNull(); Assert.Equal(absoluteProjectPath, projectDefinition.ProjectPath); + Assert.Equal(projectSolutionPath, projectDefinition.ProjectSolutionPath); } [Theory] @@ -60,6 +62,7 @@ public async Task ParseProjectDefinitionWithAbsoluteProjectPath(string projectNa var projectDirectoryPath = SystemIOUtilities.ResolvePath(projectName); var absoluteProjectDirectoryPath = new DirectoryInfo(projectDirectoryPath).FullName; var absoluteProjectPath = Path.Combine(absoluteProjectDirectoryPath, csprojName); + var projectSolutionPath = SystemIOUtilities.ResolvePathToSolution(); // Act var projectDefinition = await new ProjectDefinitionParser(new FileManager(), new DirectoryManager()).Parse(absoluteProjectDirectoryPath); @@ -67,6 +70,7 @@ public async Task ParseProjectDefinitionWithAbsoluteProjectPath(string projectNa // Assert projectDefinition.ShouldNotBeNull(); Assert.Equal(absoluteProjectPath, projectDefinition.ProjectPath); + Assert.Equal(projectSolutionPath, projectDefinition.ProjectSolutionPath); } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/System.IO.Utilties.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/System.IO.Utilties.cs index ca484ff8a..2cf69fab0 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/System.IO.Utilties.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/System.IO.Utilties.cs @@ -20,5 +20,16 @@ public static string ResolvePath(string projectName) return Path.Combine(testsPath, "..", "testapps", projectName); } + public static string ResolvePathToSolution() + { + var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + while (path != null && !string.Equals(new DirectoryInfo(path).Name, "aws-dotnet-deploy", StringComparison.OrdinalIgnoreCase)) + { + path = Directory.GetParent(path).FullName; + } + + return new DirectoryInfo(Path.Combine(path, "AWS.Deploy.sln")).FullName; + } + } } From 6376f6dd54aa3327f827b288e36ca8066ae0fcd6 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Mon, 16 Aug 2021 19:20:13 +0000 Subject: [PATCH 02/13] build: version bump to 0.17 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a9d6f1ac6..0cefc4d15 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.16", + "version": "0.17", "publicReleaseRefSpec": [ ".*" ], From 55759aa35f1ad16ccc22cce27f8ad13fda762631 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Tue, 17 Aug 2021 10:29:15 -0700 Subject: [PATCH 03/13] fix: replace ErrorCode with Throttling This change also change polling period to 5 seconds for both DeleteDeploymentCommand and StackEventMonitor --- .../CloudFormation/StackEventMonitor.cs | 2 +- .../Commands/DeleteDeploymentCommand.cs | 4 +-- .../Services/InMemoryInteractiveService.cs | 32 ++++++++++++++++++- .../InMemoryInteractiveServiceTests.cs | 22 +++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/AWS.Deploy.CLI/CloudFormation/StackEventMonitor.cs b/src/AWS.Deploy.CLI/CloudFormation/StackEventMonitor.cs index 4569786d5..b4b130402 100644 --- a/src/AWS.Deploy.CLI/CloudFormation/StackEventMonitor.cs +++ b/src/AWS.Deploy.CLI/CloudFormation/StackEventMonitor.cs @@ -23,7 +23,7 @@ internal class StackEventMonitor private const int RESOURCE_STATUS_WIDTH = 20; private const int RESOURCE_TYPE_WIDTH = 40; private const int LOGICAL_RESOURCE_WIDTH = 40; - private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(2); + private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5); private readonly string _stackName; private bool _isActive; diff --git a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs index 8a7942e47..f35db27ba 100644 --- a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs @@ -19,7 +19,7 @@ namespace AWS.Deploy.CLI.Commands /// public class DeleteDeploymentCommand { - private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(1); + private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5); private readonly IAWSClientFactory _awsClientFactory; private readonly IToolInteractiveService _interactiveService; @@ -173,7 +173,7 @@ private async Task WaitForStackDelete(string stackName) { shouldRetry = false; } - catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ThrottlingException")) + catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("Throttling")) { _interactiveService.WriteDebugLine(exception.PrettyPrint()); shouldRetry = true; diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs index 5e6663c89..078629b60 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs @@ -11,9 +11,11 @@ public class InMemoryInteractiveService : IToolInteractiveService { private long _stdOutWriterPosition; private long _stdErrorWriterPosition; + private long _stdDebugWriterPosition; private long _stdInReaderPosition; private readonly StreamWriter _stdOutWriter; private readonly StreamWriter _stdErrorWriter; + private readonly StreamWriter _stdDebugWriter; private readonly StreamReader _stdInReader; /// @@ -32,6 +34,11 @@ public class InMemoryInteractiveService : IToolInteractiveService /// public StreamReader StdErrorReader { get; } + /// + /// Allows consumers to read string which is written via + /// + public StreamReader StdDebugReader { get; } + public InMemoryInteractiveService() { var stdOut = new MemoryStream(); @@ -42,6 +49,10 @@ public InMemoryInteractiveService() _stdErrorWriter = new StreamWriter(stdError); StdErrorReader = new StreamReader(stdError); + var stdDebug = new MemoryStream(); + _stdDebugWriter = new StreamWriter(stdDebug); + StdDebugReader = new StreamReader(stdDebug); + var stdIn = new MemoryStream(); _stdInReader = new StreamReader(stdIn); StdInWriter = new StreamWriter(stdIn); @@ -89,7 +100,26 @@ public void WriteLine(string message) StdOutReader.BaseStream.Position = stdOutReaderPosition; } - public void WriteDebugLine(string message) => throw new System.NotImplementedException(); + public void WriteDebugLine(string message) + { + Console.WriteLine(message); + Debug.WriteLine(message); + + // Save BaseStream position, it must be only modified by the consumer of StdDebugReader + // After writing to the BaseStream, we will reset it to the original position. + var stdDebugReaderPosition = StdDebugReader.BaseStream.Position; + + // Reset the BaseStream to the last save position to continue writing from where we left. + _stdDebugWriter.BaseStream.Position = _stdDebugWriterPosition; + _stdDebugWriter.WriteLine(message); + _stdDebugWriter.Flush(); + + // Save the BaseStream position for future writes. + _stdDebugWriterPosition = _stdDebugWriter.BaseStream.Position; + + // Reset the BaseStream position to the original position + StdDebugReader.BaseStream.Position = stdDebugReaderPosition; + } public void WriteErrorLine(string message) { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs index 0bcd67878..88b440191 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Services/InMemoryInteractiveServiceTests.cs @@ -67,6 +67,28 @@ public void WriteErrorLine() Assert.Equal("Error Line 6", service.StdErrorReader.ReadLine()); } + [Fact] + public void WriteDebugLine() + { + var service = new InMemoryInteractiveService(); + + service.WriteDebugLine("Debug Line 1"); + service.WriteDebugLine("Debug Line 2"); + service.WriteDebugLine("Debug Line 3"); + + Assert.Equal("Debug Line 1", service.StdDebugReader.ReadLine()); + Assert.Equal("Debug Line 2", service.StdDebugReader.ReadLine()); + Assert.Equal("Debug Line 3", service.StdDebugReader.ReadLine()); + + service.WriteDebugLine("Debug Line 4"); + service.WriteDebugLine("Debug Line 5"); + service.WriteDebugLine("Debug Line 6"); + + Assert.Equal("Debug Line 4", service.StdDebugReader.ReadLine()); + Assert.Equal("Debug Line 5", service.StdDebugReader.ReadLine()); + Assert.Equal("Debug Line 6", service.StdDebugReader.ReadLine()); + } + [Fact] public void ReadLine() { From d729210ff99c12ffe5c835a379c1979e3ce292d8 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 5 Aug 2021 00:23:05 -0700 Subject: [PATCH 04/13] feat: Make ASP.NET Core ECS recipe customizable --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 4 + .../ExistingApplicationLoadBalancerCommand.cs | 42 ++ .../TypeHints/TypeHintCommandFactory.cs | 1 + src/AWS.Deploy.CLI/ConsoleUtilities.cs | 4 +- .../Recipes/OptionSettingTypeHint.cs | 1 + .../Recipes/OptionSettingValueType.cs | 1 + .../CdkAppSettingsSerializer.cs | 2 +- .../Data/AWSResourceQueryer.cs | 18 + .../CDKRecipeCustomizer.cs | 69 ++++ .../CDKRecipeSetup.cs | 47 ++- .../DeployToolStackProps.cs | 38 ++ .../RecipeConfiguration.cs | 3 + .../RecipeProps.cs | 153 +++++++ .../AspNetAppEcsFargate/AppStack.cs | 156 ++----- .../AspNetAppEcsFargate.csproj | 4 +- .../Configurations/Configuration.cs | 68 +--- .../Configurations/IAMRoleConfiguration.cs | 19 - .../AutoScalingConfiguration.cs | 48 +++ .../Generated/Configurations/Configuration.cs | 87 ++++ .../Configurations/ECSClusterConfiguration.cs | 8 +- .../Configurations/IAMRoleConfiguration.cs | 25 ++ .../LoadBalancerConfiguration.cs | 85 ++++ .../Configurations/VpcConfiguration.cs | 8 +- .../AspNetAppEcsFargate/Generated/Recipe.cs | 342 ++++++++++++++++ .../AspNetAppEcsFargate/Program.cs | 17 +- .../AspNetAppEcsFargate/README.md | 59 ++- .../AspNetAppEcsFargate/appsettings.json | 3 - .../ASP.NETAppECSFargate.recipe | 385 +++++++++++++++++- .../ECSFargateDeploymentTest.cs | 5 +- .../Utilities/TestToolAWSResourceQueryer.cs | 2 + .../WebAppWithDockerFileTests.cs | 5 +- .../Utilities/TestToolAWSResourceQueryer.cs | 3 + .../DisplayedResourcesHandlerTests.cs | 2 +- 33 files changed, 1480 insertions(+), 234 deletions(-) create mode 100644 src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs create mode 100644 src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeCustomizer.cs create mode 100644 src/AWS.Deploy.Recipes.CDK.Common/DeployToolStackProps.cs create mode 100644 src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs delete mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/IAMRoleConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/AutoScalingConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs rename src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/{ => Generated}/Configurations/ECSClusterConfiguration.cs (72%) create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/IAMRoleConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs rename src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/{ => Generated}/Configurations/VpcConfiguration.cs (70%) create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs delete mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/appsettings.json diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index c9d701b28..5f5ddae3c 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -337,6 +337,9 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us case OptionSettingValueType.Bool: settingValue = bool.Parse(optionSettingValue); break; + case OptionSettingValueType.Double: + settingValue = double.Parse(optionSettingValue); + break; default: throw new InvalidOverrideValueException($"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); } @@ -710,6 +713,7 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, Opt { case OptionSettingValueType.String: case OptionSettingValueType.Int: + case OptionSettingValueType.Double: settingValue = _consoleUtilities.AskUserForValue(string.Empty, currentValue.ToString() ?? "", allowEmpty: true, resetValue: recommendation.GetOptionSettingDefaultValue(setting) ?? ""); break; case OptionSettingValueType.Bool: diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs new file mode 100644 index 000000000..54708d487 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading.Tasks; +using Amazon.ElasticLoadBalancingV2; +using Amazon.ElasticLoadBalancingV2.Model; +using AWS.Deploy.CLI.TypeHintResponses; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Orchestration.Data; + +namespace AWS.Deploy.CLI.Commands.TypeHints +{ + public class ExistingApplicationLoadBalancerCommand : ITypeHintCommand + { + private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly IConsoleUtilities _consoleUtilities; + + public ExistingApplicationLoadBalancerCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities) + { + _awsResourceQueryer = awsResourceQueryer; + _consoleUtilities = consoleUtilities; + } + + public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + var loadBalancers = await _awsResourceQueryer.ListOfLoadBalancers(LoadBalancerTypeEnum.Application); + var currentValue = recommendation.GetOptionSettingValue(optionSetting); + + var userInputConfiguration = new UserInputConfiguration( + loadBalancer => loadBalancer.LoadBalancerName, + loadBalancer => loadBalancer.LoadBalancerArn.Equals(currentValue)) + { + AskNewName = false + }; + + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(loadBalancers, "Select Load Balancer to deploy to:", userInputConfiguration); + + return userResponse.SelectedOption?.LoadBalancerArn ?? string.Empty; + } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index 9e8fd6d74..3775c664e 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -45,6 +45,7 @@ public TypeHintCommandFactory(IToolInteractiveService toolInteractiveService, IA { OptionSettingTypeHint.DockerExecutionDirectory, new DockerExecutionDirectoryCommand(consoleUtilities) }, { OptionSettingTypeHint.DockerBuildArgs, new DockerBuildArgsCommand(consoleUtilities) }, { OptionSettingTypeHint.ECSCluster, new ECSClusterCommand(awsResourceQueryer, consoleUtilities) }, + { OptionSettingTypeHint.ExistingApplicationLoadBalancer, new ExistingApplicationLoadBalancerCommand(awsResourceQueryer, consoleUtilities) }, }; } diff --git a/src/AWS.Deploy.CLI/ConsoleUtilities.cs b/src/AWS.Deploy.CLI/ConsoleUtilities.cs index 7bbde9a3d..57013da44 100644 --- a/src/AWS.Deploy.CLI/ConsoleUtilities.cs +++ b/src/AWS.Deploy.CLI/ConsoleUtilities.cs @@ -410,9 +410,9 @@ public void DisplayValues(Dictionary objectValues, string indent _interactiveService.WriteLine($"{indent}{key}: {stringValue}"); } } - else if (value is bool boolValue) + else if(value != null) { - _interactiveService.WriteLine($"{indent}{key}: {boolValue}"); + _interactiveService.WriteLine($"{indent}{key}: {value}"); } } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs index 8ecd23ab6..aa66d603b 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs @@ -14,6 +14,7 @@ public enum OptionSettingTypeHint ECSTaskSchedule, EC2KeyPair, Vpc, + ExistingApplicationLoadBalancer, DotnetBeanstalkPlatformArn, DotnetPublishSelfContainedBuild, DotnetPublishBuildConfiguration, diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingValueType.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingValueType.cs index 836fad622..2ace088be 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingValueType.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingValueType.cs @@ -7,6 +7,7 @@ public enum OptionSettingValueType { String, Int, + Double, Bool, Object }; diff --git a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs index 6e495bb99..48e1d0d48 100644 --- a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs +++ b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs @@ -18,7 +18,7 @@ public string Build(CloudApplication cloudApplication, Recommendation recommenda throw new InvalidProjectPathException("The project path provided is invalid."); // General Settings - var appSettingsContainer = new RecipeConfiguration>( + var appSettingsContainer = new RecipeProps>( cloudApplication.StackName, projectPath, recommendation.Recipe.Id, diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index ef29a7742..c00c483d3 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -52,6 +52,7 @@ public interface IAWSResourceQueryer Task CreateECRRepository(string repositoryName); Task> GetCloudFormationStacks(); Task GetCallerIdentity(); + Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType); } public class AWSResourceQueryer : IAWSResourceQueryer @@ -387,5 +388,22 @@ public async Task GetCallerIdentity() using var stsClient = _awsClientFactory.GetAWSClient(); return await stsClient.GetCallerIdentityAsync(new GetCallerIdentityRequest()); } + + public async Task> ListOfLoadBalancers(Amazon.ElasticLoadBalancingV2.LoadBalancerTypeEnum loadBalancerType) + { + var client = _awsClientFactory.GetAWSClient(); + + var loadBalancers = new List(); + + await foreach(var loadBalancer in client.Paginators.DescribeLoadBalancers(new DescribeLoadBalancersRequest()).LoadBalancers) + { + if(loadBalancer.Type == loadBalancerType) + { + loadBalancers.Add(loadBalancer); + } + } + + return loadBalancers; + } } } diff --git a/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeCustomizer.cs b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeCustomizer.cs new file mode 100644 index 000000000..bac55cb7f --- /dev/null +++ b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeCustomizer.cs @@ -0,0 +1,69 @@ +// 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.Recipes.CDK.Common +{ + public delegate void CustomizePropsDelegate(CustomizePropsEventArgs evnt) where GeneratedConstruct : Amazon.CDK.Construct; + + /// + /// Event object created after each CDK constructor properties object is created in the recipe's container construct. + /// The CDK properties object can be further customized before the property object is passed into the CDK construct + /// making the properties immutable. + /// + /// + public class CustomizePropsEventArgs where GeneratedConstruct : Amazon.CDK.Construct + { + /// + /// The CDK props object about to be used to create the CDK construct + /// + public object Props { get; } + + /// + /// The CDK logical name of the CDK construct about to be created. + /// + public string ResourceLogicalName { get; } + + /// + /// The container construct for all of the CDK constructs that are part of the generated CDK project. + /// + public GeneratedConstruct Construct { get; } + + + public CustomizePropsEventArgs(object props, string resourceLogicalName, GeneratedConstruct construct) + { + Props = props; + ResourceLogicalName = resourceLogicalName; + Construct = construct; + } + } + + public class CDKRecipeCustomizer where GeneratedConstruct : Amazon.CDK.Construct + { + /// + /// Event called whenever a CDK construct property object is created. Subscribers to the event can customize + /// the property object before it is passed into the CDK construct + /// making the properties immutable. + /// + public static event CustomizePropsDelegate? CustomizeCDKProps; + + /// + /// Utility method used in recipes to trigger the CustomizeCDKProps event. + /// + /// + /// + /// + /// + /// + public static T InvokeCustomizeCDKPropsEvent(string resourceLogicalName, GeneratedConstruct construct, T props) where T : class + { + var handler = CustomizeCDKProps; + handler?.Invoke(new CustomizePropsEventArgs(props, resourceLogicalName, construct)); + + return props; + } + } +} diff --git a/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs index 9fb7854dc..06c5e2d3d 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs @@ -21,7 +21,7 @@ public static class CDKRecipeSetup /// The configuration type defined as part of the recipe that contains all of the recipe specific settings. /// /// - public static void RegisterStack(Stack stack, RecipeConfiguration recipeConfiguration) + public static void RegisterStack(Stack stack, IRecipeProps recipeConfiguration) { // Set the AWS .NET deployment tool tag which also identifies the recipe used. stack.Tags.SetTag(Constants.CloudFormationIdentifier.STACK_TAG, $"{recipeConfiguration.RecipeId}"); @@ -59,5 +59,50 @@ public static void RegisterStack(Stack stack, RecipeConfiguration recipeCo stack.TemplateOptions.Description = $"{Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX}: {stack.TemplateOptions.Description}"; } } + + /// + /// TODO remove this function once all recipes have migrated to IRecipeProps + /// + /// + /// + /// + public static void RegisterStack(Stack stack, RecipeConfiguration recipeConfiguration) + { + // Set the AWS .NET deployment tool tag which also identifies the recipe used. + stack.Tags.SetTag(Constants.CloudFormationIdentifier.STACK_TAG, $"{recipeConfiguration.RecipeId}"); + + // Serializes all AWS .NET deployment tool settings. + var json = JsonSerializer.Serialize(recipeConfiguration.Settings, new JsonSerializerOptions { WriteIndented = false }); + + Dictionary metadata; + if (stack.TemplateOptions.Metadata?.Count > 0) + { + metadata = new Dictionary(stack.TemplateOptions.Metadata); + } + else + { + metadata = new Dictionary(); + } + + // Save the settings, recipe id and version as metadata to the CloudFormation template. + metadata[Constants.CloudFormationIdentifier.STACK_METADATA_SETTINGS] = json; + metadata[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_ID] = recipeConfiguration.RecipeId; + metadata[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_VERSION] = recipeConfiguration.RecipeVersion; + + // For the CDK to pick up the changes to the metadata .NET Dictionary you have to reassign the Metadata property. + stack.TemplateOptions.Metadata = metadata; + + // CloudFormation tags are propagated to resources created by the stack. In case of Beanstalk deployment a second CloudFormation stack is + // launched which will also have the AWS .NET deployment tool tag. To differentiate these additional stacks a special AWS .NET deployment tool prefix + // is added to the description. + if (string.IsNullOrEmpty(stack.TemplateOptions.Description)) + { + stack.TemplateOptions.Description = Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX; + } + else + { + stack.TemplateOptions.Description = $"{Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX}: {stack.TemplateOptions.Description}"; + } + } } } diff --git a/src/AWS.Deploy.Recipes.CDK.Common/DeployToolStackProps.cs b/src/AWS.Deploy.Recipes.CDK.Common/DeployToolStackProps.cs new file mode 100644 index 000000000..a750cfdfd --- /dev/null +++ b/src/AWS.Deploy.Recipes.CDK.Common/DeployToolStackProps.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; + +namespace AWS.Deploy.Recipes.CDK.Common +{ + /// + /// Properties to be passed into a CDK stack used by the deploy tool. This object contains all of the configuration properties specified by the + /// deploy tool recipe. + /// + /// + public interface IDeployToolStackProps : Amazon.CDK.IStackProps + { + /// + /// The user specified settings that are defined as part of the deploy tool recipe. + /// + IRecipeProps RecipeProps { get; set; } + } + + /// + /// Properties to be passed into a CDK stack used by the deploy tool. This object contains all of the configuration properties specified by the + /// deploy tool recipe. + /// + /// + public class DeployToolStackProps : Amazon.CDK.StackProps, IDeployToolStackProps + { + public IRecipeProps RecipeProps { get; set; } + + public DeployToolStackProps(IRecipeProps props) + { + RecipeProps = props; + StackName = props.StackName; + } + } +} diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs index 136718dfd..f58a9b56b 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs @@ -5,6 +5,9 @@ namespace AWS.Deploy.Recipes.CDK.Common { + // TODO, remove this class once all recipes are migrated to IRecipeProps. + + /// /// A representation of the settings transferred from the AWS .NET deployment tool to the CDK project. /// diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs new file mode 100644 index 000000000..1b02f26ae --- /dev/null +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs @@ -0,0 +1,153 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json.Serialization; + +namespace AWS.Deploy.Recipes.CDK.Common +{ + /// + /// A representation of the settings transferred from the AWS .NET deployment tool to the CDK project. + /// + /// + public interface IRecipeProps + { + /// + /// The name of the CloudFormation stack + /// + public string StackName { get; set; } + + /// + /// The path to the .NET project to deploy to AWS. + /// + public string ProjectPath { get; set; } + + /// + /// The ECR Repository Name where the docker image will be pushed to. + /// + public string? ECRRepositoryName { get; set; } + + /// + /// The ECR Image Tag of the docker image. + /// + public string? ECRImageTag { get; set; } + + /// + /// The path of the zip file containing the assemblies produced by the dotnet publish command. + /// + public string? DotnetPublishZipPath { get; set; } + + /// + /// The directory containing the assemblies produced by the dotnet publish command. + /// + public string? DotnetPublishOutputDirectory { get; set; } + + /// + /// The ID of the recipe being used to deploy the application. + /// + public string RecipeId { get; set; } + + /// + /// The version of the recipe being used to deploy the application. + /// + public string RecipeVersion { get; set; } + + /// + /// The configured settings made by the frontend. These are recipe specific and defined in the recipe's definition. + /// + public T Settings { get; set; } + + /// + /// The Region used during deployment. + /// + public string? AWSRegion { get; set; } + + /// + /// The account ID used during deployment. + /// + public string? AWSAccountId { get; set; } + } + + /// + /// A representation of the settings transferred from the AWS .NET deployment tool to the CDK project. + /// + /// + public class RecipeProps : IRecipeProps + { + /// + /// The name of the CloudFormation stack + /// + public string StackName { get; set; } + + /// + /// The path to the .NET project to deploy to AWS. + /// + public string ProjectPath { get; set; } + + /// + /// The ECR Repository Name where the docker image will be pushed to. + /// + public string? ECRRepositoryName { get; set; } + + /// + /// The ECR Image Tag of the docker image. + /// + public string? ECRImageTag { get; set; } + + /// + /// The path of the zip file containing the assemblies produced by the dotnet publish command. + /// + public string? DotnetPublishZipPath { get; set; } + + /// + /// The directory containing the assemblies produced by the dotnet publish command. + /// + public string? DotnetPublishOutputDirectory { get; set; } + + /// + /// The ID of the recipe being used to deploy the application. + /// + public string RecipeId { get; set; } + + /// + /// The version of the recipe being used to deploy the application. + /// + public string RecipeVersion { get; set; } + + /// + /// The configured settings made by the frontend. These are recipe specific and defined in the recipe's definition. + /// + public T Settings { get; set; } + + /// + /// The Region used during deployment. + /// + public string? AWSRegion { get; set; } + + /// + /// The account ID used during deployment. + /// + public string? AWSAccountId { get; set; } + + /// A parameterless constructor is needed for + /// or the classes will fail to initialize. + /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. +#nullable disable warnings + public RecipeProps() + { + + } +#nullable restore warnings + + public RecipeProps(string stackName, string projectPath, string recipeId, string recipeVersion, + string? awsAccountId, string? awsRegion, T settings) + { + StackName = stackName; + ProjectPath = projectPath; + RecipeId = recipeId; + RecipeVersion = recipeVersion; + AWSAccountId = awsAccountId; + AWSRegion = awsRegion; + Settings = settings; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AppStack.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AppStack.cs index 3f44f813d..bbd0b2b23 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AppStack.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AppStack.cs @@ -1,139 +1,59 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; +using System.Collections.Generic; using Amazon.CDK; -using Amazon.CDK.AWS.EC2; using Amazon.CDK.AWS.ECS; -using Amazon.CDK.AWS.ECS.Patterns; -using Amazon.CDK.AWS.IAM; using AWS.Deploy.Recipes.CDK.Common; + using AspNetAppEcsFargate.Configurations; -using Protocol = Amazon.CDK.AWS.ECS.Protocol; -using Amazon.CDK.AWS.ECR; -using System.Collections.Generic; namespace AspNetAppEcsFargate { public class AppStack : Stack { - internal AppStack(Construct scope, RecipeConfiguration recipeConfiguration, IStackProps? props = null) - : base(scope, recipeConfiguration.StackName, props) + internal AppStack(Construct scope, IDeployToolStackProps props) + : base(scope, props.StackName, props) { - var settings = recipeConfiguration.Settings; - - IVpc vpc; - if (settings.Vpc.IsDefault) - { - vpc = Vpc.FromLookup(this, "Vpc", new VpcLookupOptions - { - IsDefault = true - }); - } - else if (settings.Vpc.CreateNew) - { - vpc = new Vpc(this, "Vpc", new VpcProps - { - MaxAzs = 2 - }); - } - else - { - vpc = Vpc.FromLookup(this, "Vpc", new VpcLookupOptions - { - VpcId = settings.Vpc.VpcId - }); - } - - ICluster cluster; - if (settings.ECSCluster.CreateNew) - { - cluster = new Cluster(this, "Cluster", new ClusterProps - { - Vpc = vpc, - ClusterName = settings.ECSCluster.NewClusterName - }); - } - else - { - cluster = Cluster.FromClusterAttributes(this, "Cluster", new ClusterAttributes - { - ClusterArn = settings.ECSCluster.ClusterArn, - ClusterName = ECSFargateUtilities.GetClusterNameFromArn(settings.ECSCluster.ClusterArn), - SecurityGroups = new ISecurityGroup[0], - Vpc = vpc - }); - } - - IRole taskRole; - if (settings.ApplicationIAMRole.CreateNew) - { - taskRole = new Role(this, "TaskRole", new RoleProps - { - AssumedBy = new ServicePrincipal("ecs-tasks.amazonaws.com") - }); - } - else - { - if (string.IsNullOrEmpty(settings.ApplicationIAMRole.RoleArn)) - throw new InvalidOrMissingConfigurationException("The provided Application IAM Role ARN is null or empty."); + // Setup callback for generated construct to provide access to customize CDK properties before creating constructs. + CDKRecipeCustomizer.CustomizeCDKProps += CustomizeCDKProps; - taskRole = Role.FromRoleArn(this, "TaskRole", settings.ApplicationIAMRole.RoleArn, new FromRoleArnOptions { - Mutable = false - }); - } + // Create custom CDK constructs here that might need to be referenced in the CustomizeCDKProps. For example if + // creating a DynamoDB table construct and then later using the CDK construct reference in CustomizeCDKProps to + // pass the table name as an environment variable to the container image. - var taskDefinition = new FargateTaskDefinition(this, "TaskDefinition", new FargateTaskDefinitionProps - { - TaskRole = taskRole, - Cpu = settings.TaskCpu, - MemoryLimitMiB = settings.TaskMemory - }); + // Create the recipe defined CDK construct with all of its sub constructs. + var generatedRecipe = new Recipe(this, props.RecipeProps); - if (string.IsNullOrEmpty(recipeConfiguration.ECRRepositoryName)) - throw new InvalidOrMissingConfigurationException("The provided ECR Repository Name is null or empty."); - - var ecrRepository = Repository.FromRepositoryName(this, "ECRRepository", recipeConfiguration.ECRRepositoryName); - var container = taskDefinition.AddContainer("Container", new ContainerDefinitionOptions - { - Image = ContainerImage.FromEcrRepository(ecrRepository, recipeConfiguration.ECRImageTag) - }); - - container.AddPortMappings(new PortMapping - { - ContainerPort = 80, - Protocol = Protocol.TCP - }); - - var ecsLoadBalancerAccessSecurityGroup = new SecurityGroup(this, "WebAccessSecurityGroup", new SecurityGroupProps - { - Vpc = vpc, - SecurityGroupName = $"{recipeConfiguration.StackName}-ECSService" - }); - - var ecsServiceSecurityGroups = new List(); - ecsServiceSecurityGroups.Add(ecsLoadBalancerAccessSecurityGroup); + // Create additional CDK constructs here. The recipe's constructs can be accessed as properties on + // the generatedRecipe variable. + } - if (!string.IsNullOrEmpty(settings.AdditionalECSServiceSecurityGroups)) - { - var count = 1; - foreach (var securityGroupId in settings.AdditionalECSServiceSecurityGroups.Split(',')) - { - ecsServiceSecurityGroups.Add(SecurityGroup.FromSecurityGroupId(this, $"AdditionalGroup-{count++}", securityGroupId.Trim(), new SecurityGroupImportOptions - { - Mutable = false - })); - } - } + public const string FakeConstant = "FakeConstant"; - new ApplicationLoadBalancedFargateService(this, "FargateService", new ApplicationLoadBalancedFargateServiceProps - { - Cluster = cluster, - TaskDefinition = taskDefinition, - DesiredCount = settings.DesiredCount, - ServiceName = settings.ECSServiceName, - AssignPublicIp = settings.Vpc.IsDefault, - SecurityGroups = ecsServiceSecurityGroups.ToArray() - }); + /// + /// This method can be used to customize the properties for CDK constructs before creating the constructs. + /// + /// The pattern used in this method is to check to evnt.ResourceLogicalName to see if the CDK construct about to be created is one + /// you want to customize. If so cast the evnt.Props object to the CDK properties object and make the appropriate settings. + /// + /// + private void CustomizeCDKProps(CustomizePropsEventArgs evnt) + { + // Example of how to customize the container image definition to include environment variables to the running applications. + // + //if (string.Equals(evnt.ResourceLogicalName, nameof(evnt.Construct.AppContainerDefinition))) + //{ + // if(evnt.Props is ContainerDefinitionOptions props) + // { + // Console.WriteLine("Customizing AppContainerDefinition"); + // if (props.Environment == null) + // props.Environment = new Dictionary(); + + // props.Environment["EXAMPLE_ENV1"] = "EXAMPLE_VALUE1"; + // } + //} } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj index ff9e736a7..d373133b7 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj @@ -16,7 +16,6 @@ - @@ -44,7 +43,8 @@ - + + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/Configuration.cs index fe0f52e47..c0c1136b9 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/Configuration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/Configuration.cs @@ -3,67 +3,15 @@ namespace AspNetAppEcsFargate.Configurations { - public class Configuration + /// + /// The configuration settings that are passed in from the deploy tool to the CDK project. If + /// custom settings are defined in the recipe definition then corresponding properties should be added here. + /// + /// This is a partial class with all of the settings defined by default in the recipe declared in the + /// Generated directory's version of this file. + /// + public partial class Configuration { - /// - /// The Identity and Access Management Role that provides AWS credentials to the application to access AWS services. - /// - public IAMRoleConfiguration ApplicationIAMRole { get; set; } - /// - /// The desired number of ECS tasks to run for the service. - /// - public double DesiredCount { get; set; } - - /// - /// The name of the ECS service running in the cluster. - /// - public string ECSServiceName { get; set; } - - /// - /// The ECS cluster that will host the deployed application. - /// - public ECSClusterConfiguration ECSCluster { get; set; } - - /// - /// Virtual Private Cloud to launch container instance into a virtual network. - /// - public VpcConfiguration Vpc { get; set; } - - /// - /// Comma-delimited list of security groups assigned to the ECS service. - /// - public string AdditionalECSServiceSecurityGroups { get; set; } - - /// - public double? TaskCpu { get; set; } - - /// - public double? TaskMemory { get; set; } - - /// A parameterless constructor is needed for - /// or the classes will fail to initialize. - /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. -#nullable disable warnings - public Configuration() - { - - } -#nullable restore warnings - - public Configuration( - IAMRoleConfiguration applicationIAMRole, - string ecsServiceName, - ECSClusterConfiguration ecsCluster, - VpcConfiguration vpc, - string additionalECSServiceSecurityGroups - ) - { - ApplicationIAMRole = applicationIAMRole; - ECSServiceName = ecsServiceName; - ECSCluster = ecsCluster; - Vpc = vpc; - AdditionalECSServiceSecurityGroups = additionalECSServiceSecurityGroups; - } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/IAMRoleConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/IAMRoleConfiguration.cs deleted file mode 100644 index 716698f6c..000000000 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/IAMRoleConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r -// SPDX-License-Identifier: Apache-2.0 - -namespace AspNetAppEcsFargate.Configurations -{ - public class IAMRoleConfiguration - { - /// - /// If set, create a new anonymously named IAM role. - /// - public bool CreateNew { get; set; } - - /// - /// If is false, - /// then use an existing IAM role by referencing through - /// - public string? RoleArn { get; set; } - } -} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/AutoScalingConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/AutoScalingConfiguration.cs new file mode 100644 index 000000000..fef28be43 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/AutoScalingConfiguration.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.Text; + +namespace AspNetAppEcsFargate.Configurations +{ + public class AutoScalingConfiguration + { + const int defaultCooldown = 300; + + public bool Enabled { get; set; } + + public int MinCapacity { get; set; } = 3; + + public int MaxCapacity { get; set; } = 6; + + public enum ScalingTypeEnum { Cpu, Memory, Request } + + public ScalingTypeEnum ScalingType { get; set; } = ScalingTypeEnum.Cpu; + + + + public double CpuTypeTargetUtilizationPercent { get; set; } = 70; + + public int CpuTypeScaleInCooldownSeconds { get; set; } = defaultCooldown; + + public int CpuTypeScaleOutCooldownSeconds { get; set; } = defaultCooldown; + + + + public int RequestTypeRequestsPerTarget { get; set; } = 10000; + + public int RequestTypeScaleInCooldownSeconds { get; set; } = defaultCooldown; + + public int RequestTypeScaleOutCooldownSeconds { get; set; } = defaultCooldown; + + + + public int MemoryTypeTargetUtilizationPercent { get; set; } = 70; + + public int MemoryTypeScaleInCooldownSeconds { get; set; } = defaultCooldown; + + public int MemoryTypeScaleOutCooldownSeconds { get; set; } = defaultCooldown; + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs new file mode 100644 index 000000000..bafe839e5 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppEcsFargate.Configurations +{ + public partial class Configuration + { + /// + /// The Identity and Access Management Role that provides AWS credentials to the application to access AWS services. + /// + public IAMRoleConfiguration ApplicationIAMRole { get; set; } + + /// + /// The desired number of ECS tasks to run for the service. + /// + public double DesiredCount { get; set; } + + /// + /// The name of the ECS service running in the cluster. + /// + public string ECSServiceName { get; set; } + + /// + /// The ECS cluster that will host the deployed application. + /// + public ECSClusterConfiguration ECSCluster { get; set; } + + /// + /// Virtual Private Cloud to launch container instance into a virtual network. + /// + public VpcConfiguration Vpc { get; set; } + + /// + /// Comma-delimited list of security groups assigned to the ECS service. + /// + public string AdditionalECSServiceSecurityGroups { get; set; } + + /// + /// The amount of CPU to allocate to the Fargate task + /// + public double? TaskCpu { get; set; } + + /// + /// The amount of memory to allocate to the Fargate task + /// + public double? TaskMemory { get; set; } + + public LoadBalancerConfiguration LoadBalancer { get; set; } + + public AutoScalingConfiguration AutoScaling { get; set; } + + /// A parameterless constructor is needed for + /// or the classes will fail to initialize. + /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. +#nullable disable warnings + public Configuration() + { + + } +#nullable restore warnings + + public Configuration( + IAMRoleConfiguration applicationIAMRole, + string ecsServiceName, + ECSClusterConfiguration ecsCluster, + VpcConfiguration vpc, + string additionalECSServiceSecurityGroups, + LoadBalancerConfiguration loadBalancer, + AutoScalingConfiguration autoScaling + ) + { + ApplicationIAMRole = applicationIAMRole; + ECSServiceName = ecsServiceName; + ECSCluster = ecsCluster; + Vpc = vpc; + AdditionalECSServiceSecurityGroups = additionalECSServiceSecurityGroups; + LoadBalancer = loadBalancer; + AutoScaling = autoScaling; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/ECSClusterConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/ECSClusterConfiguration.cs similarity index 72% rename from src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/ECSClusterConfiguration.cs rename to src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/ECSClusterConfiguration.cs index 2a17fb460..c0d71c731 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/ECSClusterConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/ECSClusterConfiguration.cs @@ -1,9 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + namespace AspNetAppEcsFargate.Configurations { - public class ECSClusterConfiguration + public partial class ECSClusterConfiguration { /// /// Indicates whether to create a new ECS Cluster or use and existing one diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/IAMRoleConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/IAMRoleConfiguration.cs new file mode 100644 index 000000000..f39c874aa --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/IAMRoleConfiguration.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppEcsFargate.Configurations +{ + public partial class IAMRoleConfiguration + { + /// + /// If set, create a new anonymously named IAM role. + /// + public bool CreateNew { get; set; } + + /// + /// If is false, + /// then use an existing IAM role by referencing through + /// + public string? RoleArn { get; set; } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs new file mode 100644 index 000000000..4c933e87b --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppEcsFargate.Configurations +{ + public partial class LoadBalancerConfiguration + { + /// + /// If set and is false, create a new LoadBalancer + /// + public bool CreateNew { get; set; } + + /// + /// If not creating a new Load Balancer then this is set to an existing load balancer arn. + /// + public string ExistingLoadBalancerArn { get; set; } + + /// + /// How much time to allow currently executing request in ECS tasks to finish before deregistering tasks. + /// + public int DeregistrationDelayInSeconds { get; set; } = 60; + + /// + /// The ping path destination where Elastic Load Balancing sends health check requests. + /// + public string? HealthCheckPath { get; set; } + + /// + /// The approximate number of seconds between health checks. + /// + public int? HealthCheckInternval { get; set; } + + /// + /// The number of consecutive health check successes required before considering an unhealthy target healthy. + /// + public int? HealthyThresholdCount { get; set; } + + /// + /// The number of consecutive health check successes required before considering an unhealthy target unhealthy. + /// + public int? UnhealthyThresholdCount { get; set; } + + public enum ListenerConditionTypeEnum { None, Path} + /// + /// The type of listener condition to create. Current valid values are "None" and "Path" + /// + public ListenerConditionTypeEnum? ListenerConditionType { get; set; } + + /// + /// The resource path pattern to use with ListenerConditionType is set to "Path" + /// + public string? ListenerConditionPathPattern { get; set; } + + /// + /// The priority of the listener condition rule. + /// + public double ListenerConditionPriority { get; set; } = 100; + + + /// A parameterless constructor is needed for + /// or the classes will fail to initialize. + /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. +#nullable disable warnings + public LoadBalancerConfiguration() + { + + } +#nullable restore warnings + + public LoadBalancerConfiguration( + bool createNew, + string loadBalancerId) + { + CreateNew = createNew; + ExistingLoadBalancerArn = loadBalancerId; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/VpcConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/VpcConfiguration.cs similarity index 70% rename from src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/VpcConfiguration.cs rename to src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/VpcConfiguration.cs index b4d001f9b..793b0e35c 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Configurations/VpcConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/VpcConfiguration.cs @@ -1,9 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + namespace AspNetAppEcsFargate.Configurations { - public class VpcConfiguration + public partial class VpcConfiguration { /// /// If set, use default VPC diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs new file mode 100644 index 000000000..478f49874 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; + +using Amazon.CDK; +using Amazon.CDK.AWS.ApplicationAutoScaling; +using Amazon.CDK.AWS.EC2; +using Amazon.CDK.AWS.ECS; +using Amazon.CDK.AWS.ElasticLoadBalancingV2; +using Amazon.CDK.AWS.IAM; +using AWS.Deploy.Recipes.CDK.Common; +using AspNetAppEcsFargate.Configurations; +using Amazon.CDK.AWS.ECR; +using System.Linq; + + +// This is a generated file from the original deployment recipe. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// To customize the CDK constructs created in this file you should use the AppStack.CustomizeCDKProps() method. + +namespace AspNetAppEcsFargate +{ + using static AWS.Deploy.Recipes.CDK.Common.CDKRecipeCustomizer; + + + public class Recipe : Construct + { + public IVpc? AppVpc { get; private set; } + public ICluster? EcsCluster { get; private set; } + public IRole? AppIAMTaskRole { get; private set; } + public TaskDefinition? AppTaskDefinition { get; private set; } + public IRepository? EcrRepository { get; private set; } + public ContainerDefinition? AppContainerDefinition { get; private set; } + public SecurityGroup? WebAccessSecurityGroup { get; private set; } + public IList? EcsServiceSecurityGroups { get; private set; } + public FargateService? AppFargateService { get; private set; } + public IApplicationLoadBalancer? ServiceLoadBalancer { get; private set; } + public IApplicationListener? LoadBalancerListener { get; private set; } + public ApplicationTargetGroup? ServiceTargetGroup { get; private set; } + public AwsLogDriver? AppLogging { get; private set; } + + public ScalableTaskCount? AutoScalingConfiguration { get; private set; } + + public string AutoScaleTypeCpuType { get; } = "AutoScaleTypeCpuType"; + public string AutoScaleTypeRequestType { get; } = "AutoScaleTypeRequestType"; + public string AutoScaleTypeMemoryType { get; } = "AutoScaleTypeMemoryType"; + + public string AddTargetGroup { get; } = "AddTargetGroup"; + + public Recipe(Construct scope, IRecipeProps props) + : base(scope, "GeneratedRecipeConstruct") + { + var settings = props.Settings; + + ConfigureVpc(settings); + ConfigureApplicationIAMRole(settings); + ConfigureECSClusterAndService(props); + ConfigureLoadBalancer(settings); + ConfigureAutoScaling(settings); + } + + private void ConfigureVpc(Configuration settings) + { + if (settings.Vpc.IsDefault) + { + AppVpc = Vpc.FromLookup(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcLookupOptions + { + IsDefault = true + })); + } + else if (settings.Vpc.CreateNew) + { + AppVpc = new Vpc(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcProps + { + MaxAzs = 2 + })); + } + else + { + AppVpc = Vpc.FromLookup(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcLookupOptions + { + VpcId = settings.Vpc.VpcId + })); + } + } + + private void ConfigureApplicationIAMRole(Configuration settings) + { + if (settings.ApplicationIAMRole.CreateNew) + { + AppIAMTaskRole = new Role(this, nameof(AppIAMTaskRole), InvokeCustomizeCDKPropsEvent(nameof(AppIAMTaskRole), this, new RoleProps + { + AssumedBy = new ServicePrincipal("ecs-tasks.amazonaws.com") + })); + } + else + { + if (string.IsNullOrEmpty(settings.ApplicationIAMRole.RoleArn)) + throw new InvalidOrMissingConfigurationException("The provided Application IAM Role ARN is null or empty."); + + AppIAMTaskRole = Role.FromRoleArn(this, nameof(AppIAMTaskRole), settings.ApplicationIAMRole.RoleArn, InvokeCustomizeCDKPropsEvent(nameof(AppIAMTaskRole), this, new FromRoleArnOptions + { + Mutable = false + })); + } + } + + private void ConfigureECSClusterAndService(IRecipeProps recipeConfiguration) + { + if (AppVpc == null) + throw new ArgumentNullException(nameof(AppVpc)); + + var settings = recipeConfiguration.Settings; + if (settings.ECSCluster.CreateNew) + { + EcsCluster = new Cluster(this, nameof(EcsCluster), InvokeCustomizeCDKPropsEvent(nameof(EcsCluster), this, new ClusterProps + { + Vpc = AppVpc, + ClusterName = settings.ECSCluster.NewClusterName + })); + } + else + { + EcsCluster = Cluster.FromClusterAttributes(this, nameof(EcsCluster), InvokeCustomizeCDKPropsEvent(nameof(EcsCluster), this, new ClusterAttributes + { + ClusterArn = settings.ECSCluster.ClusterArn, + ClusterName = ECSFargateUtilities.GetClusterNameFromArn(settings.ECSCluster.ClusterArn), + SecurityGroups = new ISecurityGroup[0], + Vpc = AppVpc + })); + } + + AppTaskDefinition = new FargateTaskDefinition(this, nameof(AppTaskDefinition), InvokeCustomizeCDKPropsEvent(nameof(AppTaskDefinition), this, new FargateTaskDefinitionProps + { + TaskRole = AppIAMTaskRole, + Cpu = settings.TaskCpu, + MemoryLimitMiB = settings.TaskMemory + })); + + AppLogging = new AwsLogDriver(InvokeCustomizeCDKPropsEvent(nameof(AppLogging), this, new AwsLogDriverProps + { + StreamPrefix = recipeConfiguration.StackName + })); + + if (string.IsNullOrEmpty(recipeConfiguration.ECRRepositoryName)) + throw new InvalidOrMissingConfigurationException("The provided ECR Repository Name is null or empty."); + + EcrRepository = Repository.FromRepositoryName(this, nameof(EcrRepository), recipeConfiguration.ECRRepositoryName); + AppContainerDefinition = AppTaskDefinition.AddContainer(nameof(AppContainerDefinition), InvokeCustomizeCDKPropsEvent(nameof(AppContainerDefinition), this, new ContainerDefinitionOptions + { + Image = ContainerImage.FromEcrRepository(EcrRepository, recipeConfiguration.ECRImageTag), + Logging = AppLogging + })); + + AppContainerDefinition.AddPortMappings(new PortMapping + { + ContainerPort = 80, + Protocol = Amazon.CDK.AWS.ECS.Protocol.TCP + }); + + WebAccessSecurityGroup = new SecurityGroup(this, nameof(WebAccessSecurityGroup), InvokeCustomizeCDKPropsEvent(nameof(WebAccessSecurityGroup), this, new SecurityGroupProps + { + Vpc = AppVpc, + SecurityGroupName = $"{recipeConfiguration.StackName}-ECSService" + })); + + EcsServiceSecurityGroups = new List(); + EcsServiceSecurityGroups.Add(WebAccessSecurityGroup); + + if (!string.IsNullOrEmpty(settings.AdditionalECSServiceSecurityGroups)) + { + var count = 1; + foreach (var securityGroupId in settings.AdditionalECSServiceSecurityGroups.Split(',')) + { + EcsServiceSecurityGroups.Add(SecurityGroup.FromSecurityGroupId(this, $"AdditionalGroup-{count++}", securityGroupId.Trim(), new SecurityGroupImportOptions + { + Mutable = false + })); + } + } + + AppFargateService = new FargateService(this, nameof(AppFargateService), InvokeCustomizeCDKPropsEvent(nameof(AppFargateService), this, new FargateServiceProps + { + Cluster = EcsCluster, + TaskDefinition = AppTaskDefinition, + DesiredCount = settings.DesiredCount, + ServiceName = settings.ECSServiceName, + AssignPublicIp = settings.Vpc.IsDefault, + SecurityGroups = EcsServiceSecurityGroups.ToArray() + })); + } + + private void ConfigureLoadBalancer(Configuration settings) + { + if (AppVpc == null) + throw new ArgumentNullException(nameof(AppVpc)); + if (EcsCluster == null) + throw new ArgumentNullException(nameof(EcsCluster)); + if (AppFargateService == null) + throw new ArgumentNullException(nameof(AppFargateService)); + + if (settings.LoadBalancer.CreateNew) + { + ServiceLoadBalancer = new ApplicationLoadBalancer(this, nameof(ServiceLoadBalancer), InvokeCustomizeCDKPropsEvent(nameof(ServiceLoadBalancer), this, new ApplicationLoadBalancerProps + { + Vpc = AppVpc, + InternetFacing = true + })); + + LoadBalancerListener = ServiceLoadBalancer.AddListener(nameof(LoadBalancerListener), InvokeCustomizeCDKPropsEvent(nameof(LoadBalancerListener), this, new ApplicationListenerProps + { + Protocol = ApplicationProtocol.HTTP, + Port = 80, + Open = true + })); + + ServiceTargetGroup = LoadBalancerListener.AddTargets(nameof(ServiceTargetGroup), InvokeCustomizeCDKPropsEvent(nameof(ServiceTargetGroup), this, new AddApplicationTargetsProps + { + Protocol = ApplicationProtocol.HTTP, + DeregistrationDelay = Duration.Seconds(settings.LoadBalancer.DeregistrationDelayInSeconds) + })); + } + else + { + ServiceLoadBalancer = ApplicationLoadBalancer.FromLookup(this, nameof(ServiceLoadBalancer), InvokeCustomizeCDKPropsEvent(nameof(ServiceLoadBalancer), this, new ApplicationLoadBalancerLookupOptions + { + LoadBalancerArn = settings.LoadBalancer.ExistingLoadBalancerArn + })); + + LoadBalancerListener = ApplicationListener.FromLookup(this, nameof(LoadBalancerListener), InvokeCustomizeCDKPropsEvent(nameof(LoadBalancerListener), this, new ApplicationListenerLookupOptions + { + LoadBalancerArn = settings.LoadBalancer.ExistingLoadBalancerArn, + ListenerPort = 80 + })); + + ServiceTargetGroup = new ApplicationTargetGroup(this, nameof(ServiceTargetGroup), InvokeCustomizeCDKPropsEvent(nameof(ServiceTargetGroup), this, new ApplicationTargetGroupProps + { + Port = 80, + Vpc = EcsCluster.Vpc, + })); + + + var addApplicationTargetGroupsProps = new AddApplicationTargetGroupsProps + { + TargetGroups = new[] { ServiceTargetGroup } + }; + + if(settings.LoadBalancer.ListenerConditionType != LoadBalancerConfiguration.ListenerConditionTypeEnum.None) + { + addApplicationTargetGroupsProps.Priority = settings.LoadBalancer.ListenerConditionPriority; + } + + if (settings.LoadBalancer.ListenerConditionType == LoadBalancerConfiguration.ListenerConditionTypeEnum.Path) + { + if(settings.LoadBalancer.ListenerConditionPathPattern == null) + { + throw new ArgumentNullException("Listener condition type was set to \"Path\" but no value was set for the \"TargetPathPattern\""); + } + addApplicationTargetGroupsProps.Conditions = new ListenerCondition[] + { + ListenerCondition.PathPatterns(new []{ settings.LoadBalancer.ListenerConditionPathPattern }) + }; + } + + LoadBalancerListener.AddTargetGroups("AddTargetGroup", InvokeCustomizeCDKPropsEvent("AddTargetGroup", this, addApplicationTargetGroupsProps)); + } + + // Configure health check for ALB Target Group + var healthCheck = new Amazon.CDK.AWS.ElasticLoadBalancingV2.HealthCheck(); + if(settings.LoadBalancer.HealthCheckPath != null) + { + var path = settings.LoadBalancer.HealthCheckPath; + if (!path.StartsWith("/")) + path = "/" + path; + healthCheck.Path = path; + } + if(settings.LoadBalancer.HealthCheckInternval.HasValue) + { + healthCheck.Interval = Duration.Seconds(settings.LoadBalancer.HealthCheckInternval.Value); + } + if (settings.LoadBalancer.HealthyThresholdCount.HasValue) + { + healthCheck.HealthyThresholdCount = settings.LoadBalancer.HealthyThresholdCount.Value; + } + if (settings.LoadBalancer.UnhealthyThresholdCount.HasValue) + { + healthCheck.UnhealthyThresholdCount = settings.LoadBalancer.UnhealthyThresholdCount.Value; + } + + ServiceTargetGroup.ConfigureHealthCheck(healthCheck); + + ServiceTargetGroup.AddTarget(AppFargateService); + } + + private void ConfigureAutoScaling(Configuration settings) + { + if (AppFargateService == null) + throw new ArgumentNullException(nameof(AppFargateService)); + if (ServiceTargetGroup == null) + throw new ArgumentNullException(nameof(ServiceTargetGroup)); + + if (settings.AutoScaling.Enabled) + { + AutoScalingConfiguration = AppFargateService.AutoScaleTaskCount(InvokeCustomizeCDKPropsEvent(nameof(AutoScalingConfiguration), this, new EnableScalingProps + { + MinCapacity = settings.AutoScaling.MinCapacity, + MaxCapacity = settings.AutoScaling.MaxCapacity + })); + + switch (settings.AutoScaling.ScalingType) + { + case AspNetAppEcsFargate.Configurations.AutoScalingConfiguration.ScalingTypeEnum.Cpu: + AutoScalingConfiguration.ScaleOnCpuUtilization(AutoScaleTypeCpuType, InvokeCustomizeCDKPropsEvent(AutoScaleTypeCpuType, this, new CpuUtilizationScalingProps + { + TargetUtilizationPercent = settings.AutoScaling.CpuTypeTargetUtilizationPercent, + ScaleOutCooldown = Duration.Seconds(settings.AutoScaling.CpuTypeScaleOutCooldownSeconds), + ScaleInCooldown = Duration.Seconds(settings.AutoScaling.CpuTypeScaleInCooldownSeconds) + })); + break; + case AspNetAppEcsFargate.Configurations.AutoScalingConfiguration.ScalingTypeEnum.Memory: + AutoScalingConfiguration.ScaleOnMemoryUtilization(AutoScaleTypeMemoryType, InvokeCustomizeCDKPropsEvent(AutoScaleTypeMemoryType, this, new MemoryUtilizationScalingProps + { + TargetUtilizationPercent = settings.AutoScaling.MemoryTypeTargetUtilizationPercent, + ScaleOutCooldown = Duration.Seconds(settings.AutoScaling.MemoryTypeScaleOutCooldownSeconds), + ScaleInCooldown = Duration.Seconds(settings.AutoScaling.MemoryTypeScaleInCooldownSeconds) + })); + break; + case AspNetAppEcsFargate.Configurations.AutoScalingConfiguration.ScalingTypeEnum.Request: + AutoScalingConfiguration.ScaleOnRequestCount(AutoScaleTypeRequestType, InvokeCustomizeCDKPropsEvent(AutoScaleTypeRequestType, this, new RequestCountScalingProps + { + TargetGroup = ServiceTargetGroup, + RequestsPerTarget = settings.AutoScaling.RequestTypeRequestsPerTarget, + ScaleOutCooldown = Duration.Seconds(settings.AutoScaling.RequestTypeScaleOutCooldownSeconds), + ScaleInCooldown = Duration.Seconds(settings.AutoScaling.RequestTypeScaleInCooldownSeconds) + })); + break; + default: + throw new System.ArgumentException($"Invalid AutoScaling type {settings.AutoScaling.ScalingType}"); + } + } + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs index 8de5e9cdf..34ed5e44e 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs @@ -17,16 +17,21 @@ public static void Main(string[] args) var app = new App(); var builder = new ConfigurationBuilder().AddAWSDeployToolConfiguration(app); - var recipeConfiguration = builder.Build().Get>(); - - CDKRecipeSetup.RegisterStack(new AppStack(app, recipeConfiguration, new StackProps + var recipeProps = builder.Build().Get>(); + var appStackProps = new DeployToolStackProps(recipeProps) { Env = new Environment { - Account = recipeConfiguration.AWSAccountId, - Region = recipeConfiguration.AWSRegion + Account = recipeProps.AWSAccountId, + Region = recipeProps.AWSRegion } - }), recipeConfiguration); + }; + + // The RegisterStack method is used to set identifying information on the stack + // for the recipe used to deploy the application and preserve the settings used in the recipe + // to allow redeployment. The information is stored as CloudFormation tags and metadata inside + // the generated CloudFormation template. + CDKRecipeSetup.RegisterStack(new AppStack(app, appStackProps), appStackProps.RecipeProps); app.Synth(); } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/README.md b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/README.md index fee04ca40..c8a2c3395 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/README.md +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/README.md @@ -1,14 +1,55 @@ -# Welcome to your CDK C# project! +# AWS deploy tool deployment project -This is a blank project for C# development with CDK. +This .NET project is a deployment project used by the AWS deploy tool to deploy .NET applications to AWS. The project is made +up of 2 parts. -The `cdk.json` file tells the CDK Toolkit how to execute your app. +First is a *.recipe file which defines all of the settings for deployment project. The recipe file is what +the AWS.Deploy.CLI tool and the AWS Toolkit for Visual Studio use to drive the user experience to deploy a .NET application +with this deployment project. -It uses the [.NET Core CLI](https://docs.microsoft.com/dotnet/articles/core/) to compile and execute your project. +The second part of the deployment project is a .NET AWS CDK project which defines the AWS infrastructure that the +.NET application will be deployed to. -## Useful commands +## What is CDK? + +The AWS Cloud Development Kit (CDK) is an open source software development framework to define your cloud application +resources using familiar programming languages like C#. In CDK projects, constructs are instantiated for each of the +AWS resources required. CDK projects are used to generate an AWS CloudFormation template to be used by the +AWS CloudFormation service to create a Stack of all of the resources defined in a template. + +Visit the following link for more information on the AWS CDK: +https://aws.amazon.com/cdk/ + +## Should I use the CDK CLI? + +In a regular CDK project the CDK CLI, acquired from NPM, would be used to execute the CDK project. Because AWS deploy +tool deployment projects are made of both a recipe and a CDK project you should not use the CDK CLI directly on +the deployment project. + +The AWS deploy tool from either AWS.Deploy.CLI tool package or AWS Toolkit for Visual Studio +should be used to drive the experience. The AWS deploy tool will take care of acquiring the CDK CLI and executing the +CDK CLI passing in all of the settings gathered in the AWS deploy tool. + +## Can I modify the deployment project? + +When a deployment projects is saved the project can be customized by adding more CDK constructs or customizing the existing +CDK constructs. + +The default folder structure puts the CDK constructs originally defined by the deployment recipe into a folder called +"Generated". It is recommended to not directly modify these files and instead customize the settings via the +AppStack.CustomizeCDKProps() method. This allows the AWS deploy tool to easily updated the generated code +as new features are added to the original recipe the deployment project was created from. Checkout the AppStack.cs +file for information on how to customize the CDK project. + +## Can I add more settings to the recipe? + +As you customize the deployment project you might want to present the user's of the deployment project more +settings that will be displayed in the AWS.Deploy.CLI tool package or AWS Toolkit for Visual Studio. The recipe +file in the deployment project can be modified to add new settings. Below is the link to the JSON schema for the +recipe. + +https://github.com/aws/aws-dotnet-deploy/blob/main/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json + +For any new settings added to the recipe you will need to add corresponding fields in the Configuration class using the +setting ids as the property names. -* `dotnet build src` compile this app -* `cdk deploy` deploy this stack to your default AWS account/region -* `cdk diff` compare deployed stack with current state -* `cdk synth` emits the synthesized CloudFormation template \ No newline at end of file diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/appsettings.json b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/appsettings.json deleted file mode 100644 index 0db3279e4..000000000 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/appsettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index e57d03882..825c9c4ae 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -13,15 +13,15 @@ "DisplayedResources": [ { - "LogicalId": "FargateServiceLBB353E155", + "LogicalId": "GeneratedRecipeConstructServiceLoadBalancerA6C6B865", "Description": "Application Endpoint" }, { - "LogicalId": "FargateServiceECC8084D", + "LogicalId": "GeneratedRecipeConstructAppFargateServiceA4EB7E71", "Description": "ECS Service" }, { - "LogicalId": "ClusterEB0386A7", + "LogicalId": "GeneratedRecipeConstructEcsClusterB9B37255", "Description": "ECS Cluster" } ], @@ -205,7 +205,7 @@ "ServicePrincipal": "ecs-tasks.amazonaws.com" }, "AdvancedSetting": false, - "Updatable": false, + "Updatable": true, "ChildOptionSettings": [ { "Id": "CreateNew", @@ -406,6 +406,383 @@ "29696": "29 GB", "30720": "30 GB" } + }, + { + "Id": "LoadBalancer", + "Name": "Elastic Load Balancer", + "Description": "Load Balancer the ECS Service will register tasks to.", + "Type": "Object", + "AdvancedSetting": true, + "Updatable": true, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New Load Balancer", + "Description": "Do you want to create a new Load Balancer?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "ExistingLoadBalancerArn", + "Name": "Existing Load Balancer ARN", + "Description": "The ARN of an existing load balancer to use.", + "Type": "String", + "TypeHint": "ExistingApplicationLoadBalancer", + "DefaultValue": null, + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + } + ] + }, + { + "Id": "DeregistrationDelayInSeconds", + "Name": "Deregistration delay (seconds)", + "Description": "The amount of time to allow requests to finish before deregistering ECS tasks.", + "Type": "Int", + "DefaultValue": 60, + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "HealthCheckPath", + "Name": "Health Check Path", + "Description": "The ping path destination where Elastic Load Balancing sends health check requests.", + "Type": "String", + "DefaultValue": "/", + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "HealthCheckInternval", + "Name": "Health Check Interval", + "Description": "The number of consecutive health check successes required before considering an unhealthy target healthy.", + "Type": "Int", + "DefaultValue": 30, + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "HealthyThresholdCount", + "Name": "Healthy Threshold Count", + "Description": "The number of consecutive health check successes required before considering an unhealthy target healthy.", + "Type": "Int", + "DefaultValue": 5, + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "UnhealthyThresholdCount", + "Name": "Unhealthy Threshold Count", + "Description": "The number of consecutive health check successes required before considering an unhealthy target unhealthy.", + "Type": "Int", + "DefaultValue": 2, + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "ListenerConditionType", + "Name": "Type of Listener Condition", + "Description": "The type of listener rule to create to direct traffic to ECS service.", + "Type": "String", + "DefaultValue": "None", + "AdvancedSetting": false, + "Updatable": true, + "AllowedValues": [ + "None", + "Path" + ], + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + } + ] + }, + { + "Id": "ListenerConditionPathPattern", + "Name": "Listener Condition Path Pattern", + "Description": "The resource path pattern to use for the listener rule. (i.e. \"/api/*\") ", + "Type": "String", + "DefaultValue": null, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + }, + { + "Id": "LoadBalancer.ListenerConditionType", + "Value": "Path" + } + ] + }, + { + "Id": "ListenerConditionPriority", + "Name": "Listener Condition Priority", + "Description": "Priority of the condition rule. The value must be unique for the Load Balancer listener.", + "Type": "Double", + "DefaultValue": 100, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + }, + { + "Id": "LoadBalancer.ListenerConditionType", + "Value": "Path" + } + ] + } + ] + }, + { + "Id": "AutoScaling", + "Name": "AutoScaling", + "Description": "The AutoScaling configuration for the ECS service.", + "Type": "Object", + "AdvancedSetting": true, + "Updatable": true, + "ChildOptionSettings": [ + { + "Id": "Enabled", + "Name": "Enable", + "Description": "Do you want to enable AutoScaling?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "MinCapacity", + "Name": "Minimum Capacity", + "Description": "The minimum number of ECS tasks handling the demand for the ECS service.", + "Type": "Int", + "DefaultValue": 3, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "MaxCapacity", + "Name": "Maximum Capacity", + "Description": "The maximum number of ECS tasks handling the demand for the ECS service.", + "Type": "Int", + "DefaultValue": 6, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "ScalingType", + "Name": "AutoScaling Metric", + "Description": "The metric to monitor for scaling changes.", + "Type": "String", + "DefaultValue": "Cpu", + "AdvancedSetting": false, + "Updatable": true, + "AllowedValues": [ + "Cpu", + "Memory", + "Request" + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "CpuTypeTargetUtilizationPercent", + "Name": "CPU Target Utilization", + "Description": "The target cpu utilization that triggers a scaling change.", + "Type": "Double", + "DefaultValue": 70, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "CpuTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "CpuTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "MemoryTypeTargetUtilizationPercent", + "Name": "Memory Target Utilization", + "Description": "The target memory utilization that triggers a scaling change.", + "Type": "Double", + "DefaultValue": 70, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "MemoryTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "MemoryTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "RequestTypeRequestsPerTarget", + "Name": "Request per task", + "Description": "The number of request per ECS task that triggers a scaling change.", + "Type": "Int", + "DefaultValue": 1000, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + }, + { + "Id": "RequestTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + }, + { + "Id": "RequestTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + } + ] } ] } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs index 91a316beb..1fe057b67 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -82,9 +82,8 @@ public async Task PerformDeployment() var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - // Example: WebAppWithDockerFile3d078c3ca551.FargateServiceServiceURL47701F45 = http://WebAp-Farga-12O3W5VNB5OLC-166471465.us-west-2.elb.amazonaws.com - var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.FargateServiceServiceURL")) - .Split("=")[1] + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] .Trim(); // 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/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index 5965446d2..e91e0b1cc 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -11,6 +11,7 @@ using Amazon.ECR.Model; using Amazon.ECS.Model; using Amazon.ElasticBeanstalk.Model; +using Amazon.ElasticLoadBalancingV2; using Amazon.IdentityManagement.Model; using Amazon.SecurityToken.Model; using AWS.Deploy.Orchestration.Data; @@ -44,5 +45,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> ListOfElasticBeanstalkEnvironments(string applicationName) => throw new NotImplementedException(); public Task> ListOfIAMRoles(string servicePrincipal) => throw new NotImplementedException(); public Task DescribeAppRunnerService(string serviceArn) => throw new NotImplementedException(); + public Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index 1c19f6245..a201d9082 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs @@ -78,9 +78,8 @@ public async Task DefaultConfigurations() var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - // Example: WebAppWithDockerFile3d078c3ca551.FargateServiceServiceURL47701F45 = http://WebAp-Farga-12O3W5VNB5OLC-166471465.us-west-2.elb.amazonaws.com - var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.FargateServiceServiceURL")) - .Split("=")[1] + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] .Trim(); // 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.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index a75f24641..40dd2abdd 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -9,6 +9,7 @@ using Amazon.EC2.Model; using Amazon.ECR.Model; using Amazon.ElasticBeanstalk.Model; +using Amazon.ElasticLoadBalancingV2; using Amazon.IdentityManagement.Model; using Amazon.S3; using Amazon.SecurityToken.Model; @@ -69,5 +70,7 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn) => throw new NotImplementedException(); public Task DescribeCloudWatchRule(string ruleName) => throw new NotImplementedException(); Task IAWSResourceQueryer.GetS3BucketLocation(string bucketName) => throw new NotImplementedException(); + + public Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs index b850d3e81..1d13a39bb 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs @@ -93,7 +93,7 @@ public async Task GetDeploymentOutputs_ElasticLoadBalancer() var recommendations = await engine.ComputeRecommendations(); var recommendation = recommendations.First(r => r.Recipe.Id.Equals("AspNetAppEcsFargate")); - _stackResource.LogicalResourceId = "FargateServiceLBB353E155"; + _stackResource.LogicalResourceId = "GeneratedRecipeConstructServiceLoadBalancerA6C6B865"; _stackResource.PhysicalResourceId = "PhysicalResourceId"; _stackResource.ResourceType = "AWS::ElasticLoadBalancingV2::LoadBalancer"; _loadBalancer.DNSName = "www.website.com"; From 4ed4e15fa4b0afad985983c6ffff967d11b2573a Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Mon, 16 Aug 2021 13:12:14 -0700 Subject: [PATCH 05/13] feat: detect CDK version by parsing deployment project csproj file Currently, Deployment Tool uses static CDK version which needs to be changed as Amazon.CDK.* package references are updated. This change allows Deployment Tool to detect the minimum compatible version of Amazon.CDK.* dependency which must be installed or will be installed by the tool. --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 12 ++- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 24 +++-- .../GenerateDeploymentProjectCommand.cs | 2 +- .../CustomServiceCollectionExtension.cs | 1 + .../Controllers/DeploymentController.cs | 6 +- src/AWS.Deploy.Common/IO/DirectoryManager.cs | 18 +++- src/AWS.Deploy.Constants/CDK.cs | 8 +- .../CDK/CDKVersionDetector.cs | 94 +++++++++++++++++++ .../CdkProjectHandler.cs | 38 ++++---- src/AWS.Deploy.Orchestration/Orchestrator.cs | 33 +++++-- .../IO/TestDirectoryManager.cs | 2 + .../RecommendationTests.cs | 10 +- .../AWS.Deploy.Orchestration.UnitTests.csproj | 6 ++ .../CDK/CDKVersionDetectorTests.cs | 44 +++++++++ .../CDK/CSProjFiles/MixedReferences.csproj | 10 ++ .../CDK/CSProjFiles/NoReferences.csproj | 2 + .../CDK/CSProjFiles/SameReferences.csproj | 9 ++ .../TestDirectoryManager.cs | 3 + 18 files changed, 272 insertions(+), 50 deletions(-) create mode 100644 src/AWS.Deploy.Orchestration/CDK/CDKVersionDetector.cs create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index cd7ece17e..1c6a9528b 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections; using System.CommandLine; using System.CommandLine.Invocation; using System.IO; @@ -67,6 +68,7 @@ public class CommandFactory : ICommandFactory private readonly IDeploymentManifestEngine _deploymentManifestEngine; private readonly ICustomRecipeLocator _customRecipeLocator; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; + private readonly ICDKVersionDetector _cdkVersionDetector; public CommandFactory( IToolInteractiveService toolInteractiveService, @@ -90,7 +92,8 @@ public CommandFactory( IFileManager fileManager, IDeploymentManifestEngine deploymentManifestEngine, ICustomRecipeLocator customRecipeLocator, - ILocalUserSettingsEngine localUserSettingsEngine) + ILocalUserSettingsEngine localUserSettingsEngine, + ICDKVersionDetector cdkVersionDetector) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -114,6 +117,7 @@ public CommandFactory( _deploymentManifestEngine = deploymentManifestEngine; _customRecipeLocator = customRecipeLocator; _localUserSettingsEngine = localUserSettingsEngine; + _cdkVersionDetector = cdkVersionDetector; } public Command BuildRootCommand() @@ -193,6 +197,7 @@ private Command BuildDeployCommand() _orchestratorInteractiveService, _cdkProjectHandler, _cdkManager, + _cdkVersionDetector, _deploymentBundleHandler, dockerEngine, _awsResourceQueryer, @@ -205,7 +210,8 @@ private Command BuildDeployCommand() _consoleUtilities, _customRecipeLocator, _systemCapabilityEvaluator, - session); + session, + _directoryManager); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; if (!string.IsNullOrEmpty(deploymentProjectPath)) @@ -338,7 +344,7 @@ private Command BuildListCommand() listCommand.Add(_optionRegion); listCommand.Add(_optionProjectPath); listCommand.Add(_optionDiagnosticLogging); - } + } listCommand.Handler = CommandHandler.Create(async (ListCommandHandlerInput input) => { diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 5f5ddae3c..1ba74afc2 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -42,12 +42,15 @@ public class DeployCommand private readonly ICustomRecipeLocator _customRecipeLocator; private readonly ISystemCapabilityEvaluator _systemCapabilityEvaluator; private readonly OrchestratorSession _session; + private readonly IDirectoryManager _directoryManager; + private ICDKVersionDetector _cdkVersionDetector; public DeployCommand( IToolInteractiveService toolInteractiveService, IOrchestratorInteractiveService orchestratorInteractiveService, ICdkProjectHandler cdkProjectHandler, ICDKManager cdkManager, + ICDKVersionDetector cdkVersionDetector, IDeploymentBundleHandler deploymentBundleHandler, IDockerEngine dockerEngine, IAWSResourceQueryer awsResourceQueryer, @@ -60,7 +63,8 @@ public DeployCommand( IConsoleUtilities consoleUtilities, ICustomRecipeLocator customRecipeLocator, ISystemCapabilityEvaluator systemCapabilityEvaluator, - OrchestratorSession session) + OrchestratorSession session, + IDirectoryManager directoryManager) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -76,6 +80,8 @@ public DeployCommand( _localUserSettingsEngine = localUserSettingsEngine; _consoleUtilities = consoleUtilities; _session = session; + _directoryManager = directoryManager; + _cdkVersionDetector = cdkVersionDetector; _cdkManager = cdkManager; _customRecipeLocator = customRecipeLocator; _systemCapabilityEvaluator = systemCapabilityEvaluator; @@ -136,12 +142,14 @@ private void DisplayOutputResources(List displayedResourc _orchestratorInteractiveService, _cdkProjectHandler, _cdkManager, + _cdkVersionDetector, _awsResourceQueryer, _deploymentBundleHandler, _localUserSettingsEngine, _dockerEngine, _customRecipeLocator, - new List { RecipeLocator.FindRecipeDefinitionsPath() }); + new List { RecipeLocator.FindRecipeDefinitionsPath() }, + _directoryManager); // Determine what recommendations are possible for the project. var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath); @@ -170,7 +178,7 @@ private void DisplayOutputResources(List displayedResourc // preset settings for deployment based on last deployment. selectedRecommendation = await GetSelectedRecommendationFromPreviousDeployment(recommendations, deployedApplication, userDeploymentSettings); - } + } else { if (!string.IsNullOrEmpty(deploymentProjectPath)) @@ -182,7 +190,7 @@ private void DisplayOutputResources(List displayedResourc selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); } } - + var cloudApplication = new CloudApplication(cloudApplicationName, selectedRecommendation.Recipe.Id); return (orchestrator, selectedRecommendation, cloudApplication); @@ -275,7 +283,7 @@ private async Task GetSelectedRecommendationFromPreviousDeployme } throw new InvalidUserDeploymentSettingsException(errorMessage.Trim()); } - + selectedRecommendation = selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); var header = $"Loading {deployedApplication.Name} settings:"; @@ -440,7 +448,7 @@ private string GetCloudApplicationName(string? stackName, UserDeploymentSettings private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDeploymentSettings, List recommendations) { var deploymentSettingsRecipeId = userDeploymentSettings?.RecipeId; - + if (string.IsNullOrEmpty(deploymentSettingsRecipeId)) { if (_toolInteractiveService.DisableInteractive) @@ -453,13 +461,13 @@ private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDep } return _consoleUtilities.AskToChooseRecommendation(recommendations); } - + var selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(deploymentSettingsRecipeId, StringComparison.Ordinal)); if (selectedRecommendation == null) { throw new InvalidUserDeploymentSettingsException($"The user deployment settings provided contains an invalid value for the property '{nameof(userDeploymentSettings.RecipeId)}'."); } - + _toolInteractiveService.WriteLine(); _toolInteractiveService.WriteLine($"Configuring Recommendation with: '{selectedRecommendation.Name}'."); return selectedRecommendation; diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs index e0420d18a..bccd4641f 100644 --- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -102,7 +102,7 @@ public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDispla } } - _cdkProjectHandler.CreateCdkProjectForDeployment(selectedRecommendation, _session, saveCdkDirectoryPath); + _cdkProjectHandler.CreateCdkProject(selectedRecommendation, _session, saveCdkDirectoryPath); await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveCdkDirectoryPath, projectDisplayName); var saveCdkDirectoryFullPath = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).FullName; diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 673b738b2..6a13d7ef1 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -57,6 +57,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICustomRecipeLocator), typeof(CustomRecipeLocator), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ILocalUserSettingsEngine), typeof(LocalUserSettingsEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKVersionDetector), typeof(CDKVersionDetector), 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 62ca570d4..74b358eb9 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -464,7 +464,7 @@ private IServiceProvider CreateSessionServiceProvider(string sessionId, string a var serviceProvider = services.BuildServiceProvider(); var awsClientFactory = serviceProvider.GetRequiredService(); - + awsClientFactory.ConfigureAWSOptions(awsOptions => { awsOptions.Credentials = awsCredentials; @@ -498,12 +498,14 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), new DockerEngine.DockerEngine(session.ProjectDefinition), serviceProvider.GetRequiredService(), - new List { RecipeLocator.FindRecipeDefinitionsPath() } + new List { RecipeLocator.FindRecipeDefinitionsPath() }, + serviceProvider.GetRequiredService() ); } } diff --git a/src/AWS.Deploy.Common/IO/DirectoryManager.cs b/src/AWS.Deploy.Common/IO/DirectoryManager.cs index f8fdd7fda..be82af6c3 100644 --- a/src/AWS.Deploy.Common/IO/DirectoryManager.cs +++ b/src/AWS.Deploy.Common/IO/DirectoryManager.cs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace AWS.Deploy.Common.IO { @@ -18,19 +20,26 @@ public interface IDirectoryManager void Delete(string path, bool recursive = false); string GetRelativePath(string referenceFullPath, string targetFullPath); string GetAbsolutePath(string referenceFullPath, string targetRelativePath); + public string[] GetProjFiles(string path); } public class DirectoryManager : IDirectoryManager { + private readonly HashSet _projFileExtensions = new() + { + "csproj", + "fsproj" + }; + public DirectoryInfo CreateDirectory(string path) => Directory.CreateDirectory(path); - + public DirectoryInfo GetDirectoryInfo(string path) => new DirectoryInfo(path); public bool Exists(string path) => Directory.Exists(path); public string[] GetFiles(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => Directory.GetFiles(path, searchPattern ?? "*", searchOption); - + public string[] GetDirectories(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => Directory.GetDirectories(path, searchPattern ?? "*", searchOption); @@ -47,5 +56,10 @@ public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); + + public string[] GetProjFiles(string path) + { + return Directory.GetFiles(path).Where(filePath => _projFileExtensions.Contains(Path.GetExtension(filePath).ToLower())).ToArray(); + } } } diff --git a/src/AWS.Deploy.Constants/CDK.cs b/src/AWS.Deploy.Constants/CDK.cs index 61741aa20..e2a7dcc2e 100644 --- a/src/AWS.Deploy.Constants/CDK.cs +++ b/src/AWS.Deploy.Constants/CDK.cs @@ -19,12 +19,8 @@ internal static class CDK public static string ProjectsDirectory => Path.Combine(DeployToolWorkspaceDirectoryRoot, "Projects"); /// - /// Minimum version of CDK CLI to check before starting the deployment + /// Default version of CDK CLI /// - /// - /// Currently the version is hardcoded by design. - /// In coming iterations, this will be dynamically calculated based on the package references used in the CDK App csproj files. - /// - public static readonly Version MinimumCDKVersion = Version.Parse("1.107.0"); + public static readonly Version DefaultCDKVersion = Version.Parse("1.107.0"); } } diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKVersionDetector.cs b/src/AWS.Deploy.Orchestration/CDK/CDKVersionDetector.cs new file mode 100644 index 000000000..25647485e --- /dev/null +++ b/src/AWS.Deploy.Orchestration/CDK/CDKVersionDetector.cs @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace AWS.Deploy.Orchestration.CDK +{ + /// + /// Detects the CDK version by parsing the csproj files + /// + public interface ICDKVersionDetector + { + /// + /// Parses the given csproj file and returns the highest version among Amazon.CDK.* dependencies. + /// + /// C# project file path. + /// Highest version among Amazon.CDK.* dependencies. + Version Detect(string csprojPath); + + /// + /// This is convenience method that uses method to detect highest version among Amazon.CDK.* dependencies + /// in all the csproj files + /// + /// C# project file paths. + /// Highest version among Amazon.CDK.* dependencies in all . + Version Detect(IEnumerable csprojPaths); + } + + public class CDKVersionDetector : ICDKVersionDetector + { + private const string AMAZON_CDK_PACKAGE_REFERENCE_PREFIX = "Amazon.CDK"; + + public Version Detect(string csprojPath) + { + var content = File.ReadAllText(csprojPath); + var document = XDocument.Parse(content); + var cdkVersion = Constants.CDK.DefaultCDKVersion; + + foreach (var node in document.DescendantNodes()) + { + if (node is not XElement element || element.Name.ToString() != "PackageReference") + { + continue; + } + + var includeAttribute = element.Attribute("Include"); + if (includeAttribute == null) + { + continue; + } + + if (!includeAttribute.Value.Equals(AMAZON_CDK_PACKAGE_REFERENCE_PREFIX) && !includeAttribute.Value.StartsWith($"{AMAZON_CDK_PACKAGE_REFERENCE_PREFIX}.")) + { + continue; + } + + var versionAttribute = element.Attribute("Version"); + if (versionAttribute == null) + { + continue; + } + + var version = new Version(versionAttribute.Value); + if (version > cdkVersion) + { + cdkVersion = version; + } + } + + return cdkVersion; + } + + public Version Detect(IEnumerable csprojPaths) + { + var cdkVersion = Constants.CDK.DefaultCDKVersion; + + foreach (var csprojPath in csprojPaths) + { + var version = Detect(csprojPath); + if (version > cdkVersion) + { + cdkVersion = version; + } + } + + return cdkVersion; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 187f3c745..b16365855 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -13,8 +13,9 @@ namespace AWS.Deploy.Orchestration { public interface ICdkProjectHandler { - Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); - string CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); + 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); } public class CdkProjectHandler : ICdkProjectHandler @@ -32,14 +33,8 @@ public CdkProjectHandler(IOrchestratorInteractiveService interactiveService, ICo _directoryManager = new DirectoryManager(); } - public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation) + public async Task ConfigureCdkProject(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation) { - var recipeInfo = $"{recommendation.Recipe.Id}_{recommendation.Recipe.Version}"; - var environmentVariables = new Dictionary - { - { EnvironmentVariableKeys.AWS_EXECUTION_ENV, recipeInfo } - }; - string? cdkProjectPath; if (recommendation.Recipe.PersistedDeploymentProject) { @@ -53,16 +48,25 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat { // Create a new temporary CDK project for a new deployment _interactiveService.LogMessageLine($"Generating a {recommendation.Recipe.Name} CDK Project"); - cdkProjectPath = CreateCdkProjectForDeployment(recommendation, session); + cdkProjectPath = CreateCdkProject(recommendation, session); } - + // Write required configuration in appsettings.json var appSettingsBody = _appSettingsBuilder.Build(cloudApplication, recommendation, session); var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); - using (var appSettingsFile = new StreamWriter(appSettingsFilePath)) + await using var appSettingsFile = new StreamWriter(appSettingsFilePath); + await appSettingsFile.WriteAsync(appSettingsBody); + + return cdkProjectPath; + } + + public async Task DeployCdkProject(OrchestratorSession session, string cdkProjectPath, Recommendation recommendation) + { + var recipeInfo = $"{recommendation.Recipe.Id}_{recommendation.Recipe.Version}"; + var environmentVariables = new Dictionary { - await appSettingsFile.WriteAsync(appSettingsBody); - } + { EnvironmentVariableKeys.AWS_EXECUTION_ENV, recipeInfo } + }; _interactiveService.LogMessageLine("Starting deployment of CDK Project"); @@ -70,6 +74,8 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion}", needAwsCredentials: true); + var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); + // 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}\"", @@ -82,7 +88,7 @@ await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{ throw new FailedToDeployCDKAppException("We had an issue deploying your application to AWS. Check the deployment output for more details."); } - public string CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) + public string CreateCdkProject(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) { string? assemblyName; if (string.IsNullOrEmpty(saveCdkDirectoryPath)) @@ -101,7 +107,7 @@ public string CreateCdkProjectForDeployment(Recommendation recommendation, Orche if (string.IsNullOrEmpty(assemblyName)) throw new ArgumentNullException("The assembly name for the CDK deployment project cannot be null"); - + _directoryManager.CreateDirectory(saveCdkDirectoryPath); var templateEngine = new TemplateEngine(); diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index b3f4b0ce8..9e4596667 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -29,12 +29,14 @@ 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; @@ -43,23 +45,27 @@ public Orchestrator( IOrchestratorInteractiveService interactiveService, ICdkProjectHandler cdkProjectHandler, ICDKManager cdkManager, + ICDKVersionDetector cdkVersionDetector, IAWSResourceQueryer awsResourceQueryer, IDeploymentBundleHandler deploymentBundleHandler, ILocalUserSettingsEngine localUserSettingsEngine, IDockerEngine dockerEngine, ICustomRecipeLocator customRecipeLocator, - IList recipeDefinitionPaths) + IList recipeDefinitionPaths, + IDirectoryManager directoryManager) { _session = session; _interactiveService = interactiveService; _cdkProjectHandler = cdkProjectHandler; _cdkManager = cdkManager; + _cdkVersionDetector = cdkVersionDetector; _awsResourceQueryer = awsResourceQueryer; _deploymentBundleHandler = deploymentBundleHandler; _dockerEngine = dockerEngine; _customRecipeLocator = customRecipeLocator; _recipeDefinitionPaths = recipeDefinitionPaths; _localUserSettingsEngine = localUserSettingsEngine; + _directoryManager = directoryManager; } public Orchestrator(OrchestratorSession session, IList recipeDefinitionPaths) @@ -137,16 +143,27 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm _interactiveService.LogMessageLine(string.Empty); _interactiveService.LogMessageLine($"Initiating deployment: {recommendation.Name}"); - if (recommendation.Recipe.DeploymentType == DeploymentTypes.CdkProject) - { - _interactiveService.LogMessageLine("AWS CDK is being configured."); - await _cdkManager.EnsureCompatibleCDKExists(Constants.CDK.DeployToolWorkspaceDirectoryRoot, Constants.CDK.MinimumCDKVersion); - } - switch (recommendation.Recipe.DeploymentType) { case DeploymentTypes.CdkProject: - await _cdkProjectHandler.CreateCdkDeployment(_session, cloudApplication, recommendation); + if (_cdkVersionDetector == null) + { + throw new InvalidOperationException($"{nameof(_cdkVersionDetector)} must not be null."); + } + + if (_directoryManager == null) + { + throw new InvalidOperationException($"{nameof(_directoryManager)} must not be null."); + } + + var cdkProject = await _cdkProjectHandler.ConfigureCdkProject(_session, cloudApplication, recommendation); + _interactiveService.LogMessageLine("AWS CDK is being configured."); + + var projFiles = _directoryManager.GetProjFiles(cdkProject); + var cdkVersion = _cdkVersionDetector.Detect(projFiles); + await _cdkManager.EnsureCompatibleCDKExists(Constants.CDK.DeployToolWorkspaceDirectoryRoot, cdkVersion); + + await _cdkProjectHandler.DeployCdkProject(_session, cdkProject, recommendation); break; default: _interactiveService.LogErrorMessageLine($"Unknown deployment type {recommendation.Recipe.DeploymentType} specified in recipe."); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs index 376177979..9fdccc997 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs @@ -23,6 +23,8 @@ public DirectoryInfo CreateDirectory(string path) public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); + public string[] GetProjFiles(string path) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); public void Delete(string path, bool recursive = false) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 64536e44b..ca42a6b2b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -25,7 +25,7 @@ namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { - public class RecommendationTests + public class RecommendationTests { private readonly CommandLineWrapper _commandLineWrapper; @@ -168,7 +168,7 @@ public async Task GenerateRecommendationsFromCompatibleDeploymentProject() // ACT var recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(saveDirectoryPathEcsProject); - // ASSERT + // ASSERT recommendations.Count.ShouldEqual(1); recommendations[0].Name.ShouldEqual("Custom ECS Fargate Recipe"); recommendations[0].Recipe.Id.ShouldEqual(customEcsRecipeId); @@ -191,7 +191,7 @@ public async Task GenerateRecommendationsFromIncompatibleDeploymentProject() // ACT var recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(saveDirectoryPathEcsProject); - // ASSERT + // ASSERT recommendations.ShouldBeEmpty(); } @@ -213,12 +213,14 @@ private async Task GetOrchestrator(string targetApplicationProject consoleOrchestratorLogger, new Mock().Object, new Mock().Object, + new Mock().Object, new TestToolAWSResourceQueryer(), new Mock().Object, localUserSettingsEngine, new Mock().Object, customRecipeLocator, - new List { RecipeLocator.FindRecipeDefinitionsPath() }); + new List { RecipeLocator.FindRecipeDefinitionsPath() }, + directoryManager); } private async Task GetCustomRecipeId(string recipeFilePath) diff --git a/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj b/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj index 252d707e2..5509e8dc8 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj +++ b/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj @@ -20,6 +20,12 @@ + + + PreserveNewest + + + diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs new file mode 100644 index 000000000..21c6b9b6d --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using System.Linq; +using AWS.Deploy.Orchestration.CDK; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests.CDK +{ + public class CDKVersionDetectorTests + { + private readonly ICDKVersionDetector _cdkVersionDetector; + + public CDKVersionDetectorTests() + { + _cdkVersionDetector = new CDKVersionDetector(); + } + + [Theory] + [InlineData("MixedReferences.csproj", "1.109.0")] + [InlineData("SameReferences.csproj", "1.108.0")] + [InlineData("NoReferences.csproj", "1.107.0")] + public void Detect_CSProjPath(string fileName, string expectedVersion) + { + var csprojPath = Path.Combine("CDK", "CSProjFiles", fileName); + var version = _cdkVersionDetector.Detect(csprojPath); + Assert.Equal(expectedVersion, version.ToString()); + } + + [Fact] + public void Detect_CSProjPaths() + { + var csprojPaths = new [] + { + "MixedReferences.csproj", + "SameReferences.csproj", + "NoReferences.csproj" + }.Select(fileName => Path.Combine("CDK", "CSProjFiles", fileName)); + var version = _cdkVersionDetector.Detect(csprojPaths); + Assert.Equal("1.109.0", version.ToString()); + } + } +} diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj new file mode 100644 index 000000000..4bf1d90d6 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj new file mode 100644 index 000000000..35e3d8428 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj @@ -0,0 +1,2 @@ + + diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj new file mode 100644 index 000000000..3c08c5aa8 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs index 2958f08d7..4e9019f1a 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs @@ -24,6 +24,9 @@ public DirectoryInfo CreateDirectory(string path) public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); + public string[] GetProjFiles(string path) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + public void Delete(string path, bool recursive = false) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); From 309a091e80ba90acb52869dcbdf9645aff609be6 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 19 Aug 2021 11:25:35 -0700 Subject: [PATCH 06/13] fix: remove csproj extension to make sure project files are not restored --- .../CDK/CDKVersionDetectorTests.cs | 12 ++++++------ .../{MixedReferences.csproj => MixedReferences} | 0 .../{NoReferences.csproj => NoReferences} | 0 .../{SameReferences.csproj => SameReferences} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/{MixedReferences.csproj => MixedReferences} (100%) rename test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/{NoReferences.csproj => NoReferences} (100%) rename test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/{SameReferences.csproj => SameReferences} (100%) diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs index 21c6b9b6d..eb677d40e 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKVersionDetectorTests.cs @@ -18,9 +18,9 @@ public CDKVersionDetectorTests() } [Theory] - [InlineData("MixedReferences.csproj", "1.109.0")] - [InlineData("SameReferences.csproj", "1.108.0")] - [InlineData("NoReferences.csproj", "1.107.0")] + [InlineData("MixedReferences", "1.109.0")] + [InlineData("SameReferences", "1.108.0")] + [InlineData("NoReferences", "1.107.0")] public void Detect_CSProjPath(string fileName, string expectedVersion) { var csprojPath = Path.Combine("CDK", "CSProjFiles", fileName); @@ -33,9 +33,9 @@ public void Detect_CSProjPaths() { var csprojPaths = new [] { - "MixedReferences.csproj", - "SameReferences.csproj", - "NoReferences.csproj" + "MixedReferences", + "SameReferences", + "NoReferences" }.Select(fileName => Path.Combine("CDK", "CSProjFiles", fileName)); var version = _cdkVersionDetector.Detect(csprojPaths); Assert.Equal("1.109.0", version.ToString()); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences similarity index 100% rename from test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences.csproj rename to test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/MixedReferences diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences similarity index 100% rename from test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences.csproj rename to test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/NoReferences diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences similarity index 100% rename from test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences.csproj rename to test/AWS.Deploy.Orchestration.UnitTests/CDK/CSProjFiles/SameReferences From bb835c39e2f50bbc7d00f11a12b57684c06e7e13 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Tue, 17 Aug 2021 15:35:51 -0400 Subject: [PATCH 07/13] feat: check if system capabilities are missing before deployment in server mode --- .../Controllers/DeploymentController.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 74b358eb9..577135d0b 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -353,7 +353,7 @@ public async Task GetCompatibility(string sessionId) [HttpPost("session//execute")] [SwaggerOperation(OperationId = "StartDeployment")] [Authorize] - public IActionResult StartDeployment(string sessionId) + public async Task StartDeployment(string sessionId) { var state = _stateServer.Get(sessionId); if (state == null) @@ -368,6 +368,19 @@ public IActionResult StartDeployment(string sessionId) if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); + var systemCapabilityEvaluator = serviceProvider.GetRequiredService(); + + var capabilities = await systemCapabilityEvaluator.EvaluateSystemCapabilities(state.SelectedRecommendation); + + var missingCapabilitiesMessage = ""; + foreach (var capability in capabilities) + { + missingCapabilitiesMessage = $"{missingCapabilitiesMessage}{capability.GetMessage()}{Environment.NewLine}"; + } + + if (capabilities.Any()) + return Problem($"Unable to start deployment due to missing system capabilities.{Environment.NewLine}{missingCapabilitiesMessage}"); + var task = new DeployRecommendationTask(orchestrator, state.ApplicationDetails, state.SelectedRecommendation); state.DeploymentTask = task.Execute(); From ef81cc39bada999f6234690d2af0b3eea6475191 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 19 Aug 2021 16:12:54 -0700 Subject: [PATCH 08/13] fix: Fix issue with TypeHintData not being serialized correctly when saving CDK project --- .../SerializeModelContractResolver.cs | 19 +++++++++++++++++-- .../SaveCdkDeploymentProjectTests.cs | 10 ++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/AWS.Deploy.Common/SerializeModelContractResolver.cs b/src/AWS.Deploy.Common/SerializeModelContractResolver.cs index 741b9e5c0..806b12d1a 100644 --- a/src/AWS.Deploy.Common/SerializeModelContractResolver.cs +++ b/src/AWS.Deploy.Common/SerializeModelContractResolver.cs @@ -20,8 +20,23 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ if (property != null && property.PropertyType != null && property.PropertyName != null && property.PropertyType != typeof(string)) { if (property.PropertyType.GetInterface(nameof(IEnumerable)) != null) - property.ShouldSerialize = - instance => (instance?.GetType()?.GetProperty(property.PropertyName)?.GetValue(instance) as IEnumerable)?.Any() ?? false; + { + property.ShouldSerialize = instance => + { + var instanceValue = instance?.GetType()?.GetProperty(property.PropertyName)?.GetValue(instance); + if (instanceValue is IEnumerable list) + { + return list.Any(); + } + else if(instanceValue is System.Collections.IDictionary map) + { + return map.Count > 0; + } + + + return false; + }; + } } return property ?? throw new ArgumentException(); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs index a9327eed6..90d8a5917 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 using System.IO; +using System.Linq; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Utilities; using Xunit; using Task = System.Threading.Tasks.Task; +using Newtonsoft.Json; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -26,6 +29,13 @@ public async Task DefaultSaveDirectory() var targetApplicationProjectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); await Utilities.CreateCDKDeploymentProject(targetApplicationProjectPath); + + // Verify a bug fix that the IDictionary for TypeHintData was not getting serialized. + var recipeFilePath = Directory.GetFiles(targetApplicationProjectPath + "CDK", "*.recipe", SearchOption.TopDirectoryOnly).FirstOrDefault(); + Assert.True(File.Exists(recipeFilePath)); + var recipeRoot = JsonConvert.DeserializeObject(File.ReadAllText(recipeFilePath)); + var applicationIAMRoleSetting = recipeRoot.OptionSettings.FirstOrDefault(x => string.Equals(x.Id, "ApplicationIAMRole")); + Assert.Equal("ecs-tasks.amazonaws.com", applicationIAMRoleSetting.TypeHintData["ServicePrincipal"]); } [Fact] From f08e9b69c5c9da00cab0aaf1262d339fb478c2c4 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Fri, 20 Aug 2021 12:46:14 -0400 Subject: [PATCH 09/13] feat: Add a filter on the allowed hosts in server mode --- src/AWS.Deploy.CLI/ServerMode/Startup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/AWS.Deploy.CLI/ServerMode/Startup.cs b/src/AWS.Deploy.CLI/ServerMode/Startup.cs index 363e02b77..846ffac66 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Startup.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Startup.cs @@ -19,6 +19,7 @@ using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.ServerMode.Hubs; +using Microsoft.AspNetCore.HostFiltering; namespace AWS.Deploy.CLI.ServerMode { @@ -27,6 +28,9 @@ public class Startup // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.Configure( + options => options.AllowedHosts = new List { "127.0.0.1", "localhost" }); + services.AddCustomServices(); services.AddSingleton(new InMemoryDeploymentSessionStateServer()); From 4f9113527d01dc133448c5af2cee8076d0141162 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 26 Aug 2021 09:23:19 -0700 Subject: [PATCH 10/13] fix: For Blazor WASM recipe increase memory size for BucketDeployment so the underlying CDK Lambda function will run faster to deploy out the web applications files to S3 --- src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/AppStack.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/AppStack.cs b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/AppStack.cs index fffbf3f03..66303949e 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/AppStack.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/AppStack.cs @@ -50,7 +50,8 @@ internal AppStack(Construct scope, RecipeConfiguration recipeConf new BucketDeployment(this, "BlazorDeployment", new BucketDeploymentProps { Sources = new ISource[] { Source.Asset(Path.Combine(recipeConfiguration.DotnetPublishOutputDirectory, "wwwroot")) }, - DestinationBucket = bucket + DestinationBucket = bucket, + MemoryLimit = 4096 }); new CfnOutput(this, "EndpointURL", new CfnOutputProps From 933a9d8a4733c44fbaf0ce4759e94e40afe1c88b Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Mon, 23 Aug 2021 16:07:29 -0400 Subject: [PATCH 11/13] fix: sanitize deploy tool path to prevent command injection --- .../ServerModeSession.cs | 4 ++- .../Utilities/PathExtensions.cs | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/AWS.Deploy.ServerMode.Client/Utilities/PathExtensions.cs diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 3303b606a..8898691cc 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -11,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.Runtime; +using AWS.Deploy.ServerMode.Client.Utilities; using Newtonsoft.Json; namespace AWS.Deploy.ServerMode.Client @@ -114,7 +116,7 @@ public async Task Start(CancellationToken cancellationToken) var deployToolRoot = "dotnet aws"; if (!string.IsNullOrEmpty(_deployToolPath)) { - if (!File.Exists(_deployToolPath)) + if (!PathUtilities.IsDeployToolPathValid(_deployToolPath)) throw new InvalidAssemblyReferenceException("The specified assembly location is invalid."); deployToolRoot = _deployToolPath; diff --git a/src/AWS.Deploy.ServerMode.Client/Utilities/PathExtensions.cs b/src/AWS.Deploy.ServerMode.Client/Utilities/PathExtensions.cs new file mode 100644 index 000000000..4fa745817 --- /dev/null +++ b/src/AWS.Deploy.ServerMode.Client/Utilities/PathExtensions.cs @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using System.Linq; + +namespace AWS.Deploy.ServerMode.Client.Utilities +{ + public class PathUtilities + { + public static bool IsDeployToolPathValid(string deployToolPath) + { + deployToolPath = deployToolPath.Trim(); + + if (string.IsNullOrEmpty(deployToolPath)) + return false; + + if (deployToolPath.StartsWith(@"\\")) + return false; + + if (deployToolPath.Contains("&")) + return false; + + if (Path.GetInvalidPathChars().Any(x => deployToolPath.Contains(x))) + return false; + + if (!File.Exists(deployToolPath)) + return false; + + return true; + } + } +} From aee7863aad7f081c9ed20d30d098d315751f7940 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Tue, 24 Aug 2021 12:37:31 -0400 Subject: [PATCH 12/13] feat: delete temporary CDK project after deployment --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 4 ++-- .../ConsoleInteractiveServiceImpl.cs | 9 +-------- .../CdkProjectHandler.cs | 20 +++++++++++++++++++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 9 ++++++++- .../BlazorWasmTests.cs | 11 +++++++++- .../ConsoleAppTests.cs | 11 +++++++++- .../WebAppNoDockerFileTests.cs | 11 +++++++++- .../WebAppWithDockerFileTests.cs | 11 +++++++++- 8 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 1c6a9528b..d6f6f73bc 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -492,8 +492,8 @@ private Command BuildServerModeCommand() { try { - var toolInteractiveService = new ConsoleInteractiveServiceImpl(input.Diagnostics); - var serverMode = new ServerModeCommand(toolInteractiveService, input.Port, input.ParentPid, input.EncryptionKeyInfoStdIn); + _toolInteractiveService.Diagnostics = input.Diagnostics; + var serverMode = new ServerModeCommand(_toolInteractiveService, input.Port, input.ParentPid, input.EncryptionKeyInfoStdIn); await serverMode.ExecuteAsync(); diff --git a/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs b/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs index 858374ed0..a50bb3c0d 100644 --- a/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs +++ b/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs @@ -7,13 +7,6 @@ namespace AWS.Deploy.CLI { public class ConsoleInteractiveServiceImpl : IToolInteractiveService { - private readonly bool _diagnosticLoggingEnabled; - - public ConsoleInteractiveServiceImpl(bool diagnosticLoggingEnabled = false) - { - _diagnosticLoggingEnabled = diagnosticLoggingEnabled; - } - public string ReadLine() { return Console.ReadLine() ?? string.Empty; @@ -24,7 +17,7 @@ public string ReadLine() public void WriteDebugLine(string? message) { - if (_diagnosticLoggingEnabled) + if (Diagnostics) Console.WriteLine($"DEBUG: {message}"); } diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index b16365855..81c4db0ac 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -16,6 +16,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); + void DeleteTemporaryCdkProject(string cdkProjectPath); } public class CdkProjectHandler : ICdkProjectHandler @@ -113,7 +114,26 @@ public string CreateCdkProject(Recommendation recommendation, OrchestratorSessio var templateEngine = new TemplateEngine(); templateEngine.GenerateCDKProjectFromTemplate(recommendation, session, saveCdkDirectoryPath, assemblyName); + _interactiveService.LogDebugLine($"The CDK Project is saved at: {saveCdkDirectoryPath}"); return saveCdkDirectoryPath; } + + public void DeleteTemporaryCdkProject(string cdkProjectPath) + { + var parentPath = Path.GetFullPath(Constants.CDK.ProjectsDirectory); + cdkProjectPath = Path.GetFullPath(cdkProjectPath); + + if (!cdkProjectPath.StartsWith(parentPath)) + return; + + try + { + _directoryManager.Delete(cdkProjectPath, true); + } + catch (Exception) + { + _interactiveService.LogErrorMessageLine($"We were unable to delete the temporary project that was created for this deployment. Please manually delete it at this location: {cdkProjectPath}"); + } + } } } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 9e4596667..63d5da0aa 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -163,7 +163,14 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm var cdkVersion = _cdkVersionDetector.Detect(projFiles); await _cdkManager.EnsureCompatibleCDKExists(Constants.CDK.DeployToolWorkspaceDirectoryRoot, cdkVersion); - await _cdkProjectHandler.DeployCdkProject(_session, cdkProject, recommendation); + try + { + await _cdkProjectHandler.DeployCdkProject(_session, cdkProject, recommendation); + } + finally + { + _cdkProjectHandler.DeleteTemporaryCdkProject(cdkProject); + } break; default: _interactiveService.LogErrorMessageLine($"Unknown deployment type {recommendation.Recipe.DeploymentType} specified in recipe."); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs index 37ed888d7..6606c33dc 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs @@ -64,12 +64,21 @@ 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 }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName, "--diagnostics" }; await _app.Run(deployArgs); // Verify application is deployed and running Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + var deployStdDebug = _interactiveService.StdDebugReader.ReadAllLines(); + + var tempCdkProject = deployStdDebug.FirstOrDefault(line => line.Trim().Contains("The CDK Project is saved at: "))? + .Split(": ")[1] + .Trim(); + + Assert.NotNull(tempCdkProject); + Assert.False(Directory.Exists(tempCdkProject)); + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); // Example URL string: BlazorWasm5068e7a879d5ee.EndpointURL = http://blazorwasm5068e7a879d5ee-blazorhostc7106839-a2585dcq9xve.s3-website-us-west-2.amazonaws.com/ diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs index 8e0825164..00ebc9793 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 }; + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--stack-name", _stackName, "--diagnostics" }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -83,6 +83,15 @@ public async Task DefaultConfigurations(params string[] components) var logMessages = await _cloudWatchLogsHelper.GetLogMessages(logGroup); Assert.Contains("Hello World!", logMessages); + var deployStdDebug = _interactiveService.StdDebugReader.ReadAllLines(); + + var tempCdkProject = deployStdDebug.FirstOrDefault(line => line.Trim().Contains("The CDK Project is saved at: "))? + .Split(": ")[1] + .Trim(); + + Assert.NotNull(tempCdkProject); + Assert.False(Directory.Exists(tempCdkProject)); + // list var listArgs = new[] { "list-deployments" }; await _app.Run(listArgs); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index f019b50d1..ab570286d 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -62,12 +62,21 @@ 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 }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; await _app.Run(deployArgs); // Verify application is deployed and running Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + var deployStdDebug = _interactiveService.StdDebugReader.ReadAllLines(); + + var tempCdkProject = deployStdDebug.FirstOrDefault(line => line.Trim().Contains("The CDK Project is saved at: "))? + .Split(": ")[1] + .Trim(); + + Assert.NotNull(tempCdkProject); + Assert.False(Directory.Exists(tempCdkProject)); + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); // Example: Endpoint: http://52.36.216.238/ diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index a201d9082..cf98e46c6 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 }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--stack-name", _stackName, "--diagnostics" }; await _app.Run(deployArgs); // Verify application is deployed and running @@ -76,6 +76,15 @@ public async Task DefaultConfigurations() var cluster = await _ecsHelper.GetCluster(_stackName); Assert.Equal("ACTIVE", cluster.Status); + var deployStdDebug = _interactiveService.StdDebugReader.ReadAllLines(); + + var tempCdkProject = deployStdDebug.FirstOrDefault(line => line.Trim().Contains("The CDK Project is saved at: "))? + .Split(": ")[1] + .Trim(); + + Assert.NotNull(tempCdkProject); + Assert.False(Directory.Exists(tempCdkProject)); + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) From eed02cb22d31e986977d7a2a4b835a695aaaea61 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Fri, 20 Aug 2021 16:45:50 -0400 Subject: [PATCH 13/13] feat: make the encryption key info mode the default one --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 4 ++-- .../ServerModeCommandHandlerInput.cs | 2 +- .../Commands/ServerModeCommand.cs | 22 +++++++++---------- .../ServerMode/EncryptionKeyInfo.cs | 21 ++++++++++++------ .../ServerModeSession.cs | 2 +- .../Program.cs | 2 +- .../ServerModeTests.cs | 6 ++--- .../ServerModeAuthTests.cs | 8 +++---- .../ServerModeTests.cs | 4 ++-- 9 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index d6f6f73bc..16d043681 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -484,7 +484,7 @@ private Command BuildServerModeCommand() { serverModeCommand.Add(new Option(new[] { "--port" }, description: "Port the server mode will listen to.")); serverModeCommand.Add(new Option(new[] { "--parent-pid" }, description: "The ID of the process that is launching server mode. Server mode will exit when the parent pid terminates.")); - serverModeCommand.Add(new Option(new[] { "--encryption-keyinfo-stdin" }, description: "If set the cli reads encryption key info from stdin to use for decryption.")); + serverModeCommand.Add(new Option(new[] { "--unsecure-mode" }, description: "If set the cli uses an unsecure mode without encryption.")); serverModeCommand.Add(_optionDiagnosticLogging); } @@ -493,7 +493,7 @@ private Command BuildServerModeCommand() try { _toolInteractiveService.Diagnostics = input.Diagnostics; - var serverMode = new ServerModeCommand(_toolInteractiveService, input.Port, input.ParentPid, input.EncryptionKeyInfoStdIn); + var serverMode = new ServerModeCommand(_toolInteractiveService, input.Port, input.ParentPid, input.UnsecureMode); await serverMode.ExecuteAsync(); diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs index 90203bf98..09d2ad861 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs @@ -12,7 +12,7 @@ public class ServerModeCommandHandlerInput { public int Port { get; set; } public int ParentPid { get; set; } - public bool EncryptionKeyInfoStdIn { get; set; } + public bool UnsecureMode { get; set; } public bool Diagnostics { get; set; } } } diff --git a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs index 834e58748..d14b4aa41 100644 --- a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs @@ -21,14 +21,14 @@ public class ServerModeCommand private readonly IToolInteractiveService _interactiveService; private readonly int _port; private readonly int? _parentPid; - private readonly bool _encryptionKeyInfoStdIn; + private readonly bool _noEncryptionKeyInfo; - public ServerModeCommand(IToolInteractiveService interactiveService, int port, int? parentPid, bool encryptionKeyInfoStdIn) + public ServerModeCommand(IToolInteractiveService interactiveService, int port, int? parentPid, bool noEncryptionKeyInfo) { _interactiveService = interactiveService; _port = port; _parentPid = parentPid; - _encryptionKeyInfoStdIn = encryptionKeyInfoStdIn; + _noEncryptionKeyInfo = noEncryptionKeyInfo; } public async Task ExecuteAsync(CancellationToken cancellationToken = default(CancellationToken)) @@ -85,9 +85,13 @@ private async Task ShutDownHost(IWebHost host, CancellationToken cancellationTok private IEncryptionProvider CreateEncryptionProvider() { IEncryptionProvider encryptionProvider; - if (_encryptionKeyInfoStdIn) + if (_noEncryptionKeyInfo) { - _interactiveService.WriteLine("Waiting on encryption key info from stdin"); + encryptionProvider = new NoEncryptionProvider(); + } + else + { + _interactiveService.WriteLine("Waiting on symmetric key from stdin"); var input = _interactiveService.ReadLine(); var keyInfo = EncryptionKeyInfo.ParseStdInKeyInfo(input); @@ -108,17 +112,13 @@ private IEncryptionProvider CreateEncryptionProvider() encryptionProvider = new AesEncryptionProvider(aes); break; case null: - throw new InvalidEncryptionKeyInfoException("Missing required \"Version\" property in encryption key info"); + throw new InvalidEncryptionKeyInfoException("Missing required \"Version\" property in the symmetric key"); default: - throw new InvalidEncryptionKeyInfoException($"Unsupported encryption key info {keyInfo.Version}"); + throw new InvalidEncryptionKeyInfoException($"Unsupported symmetric key {keyInfo.Version}"); } _interactiveService.WriteLine("Encryption provider enabled"); } - else - { - encryptionProvider = new NoEncryptionProvider(); - } return encryptionProvider; } diff --git a/src/AWS.Deploy.CLI/ServerMode/EncryptionKeyInfo.cs b/src/AWS.Deploy.CLI/ServerMode/EncryptionKeyInfo.cs index 36e04e9f6..82a624955 100644 --- a/src/AWS.Deploy.CLI/ServerMode/EncryptionKeyInfo.cs +++ b/src/AWS.Deploy.CLI/ServerMode/EncryptionKeyInfo.cs @@ -22,7 +22,7 @@ public class EncryptionKeyInfo public string? Version { get; set; } /// - /// Encryption key base 64 encoded + /// Encryption key base 64 encoded /// public string? Key { get; set; } @@ -33,15 +33,22 @@ public class EncryptionKeyInfo public static EncryptionKeyInfo ParseStdInKeyInfo(string input) { - var json = Encoding.UTF8.GetString(Convert.FromBase64String(input)); - var keyInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + try + { + var json = Encoding.UTF8.GetString(Convert.FromBase64String(input)); + var keyInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + + if(string.IsNullOrEmpty(keyInfo.Key)) + { + throw new InvalidEncryptionKeyInfoException("The symmetric key is missing a \"Key\" attribute."); + } - if(string.IsNullOrEmpty(keyInfo.Key)) + return keyInfo; + } + catch (Exception) { - throw new InvalidEncryptionKeyInfoException("Encryption key info is missing \"Key\" property."); + throw new InvalidEncryptionKeyInfoException($"The symmetric key has not been passed to Stdin or is invalid."); } - - return keyInfo; } } } diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 8898691cc..a33859fe0 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -137,7 +137,7 @@ public async Task Start(CancellationToken cancellationToken) var keyInfoStdin = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(keyInfo))); - var command = $"{deployToolRoot} server-mode --port {port} --parent-pid {currentProcessId} --encryption-keyinfo-stdin"; + var command = $"{deployToolRoot} server-mode --port {port} --parent-pid {currentProcessId}"; var startServerTask = _commandLineWrapper.Run(command, keyInfoStdin); _baseUrl = $"http://localhost:{port}"; diff --git a/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs b/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs index b27c30ee6..d6b967b7b 100644 --- a/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs +++ b/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs @@ -15,7 +15,7 @@ static async Task Main(string[] args) { // Start up the server mode to make the swagger.json file available. var portNumber = 5678; - var serverCommand = new ServerModeCommand(new ConsoleInteractiveServiceImpl(), portNumber, null, false); + var serverCommand = new ServerModeCommand(new ConsoleInteractiveServiceImpl(), portNumber, null, true); var cancelSource = new CancellationTokenSource(); _ = serverCommand.ExecuteAsync(cancelSource.Token); try diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index ef0cd9208..beb178896 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -68,7 +68,7 @@ public async Task GetRecommendations() var portNumber = 4000; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, false); + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); var cancelSource = new CancellationTokenSource(); var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); @@ -119,7 +119,7 @@ public async Task GetRecommendationsWithEncryptedCredentials() await interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, true); + var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, false); var cancelSource = new CancellationTokenSource(); var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); @@ -160,7 +160,7 @@ public async Task WebFargateDeploymentNoConfigChanges() var portNumber = 4001; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, false); + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); var cancelSource = new CancellationTokenSource(); var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs index da3ce450d..e5337e9ca 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs @@ -211,7 +211,7 @@ public async Task AuthMissingEncryptionInfoVersion() await interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, true); + var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, false); var cancelSource = new CancellationTokenSource(); @@ -230,7 +230,7 @@ public async Task AuthMissingEncryptionInfoVersion() } Assert.NotNull(actualException); - Assert.Equal("Missing required \"Version\" property in encryption key info", actualException.Message); + Assert.Equal("Missing required \"Version\" property in the symmetric key", actualException.Message); } [Fact] @@ -254,7 +254,7 @@ public async Task AuthEncryptionWithInvalidVersion() await interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, true); + var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, false); var cancelSource = new CancellationTokenSource(); @@ -273,7 +273,7 @@ public async Task AuthEncryptionWithInvalidVersion() } Assert.NotNull(actualException); - Assert.Equal("Unsupported encryption key info not-valid", actualException.Message); + Assert.Equal("Unsupported symmetric key not-valid", actualException.Message); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index 67bea70c9..5018bfcc9 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -17,8 +17,8 @@ public class ServerModeTests [Fact] public async Task TcpPortIsInUseTest() { - var serverModeCommand1 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, false); - var serverModeCommand2 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, false); + var serverModeCommand1 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, true); + var serverModeCommand2 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, true); var serverModeTask1 = serverModeCommand1.ExecuteAsync(); var serverModeTask2 = serverModeCommand2.ExecuteAsync();