diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs new file mode 100644 index 000000000..732f2b5dd --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Amazon.ECR.Model; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.TypeHintData; +using AWS.Deploy.Orchestration.Data; + +namespace AWS.Deploy.CLI.Commands.TypeHints +{ + public class ECRRepositoryCommand : ITypeHintCommand + { + private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly IConsoleUtilities _consoleUtilities; + + public ECRRepositoryCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities) + { + _awsResourceQueryer = awsResourceQueryer; + _consoleUtilities = consoleUtilities; + } + + public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + var repositories = await GetData(); + var currentRepositoryName = recommendation.GetOptionSettingValue(optionSetting); + + var userInputConfiguration = new UserInputConfiguration( + rep => rep.RepositoryName, + rep => rep.RepositoryName.Equals(currentRepositoryName), + currentRepositoryName) + { + AskNewName = true, + }; + + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(repositories, "Select ECR Repository:", userInputConfiguration); + + return userResponse.SelectedOption?.RepositoryName ?? userResponse.NewName + ?? throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.ECRRepositoryPromptForNameReturnedNull, "The user response for an ECR Repository was null"); + } + public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + { + var repositories = await GetData(); + return repositories.Select(x => new TypeHintResource(x.RepositoryName, x.RepositoryName)).ToList(); + } + + private async Task> GetData() + { + return await _awsResourceQueryer.GetECRRepositories(); + } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index 050dad9af..b99748e24 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -59,6 +59,7 @@ public TypeHintCommandFactory(IToolInteractiveService toolInteractiveService, IA { OptionSettingTypeHint.SNSTopicArn, new SNSTopicArnsCommand(awsResourceQueryer, consoleUtilities) }, { OptionSettingTypeHint.S3BucketName, new S3BucketNameCommand(awsResourceQueryer, consoleUtilities) }, { OptionSettingTypeHint.InstanceType, new InstanceTypeCommand(awsResourceQueryer, consoleUtilities) }, + { OptionSettingTypeHint.ECRRepository, new ECRRepositoryCommand(awsResourceQueryer, consoleUtilities)} }; } diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index afb00a8a6..0827d93a6 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -101,6 +101,7 @@ public enum DeployToolErrorCode FailedToCreateElasticBeanstalkStorageLocation = 10007900, UnableToAccessAWSRegion = 10008000, OptInRegionDisabled = 10008100, + ECRRepositoryPromptForNameReturnedNull = 10008200 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs index cc7d20aef..1e149bfc6 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs @@ -30,6 +30,7 @@ public enum OptionSettingTypeHint ExistingIAMRole, ExistingECSCluster, ExistingVpc, - ExistingBeanstalkApplication + ExistingBeanstalkApplication, + ECRRepository }; } diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 9a2f54f82..df3d7d03e 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -45,7 +45,7 @@ public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefiniti DeploymentBundle = new DeploymentBundle(); DeploymentBundleSettings = deploymentBundleSettings; - CollectRecommendationReplacementTokens(Recipe.OptionSettings); + CollectRecommendationReplacementTokens(GetConfigurableOptionSettingItems().ToList()); foreach (var replacement in additionalReplacements) { diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index 9f6ebcd3b..5c98f8442 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -18,8 +18,5 @@ internal static class CLI public const string PROMPT_CHOOSE_DEPLOYMENT_TARGET = "Choose deployment target"; public const string CLI_APP_NAME = "AWS .NET Deployment Tool"; - - public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; - public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; } } diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs index 575269471..123eaf99a 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -6,8 +6,12 @@ namespace AWS.Deploy.Constants { internal static class RecipeIdentifier { + // Recipe IDs + public const string EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment"; + + // Replacement Tokens public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; - public const string EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment"; + public const string REPLACE_TOKEN_ECR_REPOSITORY_NAME = "{DefaultECRRepositoryName}"; } } diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 841b8d58d..d4e09274f 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -61,7 +61,7 @@ public interface IAWSResourceQueryer Task> GetElasticBeanstalkPlatformArns(); Task GetLatestElasticBeanstalkPlatformArn(); Task> GetECRAuthorizationToken(); - Task> GetECRRepositories(List repositoryNames); + Task> GetECRRepositories(List? repositoryNames = null); Task CreateECRRepository(string repositoryName); Task> GetCloudFormationStacks(); Task GetCallerIdentity(string awsRegion); @@ -407,7 +407,7 @@ public async Task> GetECRAuthorizationToken() return response.AuthorizationData; } - public async Task> GetECRRepositories(List repositoryNames) + public async Task> GetECRRepositories(List? repositoryNames = null) { var ecrClient = _awsClientFactory.GetAWSClient(); diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index f27eec6ae..2edd31db0 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -19,7 +19,7 @@ public interface IDeploymentBundleHandler { Task BuildDockerImage(CloudApplication cloudApplication, Recommendation recommendation); Task CreateDotnetPublishZip(Recommendation recommendation); - Task PushDockerImageToECR(CloudApplication cloudApplication, Recommendation recommendation, string sourceTag); + Task PushDockerImageToECR(Recommendation recommendation, string repositoryName, string sourceTag); } public class DeploymentBundleHandler : IDeploymentBundleHandler @@ -71,7 +71,7 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Re return imageTag; } - public async Task PushDockerImageToECR(CloudApplication cloudApplication, Recommendation recommendation, string sourceTag) + public async Task PushDockerImageToECR(Recommendation recommendation, string repositoryName, string sourceTag) { _interactiveService.LogMessageLine(string.Empty); _interactiveService.LogMessageLine("Pushing the docker image to ECR repository..."); @@ -79,7 +79,7 @@ public async Task PushDockerImageToECR(CloudApplication cloudApplication, Recomm await InitiateDockerLogin(); var tagSuffix = sourceTag.Split(":")[1]; - var repository = await SetupECRRepository(cloudApplication.Name.ToLower()); + var repository = await SetupECRRepository(repositoryName); var targetTag = $"{repository.RepositoryUri}:{tagSuffix}"; await TagDockerImage(sourceTag, targetTag); diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index d072f39a7..ff7fa61e1 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -121,18 +121,22 @@ public async Task> GenerateRecommendationsFromSavedDeployme public async Task ApplyAllReplacementTokens(Recommendation recommendation, string cloudApplicationName) { - if (recommendation.ReplacementTokens.ContainsKey(Constants.CLI.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN)) + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN)) { if (_awsResourceQueryer == null) throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(); - recommendation.AddReplacementToken(Constants.CLI.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN, latestPlatform.PlatformArn); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN, latestPlatform.PlatformArn); } - if (recommendation.ReplacementTokens.ContainsKey(Constants.CLI.REPLACE_TOKEN_STACK_NAME)) + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_STACK_NAME)) { // Apply the user entered stack name to the recommendation so that any default settings based on stack name are applied. - recommendation.AddReplacementToken(Constants.CLI.REPLACE_TOKEN_STACK_NAME, cloudApplicationName); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_STACK_NAME, cloudApplicationName); + } + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_ECR_REPOSITORY_NAME)) + { + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_ECR_REPOSITORY_NAME, cloudApplicationName.ToLower()); } } @@ -169,8 +173,8 @@ public async Task CreateContainerDeploymentBundle(CloudApplication cloudAp try { var imageTag = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); - - await _deploymentBundleHandler.PushDockerImageToECR(cloudApplication, recommendation, imageTag); + var respositoryName = recommendation.GetOptionSettingValue(recommendation.GetOptionSetting("ECRRepositoryName")); + await _deploymentBundleHandler.PushDockerImageToECR(recommendation, respositoryName, imageTag); } catch(DockerBuildFailedException ex) { diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Generated/Configurations/AutoScalingConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Generated/Configurations/AutoScalingConfiguration.cs index aaa6ab9eb..9979cfd14 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Generated/Configurations/AutoScalingConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Generated/Configurations/AutoScalingConfiguration.cs @@ -15,11 +15,13 @@ namespace ConsoleAppEcsFargateService.Configurations { public partial class AutoScalingConfiguration { + const int defaultCooldown = 300; + public bool Enabled { get; set; } - public int MinCapacity { get; set; } + public int MinCapacity { get; set; } = 1; - public int MaxCapacity { get; set; } + public int MaxCapacity { get; set; } = 3; public enum ScalingTypeEnum { Cpu, Memory } @@ -27,19 +29,19 @@ public enum ScalingTypeEnum { Cpu, Memory } - public double CpuTypeTargetUtilizationPercent { get; set; } + public double CpuTypeTargetUtilizationPercent { get; set; } = 70; - public int CpuTypeScaleInCooldownSeconds { get; set; } + public int CpuTypeScaleInCooldownSeconds { get; set; } = defaultCooldown; - public int CpuTypeScaleOutCooldownSeconds { get; set; } + public int CpuTypeScaleOutCooldownSeconds { get; set; } = defaultCooldown; - public int MemoryTypeTargetUtilizationPercent { get; set; } + public int MemoryTypeTargetUtilizationPercent { get; set; } = 70; - public int MemoryTypeScaleInCooldownSeconds { get; set; } + public int MemoryTypeScaleInCooldownSeconds { get; set; } = defaultCooldown; - public int MemoryTypeScaleOutCooldownSeconds { get; set; } + public int MemoryTypeScaleOutCooldownSeconds { get; set; } = defaultCooldown; /// A parameterless constructor is needed for /// or the classes will fail to initialize. diff --git a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle index 5a5922fe7..fc7a5be6a 100644 --- a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle +++ b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle @@ -20,6 +20,25 @@ "DefaultValue": "", "AdvancedSetting": true, "Updatable": true + }, + { + "Id": "ECRRepositoryName", + "Name": "ECS Repository Name", + "Description": "Specifies the ECR repository where the docker images will be stored", + "Type": "String", + "TypeHint": "ECRRepository", + "DefaultValue": "{DefaultECRRepositoryName}", + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration" : { + "Regex": "^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$", + "ValidationFailedMessage": "Invalid ECR repository Name. The ECR repository name can only contain lowercase letters, numbers, hyphens(-), dots(.), underscores(_) and forward slashes (/). For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-repository.html#cfn-ecr-repository-repositoryname" + } + } + ] } ] } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs index 17421177c..694f25d45 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -124,6 +124,19 @@ public void ListenerConditionPathPatternValidationTest(string value, bool isVali Validate(optionSettingItem, value, isValid); } + [Theory] + [InlineData("myrepo123", true)] + [InlineData("myrepo123.a/b", true)] + [InlineData("MyRepo", false)] // cannot contain uppercase letters + [InlineData("myrepo123@", false)] // cannot contain @ + [InlineData("myrepo123.a//b", false)] // cannot contain consecutive slashes. + public void ECRRepositoryNameValidationTest(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$")); + Validate(optionSettingItem, value, isValid); + } + private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) { var regexValidatorConfig = new OptionSettingItemValidatorConfig diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs index a7c8192dd..890860915 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs @@ -97,6 +97,15 @@ public async Task DefaultConfigurations(params string[] components) var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + // Arrange input for re-deployment + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings + await _interactiveService.StdInWriter.FlushAsync(); + + // Perform re-deployment + deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + // Arrange input for delete await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete await _interactiveService.StdInWriter.FlushAsync(); diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 5d6b5907b..0230d52d4 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -99,11 +99,11 @@ public async Task PushDockerImage_RepositoryNameCheck() var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); + var repositoryName = "repository"; - var cloudApplication = new CloudApplication("ConsoleAppTask", string.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); - await _deploymentBundleHandler.PushDockerImageToECR(cloudApplication, recommendation, "ConsoleAppTask:latest"); + await _deploymentBundleHandler.PushDockerImageToECR(recommendation, repositoryName, "ConsoleAppTask:latest"); - Assert.Equal(cloudApplication.Name.ToLower(), recommendation.DeploymentBundle.ECRRepositoryName); + Assert.Equal(repositoryName, recommendation.DeploymentBundle.ECRRepositoryName); } [Fact] diff --git a/version.json b/version.json index 34d5787bf..1cccf15ce 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.34", + "version": "0.35", "publicReleaseRefSpec": [ ".*" ],