From 66e8f003c5cb642d6deff5a66d833cd11504feb2 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Wed, 8 Dec 2021 14:53:39 -0500 Subject: [PATCH 1/9] feat: Add input validation to option settings in ECS Fargate based recipes --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 4 +- .../Recipes/Validation/IRecipeValidator.cs | 2 +- .../Recipes/Validation/RecipeValidatorList.cs | 7 +- .../FargateTaskCpuMemorySizeValidator.cs | 31 ++- .../MinMaxConstraintValidator.cs | 43 ++++ .../Recipes/Validation/ValidatorFactory.cs | 3 +- .../ASP.NETAppECSFargate.recipe | 205 +++++++++++++++++- .../ConsoleAppECSFargateService.recipe | 95 +++++++- .../aws-deploy-recipe-schema.json | 28 ++- ...FargateOptionSettingItemValidationTests.cs | 26 +++ 10 files changed, 411 insertions(+), 33 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 5f8be7ab2..b130771f7 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -407,7 +407,7 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us var validatorFailedResults = recommendation.Recipe .BuildValidators() - .Select(validator => validator.Validate(recommendation.Recipe, _session)) + .Select(validator => validator.Validate(recommendation, _session)) .Where(x => !x.IsValid) .ToList(); @@ -682,7 +682,7 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEn var validatorFailedResults = recommendation.Recipe .BuildValidators() - .Select(validator => validator.Validate(recommendation.Recipe, _session)) + .Select(validator => validator.Validate(recommendation, _session)) .Where(x => !x.IsValid) .ToList(); diff --git a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs index 5f12127f7..8eb7a3c31 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs @@ -10,6 +10,6 @@ namespace AWS.Deploy.Common.Recipes.Validation /// public interface IRecipeValidator { - ValidationResult Validate(RecipeDefinition recipe, IDeployToolValidationContext deployValidationContext); + ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs index 79aba9860..1d9230c20 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs @@ -8,6 +8,11 @@ public enum RecipeValidatorList /// /// Must be paired with /// - FargateTaskSizeCpuMemoryLimits + FargateTaskSizeCpuMemoryLimits, + + /// + /// Must be paired with + /// + MinMaxConstraint } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs index 46013f30e..30e41bf38 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs @@ -37,37 +37,36 @@ private static IEnumerable BuildMemoryArray(int start, int end, int incr start += increment; } } + + public string CpuOptionSettingsId { get; set; } = "TaskCpu"; - private static readonly string defaultCpuOptionSettingsId = "TaskCpu"; - private static readonly string defaultMemoryOptionSettingsId = "TaskMemory"; - private static readonly string defaultValidationFailedMessage = - "Cpu value {{cpu}} is not compatible with memory value {{memory}}. Allowed values are {{memoryList}}"; + public string MemoryOptionSettingsId { get; set; } = "TaskMemory"; /// /// Supports replacement tokens {{cpu}}, {{memory}}, and {{memoryList}} /// - public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; - - public string? InvalidCpuValueValidationFailedMessage {get;set;} + public string ValidationFailedMessage { get; set; } = + "Cpu value {{cpu}} is not compatible with memory value {{memory}}. Allowed values are {{memoryList}}"; - public string CpuOptionSettingsId { get; set; } = defaultCpuOptionSettingsId; - public string MemoryOptionSettingsId { get; set; } = defaultMemoryOptionSettingsId; + public string? InvalidCpuValueValidationFailedMessage { get; set; } /// - public ValidationResult Validate(RecipeDefinition recipe, IDeployToolValidationContext deployValidationContext) + public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { - var cpuItem = recipe.OptionSettings.FirstOrDefault(x => x.Id == CpuOptionSettingsId); - var memoryItem = recipe.OptionSettings.FirstOrDefault(x => x.Id == MemoryOptionSettingsId); + string cpu; + string memory; - if (null == cpuItem || null == memoryItem) + try + { + cpu = recommendation.GetOptionSettingValue(recommendation.GetOptionSetting(CpuOptionSettingsId)); + memory = recommendation.GetOptionSettingValue(recommendation.GetOptionSetting(MemoryOptionSettingsId)); + } + catch (OptionSettingItemDoesNotExistException) { return ValidationResult.Failed("Could not find a valid value for Task CPU or Task Memory " + "as part of of the ECS Fargate deployment configuration. Please provide a valid value and try again."); } - var cpu = cpuItem.GetValue(new Dictionary()); - var memory = memoryItem.GetValue(new Dictionary()); - if (!_cpuMemoryMap.ContainsKey(cpu)) { // this could happen, but shouldn't. diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs new file mode 100644 index 000000000..6703e88c7 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// This validator enforces a constraint that the value for one option setting item is less than another option setting item. + /// The setting that holds the minimum value is identified by the 'MinValueOptionSettingsId'. + /// The setting that holds the maximum value is identified by the 'MaxValueOptionSettingsId'. + /// + public class MinMaxConstraintValidator : IRecipeValidator + { + public string MinValueOptionSettingsId { get; set; } = string.Empty; + public string MaxValueOptionSettingsId { get; set; } = string.Empty; + public string ValidationFailedMessage { get; set; } = "The value specified for {{MinValueOptionSettingsId}} must be less than or equal to the value specified for {{MaxValueOptionSettingsId}}"; + + public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) + { + double minVal; + double maxValue; + + try + { + minVal = recommendation.GetOptionSettingValue(recommendation.GetOptionSetting(MinValueOptionSettingsId)); + maxValue = recommendation.GetOptionSettingValue(recommendation.GetOptionSetting(MaxValueOptionSettingsId)); + } + catch (OptionSettingItemDoesNotExistException) + { + return ValidationResult.Failed($"Could not find a valid value for {MinValueOptionSettingsId} or {MaxValueOptionSettingsId}. Please provide a valid value and try again."); + } + + if (minVal <= maxValue) + return ValidationResult.Valid(); + + var failureMessage = + ValidationFailedMessage + .Replace("{{MinValueOptionSettingsId}}", MinValueOptionSettingsId) + .Replace("{{MaxValueOptionSettingsId}}", MaxValueOptionSettingsId); + + return ValidationResult.Failed(failureMessage); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index f25c5ae93..2fc086fe3 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -22,7 +22,8 @@ public static class ValidatorFactory private static readonly Dictionary _recipeValidatorTypeMapping = new() { - { RecipeValidatorList.FargateTaskSizeCpuMemoryLimits, typeof(FargateTaskCpuMemorySizeValidator) } + { RecipeValidatorList.FargateTaskSizeCpuMemoryLimits, typeof(FargateTaskCpuMemorySizeValidator) }, + { RecipeValidatorList.MinMaxConstraint, typeof(MinMaxConstraintValidator) } }; public static IOptionSettingItemValidator[] BuildValidators(this OptionSettingItem optionSettingItem) diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index e26be9b28..5696bd514 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -65,6 +65,14 @@ "Validators": [ { "ValidatorType": "FargateTaskSizeCpuMemoryLimits" + }, + { + "ValidatorType": "MinMaxConstraint", + "Configuration": + { + "MinValueOptionSettingsId": "AutoScaling.MinCapacity", + "MaxValueOptionSettingsId": "AutoScaling.MaxCapacity" + } } ], @@ -414,6 +422,17 @@ "DefaultValue": null, "AdvancedSetting": false, "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "arn:[^:]+:elasticloadbalancing:[^:]*:[0-9]{12}:loadbalancer/.+", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid load balancer ARN. The ARN should contain the arn:[PARTITION]:elasticloadbalancing namespace, followed by the Region of the load balancer, the AWS account ID of the load balancer owner, the loadbalancer namespace, and then the load balancer name. For example, arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188" + } + } + ], "DependsOn": [ { "Id": "LoadBalancer.CreateNew", @@ -428,7 +447,17 @@ "Type": "Int", "DefaultValue": 60, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ] }, { "Id": "HealthCheckPath", @@ -442,11 +471,21 @@ { "Id": "HealthCheckInternval", "Name": "Health Check Interval", - "Description": "The number of consecutive health check successes required before considering an unhealthy target healthy.", + "Description": "The approximate interval, in seconds, between health checks of an individual instance.", "Type": "Int", "DefaultValue": 30, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 5, + "Max": 300 + } + } + ] }, { "Id": "HealthyThresholdCount", @@ -455,7 +494,17 @@ "Type": "Int", "DefaultValue": 5, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 2, + "Max": 10 + } + } + ] }, { "Id": "UnhealthyThresholdCount", @@ -464,7 +513,17 @@ "Type": "Int", "DefaultValue": 2, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 2, + "Max": 10 + } + } + ] }, { "Id": "ListenerConditionType", @@ -493,6 +552,17 @@ "DefaultValue": null, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "^/[a-zA-Z0-9*?&_\\-.$/~\"'@:+]{0,127}$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid listener condition path. The path is case-sensitive and can be up to 128. It starts with '/' and consists of alpha-numeric characters, wildcards (* and ?), & (using &), and the following special characters: '_-.$/~\"'@:+'" + } + } + ], "DependsOn": [ { "Id": "LoadBalancer.CreateNew", @@ -508,10 +578,20 @@ "Id": "ListenerConditionPriority", "Name": "Listener Condition Priority", "Description": "Priority of the condition rule. The value must be unique for the Load Balancer listener.", - "Type": "Double", + "Type": "Int", "DefaultValue": 100, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 50000 + } + } + ], "DependsOn": [ { "Id": "LoadBalancer.CreateNew", @@ -550,6 +630,16 @@ "DefaultValue": 3, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 5000 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -565,6 +655,16 @@ "DefaultValue": 6, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 5000 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -595,11 +695,21 @@ { "Id": "CpuTypeTargetUtilizationPercent", "Name": "CPU Target Utilization", - "Description": "The target cpu utilization that triggers a scaling change.", + "Description": "The target cpu utilization percentage that triggers a scaling change.", "Type": "Double", "DefaultValue": 70, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 100 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -619,6 +729,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -638,6 +758,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -652,11 +782,21 @@ { "Id": "MemoryTypeTargetUtilizationPercent", "Name": "Memory Target Utilization", - "Description": "The target memory utilization that triggers a scaling change.", + "Description": "The target memory utilization percentage that triggers a scaling change.", "Type": "Double", "DefaultValue": 70, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 100 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -676,6 +816,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -695,6 +845,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -714,6 +874,15 @@ "DefaultValue": 1000, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -733,6 +902,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -752,6 +931,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 1a94d8d39..ebfa3bdf6 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -93,7 +93,16 @@ "Validators": [ { "ValidatorType": "FargateTaskSizeCpuMemoryLimits" - }], + }, + { + "ValidatorType": "MinMaxConstraint", + "Configuration": + { + "MinValueOptionSettingsId": "AutoScaling.MinCapacity", + "MaxValueOptionSettingsId": "AutoScaling.MaxCapacity" + } + } + ], "OptionSettings": [ { @@ -440,6 +449,16 @@ "DefaultValue": 1, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 5000 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -455,6 +474,16 @@ "DefaultValue": 3, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 5000 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -484,11 +513,21 @@ { "Id": "CpuTypeTargetUtilizationPercent", "Name": "CPU Target Utilization", - "Description": "The target cpu utilization that triggers a scaling change.", + "Description": "The target cpu utilization percentage that triggers a scaling change.", "Type": "Double", "DefaultValue": 70, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 100 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -508,6 +547,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -527,6 +576,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -541,11 +600,21 @@ { "Id": "MemoryTypeTargetUtilizationPercent", "Name": "Memory Target Utilization", - "Description": "The target memory utilization that triggers a scaling change.", + "Description": "The target memory utilization percentage that triggers a scaling change.", "Type": "Double", "DefaultValue": 70, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 1, + "Max": 100 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -565,6 +634,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", @@ -584,6 +663,16 @@ "DefaultValue": 300, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 3600 + } + } + ], "DependsOn": [ { "Id": "AutoScaling.Enabled", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json index 43ea4479e..9a0e38f4e 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -334,7 +334,8 @@ "ValidatorType": { "type": "string", "enum": [ - "FargateTaskSizeCpuMemoryLimits" + "FargateTaskSizeCpuMemoryLimits", + "MinMaxConstraint" ] } }, @@ -360,6 +361,28 @@ } } } + }, + { + "if": { + "properties": { "ValidatorType": { "const": "MinMaxConstraint" } } + }, + "then": { + "properties": { + "Configuration": { + "properties": { + "ValidationFailedMessage": { + "type": "string" + }, + "MinValueOptionSettingsId":{ + "type": "string" + }, + "MaxValueOptionSettingsId":{ + "type": "string" + } + } + } + } + } } ] } @@ -517,6 +540,9 @@ "Max": { "type": "integer" }, + "AllowEmptyString": { + "type": "boolean" + }, "ValidationFailedMessage": { "type": "string" } 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 f6c4f2995..17421177c 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -98,6 +98,32 @@ public void VpcIdValidationTests(string value, bool isValid) Validate(optionSettingItem, value, isValid); } + [Theory] + [InlineData("arn:aws:elasticloadbalancing:us-east-1:012345678910:loadbalancer/my-load-balancer", true)] + [InlineData("arn:aws:elasticloadbalancing:us-east-1:012345678910:loadbalancer/app/my-load-balancer", true)] + [InlineData("arn:aws:elasticloadbalancing:012345678910:elasticloadbalancing:loadbalancer/my-load-balancer", false)] //missing region + [InlineData("arn:aws:elasticloadbalancing:012345678910:elasticloadbalancing:loadbalancer", false)] //missing resource path + [InlineData("arn:aws:elasticloadbalancing:01234567891:elasticloadbalancing:loadbalancer", false)] //11 digit account ID + public void LoadBalancerArnValidationTest(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:[^:]+:elasticloadbalancing:[^:]*:[0-9]{12}:loadbalancer/.+")); + Validate(optionSettingItem, value, isValid); + } + + [Theory] + [InlineData("/", true)] + [InlineData("/Api/*", true)] + [InlineData("/Api/Path/&*$-/@", true)] + [InlineData("Api/Path", false)] // does not start with '/' + [InlineData("/Api/Path/", false)] // contains invalid character '<' and '>' + public void ListenerConditionPathPatternValidationTest(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("^/[a-zA-Z0-9*?&_\\-.$/~\"'@:+]{0,127}$")); + Validate(optionSettingItem, value, isValid); + } + private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) { var regexValidatorConfig = new OptionSettingItemValidatorConfig From 1564535093d9f70ffee44a2613ac5473a4f4874b Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Fri, 10 Dec 2021 14:39:55 -0500 Subject: [PATCH 2/9] feat: Add input validation to option settings in app runner recipe --- .../ASP.NETAppAppRunner.recipe | 106 ++++++++++++++++-- ...pRunnerOptionSettingItemValidationTests.cs | 91 +++++++++++++++ 2 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index e76f38c36..9abf453d6 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -63,16 +63,36 @@ "TypeHint": "AppRunnerService", "AdvancedSetting": false, "Updatable": false, - "DefaultValue": "{StackName}-service" + "DefaultValue": "{StackName}-service", + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "^([A-Za-z0-9][A-Za-z0-9_-]{3,39})$", + "ValidationFailedMessage": "Invalid service name. The service name must be between 4 and 40 characters in length and can contain uppercase and lowercase letters, numbers, hyphen(-) and underscore(_). It must start with a letter or a number." + } + } + ] }, { "Id": "Port", "Name": "Port", "Description": "The port the container is listening for requests on.", "Type": "Int", - "DefaultValue": "80", + "DefaultValue": 80, "AdvancedSetting": false, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": + { + "Min": 0, + "Max": 51200 + } + } + ] }, { "Id": "StartCommand", @@ -114,6 +134,16 @@ }, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}", + "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-authenticationconfiguration.html" + } + } + ], "DependsOn": [ { "Id": "ApplicationIAMRole.CreateNew", @@ -155,6 +185,16 @@ }, "AdvancedSetting": false, "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}", + "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-authenticationconfiguration.html" + } + } + ], "DependsOn": [ { "Id": "ServiceAccessIAMRole.CreateNew", @@ -206,7 +246,17 @@ "Description": "The ARN of the KMS key that's used for encryption of application logs.", "Type": "String", "AdvancedSetting": true, - "Updatable": false + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": + { + "Regex": "arn:aws(-[\\w]+)*:kms:[a-z\\-]+-[0-9]{1}:[0-9]{12}:key/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "ValidationFailedMessage": "Invalid KMS key ARN. The ARN should contain the arn:[PARTITION]:kms namespace, followed by the region, account ID, and then the key-id. For example - arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab is a valid KMS key ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-encryptionconfiguration.html" + } + } + ] }, { "Id": "HealthCheckProtocol", @@ -242,7 +292,17 @@ "Type": "Int", "DefaultValue": 5, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType":"Range", + "Configuration": + { + "Min": 1, + "Max": 20 + } + } + ] }, { "Id": "HealthCheckTimeout", @@ -251,7 +311,17 @@ "Type": "Int", "DefaultValue": 2, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType":"Range", + "Configuration": + { + "Min": 1, + "Max": 20 + } + } + ] }, { "Id": "HealthCheckHealthyThreshold", @@ -260,7 +330,17 @@ "Type": "Int", "DefaultValue": 3, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType":"Range", + "Configuration": + { + "Min": 1, + "Max": 20 + } + } + ] }, { "Id": "HealthCheckUnhealthyThreshold", @@ -269,7 +349,17 @@ "Type": "Int", "DefaultValue": 3, "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType":"Range", + "Configuration": + { + "Min": 1, + "Max": 20 + } + } + ] } ] } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs new file mode 100644 index 000000000..37faf8dcc --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; +using Should; +using Xunit; + +namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation +{ + public class AppRunnerOptionSettingItemValidationTests + { + [Theory] + [InlineData("abcdef1234", true)] + [InlineData("abc123def45", true)] + [InlineData("abc12-34-56_XZ", true)] + [InlineData("abc_@1323", false)] //invalid character "@" + [InlineData("123*&$_abc_", false)] //invalid characters + [InlineData("-abc123def45", false)] // does not start with a letter or a number + public void AppRunnerServiceNameValidationTests(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + // 4 to 40 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. + optionSettingItem.Validators.Add(GetRegexValidatorConfig("^([A-Za-z0-9][A-Za-z0-9_-]{3,39})$")); + Validate(optionSettingItem, value, isValid); + } + + [Theory] + [InlineData("arn:aws:iam::123456789012:role/S3Access", true)] + [InlineData("arn:aws-cn:iam::123456789012:role/service-role/MyServiceRole", true)] + [InlineData("arn:aws:IAM::123456789012:role/S3Access", false)] //invalid uppercase IAM + [InlineData("arn:aws:iam::1234567890124354:role/S3Access", false)] //invalid account ID + [InlineData("arn:aws-new:iam::123456789012:role/S3Access", false)] // invalid aws partition + [InlineData("arn:aws:iam::123456789012:role", false)] // missing resorce path + public void RoleArnValidationTests(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}")); + Validate(optionSettingItem, value, isValid); + } + + [Theory] + [InlineData("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", true)] + [InlineData("arn:aws-us-gov:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", true)] + [InlineData("arn:aws:kms:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", false)] // missing region + [InlineData("arn:aws:kms:us-east-1:11112222:key/1234abcd-12ab-34cd-56ef-1234567890ab", false)] // invalid account ID + [InlineData("arn:aws:kms:us-west-2:111122223333:key", false)] // missing resource path + [InlineData("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab", false)] // invalid key-id structure + public void KmsKeyArnValidationTests(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:aws(-[\\w]+)*:kms:[a-z\\-]+-[0-9]{1}:[0-9]{12}:key/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")); + Validate(optionSettingItem, value, isValid); + } + + private void Validate(OptionSettingItem optionSettingItem, T value, bool isValid) + { + ValidationFailedException exception = null; + try + { + optionSettingItem.SetValueOverride(value); + } + catch (ValidationFailedException e) + { + exception = e; + } + + if (isValid) + exception.ShouldBeNull(); + else + exception.ShouldNotBeNull(); + } + + private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) + { + var regexValidatorConfig = new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.Regex, + Configuration = new RegexValidator + { + Regex = regex + } + }; + return regexValidatorConfig; + } + } +} From 7bc56fd054e6cebbb272c0178ce81ead2e844129 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Thu, 9 Dec 2021 21:23:08 +0000 Subject: [PATCH 3/9] build: version bump to 0.32 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index cf3dafc99..d49df61a9 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.31", + "version": "0.32", "publicReleaseRefSpec": [ ".*" ], From d480cf78c6a68879ccad4e6525af50b05585203f Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Thu, 16 Dec 2021 13:03:36 -0500 Subject: [PATCH 4/9] fix: Allow spaces in the deploy tool installation location in server-mode command. --- src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 2c6b60c3b..642930f57 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -144,7 +144,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}"; + var command = $"\"{deployToolRoot}\" server-mode --port {port} --parent-pid {currentProcessId}"; var startServerTask = _commandLineWrapper.Run(command, keyInfoStdin); _baseUrl = $"http://localhost:{port}"; From 8588c2235cb4d09619704476d19f05050edd53bb Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Wed, 29 Dec 2021 11:50:35 -0500 Subject: [PATCH 5/9] fix: Improve error message when a user tries to deploy to an existing Beanstalk environment via a new CloudFormation stack --- .../TypeHints/BeanstalkEnvironmentCommand.cs | 25 ++++++++---- .../BeanstalkEnvironmentTypeHintResponse.cs | 27 +++++++++++++ .../BeanstalkEnvironmentConfiguration.cs | 35 +++++++++++++++++ .../Generated/Configurations/Configuration.cs | 8 ++-- .../Generated/Recipe.cs | 5 ++- .../ASP.NETAppElasticBeanstalk.recipe | 39 ++++++++++++++----- .../ElasticBeanStalkDeploymentTest.cs | 2 +- .../ElasticBeanStalkKeyValueDeploymentTest.cs | 2 +- .../ElasticBeanStalkConfigFile.json | 18 ++++----- .../ElasticBeanStalkKeyPairConfigFile.json | 18 ++++----- .../RecommendationTests.cs | 2 +- .../ElasticBeanStalkConfigFile.json | 14 +++---- 12 files changed, 143 insertions(+), 52 deletions(-) create mode 100644 src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkEnvironmentTypeHintResponse.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs index 5aa3f67fb..211858669 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.ElasticBeanstalk.Model; +using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; @@ -39,16 +40,24 @@ private async Task> GetData(Recommendation recommen public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { - var currentValue = recommendation.GetOptionSettingValue(optionSetting); var environments = await GetData(recommendation, optionSetting); + var currentTypeHintResponse = recommendation.GetOptionSettingValue(optionSetting); - var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew( - options: environments.Select(env => env.EnvironmentName), - title: "Select Elastic Beanstalk environment to deploy to:", - askNewName: true, - defaultNewName: currentValue.ToString() ?? ""); - return userResponse.SelectedOption ?? userResponse.NewName - ?? throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.BeanstalkEnvPromptForNameReturnedNull, "The user response for a new environment name was null."); + var userInputConfiguration = new UserInputConfiguration( + env => env.EnvironmentName, + app => app.EnvironmentName.Equals(currentTypeHintResponse?.EnvironmentName), + currentTypeHintResponse.EnvironmentName) + { + AskNewName = true, + }; + + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(environments, "Select Elastic Beanstalk environment to deploy to:", userInputConfiguration); + + return new BeanstalkEnvironmentTypeHintResponse( + userResponse.CreateNew, + userResponse.SelectedOption?.EnvironmentName ?? userResponse.NewName + ?? throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.BeanstalkAppPromptForNameReturnedNull, "The user response for a new environment name was null.") + ); } } } diff --git a/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkEnvironmentTypeHintResponse.cs b/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkEnvironmentTypeHintResponse.cs new file mode 100644 index 000000000..93bc9d717 --- /dev/null +++ b/src/AWS.Deploy.CLI/TypeHintResponses/BeanstalkEnvironmentTypeHintResponse.cs @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using AWS.Deploy.Common.Recipes; + +namespace AWS.Deploy.CLI.TypeHintResponses +{ + /// + /// The class encapsulates + /// type hint response + /// + public class BeanstalkEnvironmentTypeHintResponse : IDisplayable + { + public bool CreateNew { get; set; } + public string EnvironmentName { get; set; } + + public BeanstalkEnvironmentTypeHintResponse( + bool createNew, + string environmentName) + { + CreateNew = createNew; + EnvironmentName = environmentName; + } + + public string ToDisplayString() => EnvironmentName; + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs new file mode 100644 index 000000000..5573ff42a --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/BeanstalkEnvironmentConfiguration.cs @@ -0,0 +1,35 @@ +// 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 AspNetAppElasticBeanstalkLinux.Configurations +{ + public partial class BeanstalkEnvironmentConfiguration + { + public bool CreateNew { get; set; } + public string EnvironmentName { 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 BeanstalkEnvironmentConfiguration() + { + + } +#nullable restore warnings + + public BeanstalkEnvironmentConfiguration( + bool createNew, + string environmentName) + { + CreateNew = createNew; + EnvironmentName = environmentName; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs index b7a1acd3a..f6db3921d 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs @@ -29,9 +29,9 @@ public partial class Configuration public string InstanceType { get; set; } /// - /// The Elastic Beanstalk environment name. + /// The Elastic Beanstalk environment. /// - public string EnvironmentName { get; set; } + public BeanstalkEnvironmentConfiguration BeanstalkEnvironment { get; set; } /// /// The Elastic Beanstalk application. @@ -106,7 +106,7 @@ public Configuration() public Configuration( IAMRoleConfiguration applicationIAMRole, string instanceType, - string environmentName, + BeanstalkEnvironmentConfiguration beanstalkEnvironment, BeanstalkApplicationConfiguration beanstalkApplication, string elasticBeanstalkPlatformArn, string ec2KeyPair, @@ -123,7 +123,7 @@ public Configuration( { ApplicationIAMRole = applicationIAMRole; InstanceType = instanceType; - EnvironmentName = environmentName; + BeanstalkEnvironment = beanstalkEnvironment; BeanstalkApplication = beanstalkApplication; ElasticBeanstalkPlatformArn = elasticBeanstalkPlatformArn; EC2KeyPair = ec2KeyPair; diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs index 1b5453ca3..f111956bd 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs @@ -330,9 +330,12 @@ private void ConfigureBeanstalkEnvironment(Configuration settings) } } + if (!settings.BeanstalkEnvironment.CreateNew) + throw new InvalidOrMissingConfigurationException("The ability to deploy an Elastic Beanstalk application to an existing environment via a new CloudFormation stack is not supported yet."); + BeanstalkEnvironment = new CfnEnvironment(this, nameof(BeanstalkEnvironment), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkEnvironment), this, new CfnEnvironmentProps { - EnvironmentName = settings.EnvironmentName, + EnvironmentName = settings.BeanstalkEnvironment.EnvironmentName, ApplicationName = settings.BeanstalkApplication.ApplicationName, PlatformArn = settings.ElasticBeanstalkPlatformArn, OptionSettings = optionSettingProperties.ToArray(), diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index b926cab41..f92cfca1f 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -115,22 +115,43 @@ ] }, { - "Id": "EnvironmentName", + "Id": "BeanstalkEnvironment", "ParentSettingId": "BeanstalkApplication.ApplicationName", "Name": "Environment Name", "Description": "The Elastic Beanstalk environment name.", - "Type": "String", + "Type": "Object", "TypeHint": "BeanstalkEnvironment", - "DefaultValue": "{StackName}-dev", "AdvancedSetting": false, "Updatable": false, - "Validators": [ + "ChildOptionSettings": [ { - "ValidatorType": "Regex", - "Configuration" : { - "Regex": "^[a-zA-Z0-9][a-zA-Z0-9-]{2,38}[a-zA-Z0-9]$", - "ValidationFailedMessage": "Invalid Environment Name. The Environment Name Must be from 4 to 40 characters in length. The name can contain only letters, numbers, and hyphens. It can't start or end with a hyphen." - } + "Id": "CreateNew", + "Name": "Create new Elastic Beanstalk environment", + "Description": "Do you want to create a new environment?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "EnvironmentName", + "ParentSettingId": "BeanstalkApplication.ApplicationName", + "Name": "Environment Name", + "Description": "The Elastic Beanstalk environment name.", + "Type": "String", + "TypeHint": "BeanstalkEnvironment", + "DefaultValue": "{StackName}-dev", + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration" : { + "Regex": "^[a-zA-Z0-9][a-zA-Z0-9-]{2,38}[a-zA-Z0-9]$", + "ValidationFailedMessage": "Invalid Environment Name. The Environment Name Must be from 4 to 40 characters in length. The name can contain only letters, numbers, and hyphens. It can't start or end with a hyphen." + } + } + ] } ] }, diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs index 01cd0f04d..303e7d65e 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -31,7 +31,7 @@ public void VerifyJsonParsing() var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; Assert.Equal("True", optionSettingDictionary["BeanstalkApplication.CreateNew"]); Assert.Equal("MyApplication", optionSettingDictionary["BeanstalkApplication.ApplicationName"]); - Assert.Equal("MyEnvironment", optionSettingDictionary["EnvironmentName"]); + Assert.Equal("MyEnvironment", optionSettingDictionary["BeanstalkEnvironment.EnvironmentName"]); Assert.Equal("MyInstance", optionSettingDictionary["InstanceType"]); Assert.Equal("SingleInstance", optionSettingDictionary["EnvironmentType"]); Assert.Equal("application", optionSettingDictionary["LoadBalancerType"]); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs index a7556da94..66e025f9f 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkKeyValueDeploymentTest.cs @@ -31,7 +31,7 @@ public void VerifyJsonParsing() var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; Assert.Equal("True", optionSettingDictionary["BeanstalkApplication.CreateNew"]); Assert.Equal("MyApplication", optionSettingDictionary["BeanstalkApplication.ApplicationName"]); - Assert.Equal("MyEnvironment", optionSettingDictionary["EnvironmentName"]); + Assert.Equal("MyEnvironment", optionSettingDictionary["BeanstalkEnvironment.EnvironmentName"]); Assert.Equal("MyInstance", optionSettingDictionary["InstanceType"]); Assert.Equal("SingleInstance", optionSettingDictionary["EnvironmentType"]); Assert.Equal("application", optionSettingDictionary["LoadBalancerType"]); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json index f305572ed..2b996a868 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json @@ -3,28 +3,26 @@ "AWSRegion": "us-west-2", "StackName": "MyAppStack", "RecipeId": "AspNetAppElasticBeanstalkLinux", - "OptionSettingsConfig": - { - "BeanstalkApplication": - { + "OptionSettingsConfig": { + "BeanstalkApplication": { "CreateNew": true, "ApplicationName": "MyApplication" }, - "EnvironmentName": "MyEnvironment", + "BeanstalkEnvironment": { + "CreateNew": true, + "EnvironmentName": "MyEnvironment" + }, "InstanceType": "MyInstance", "EnvironmentType": "SingleInstance", "LoadBalancerType": "application", - "ApplicationIAMRole": - { + "ApplicationIAMRole": { "CreateNew": true }, "ElasticBeanstalkPlatformArn": "MyPlatformArn", - "ElasticBeanstalkManagedPlatformUpdates": - { + "ElasticBeanstalkManagedPlatformUpdates": { "ManagedActionsEnabled": true, "PreferredStartTime": "Mon:12:00", "UpdateLevel": "minor" } } } - \ No newline at end of file diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json index 0889be927..fbd27c8c2 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkKeyPairConfigFile.json @@ -3,24 +3,23 @@ "AWSRegion": "us-west-2", "StackName": "MyAppStack", "RecipeId": "AspNetAppElasticBeanstalkLinux", - "OptionSettingsConfig": - { - "BeanstalkApplication": - { + "OptionSettingsConfig": { + "BeanstalkApplication": { "CreateNew": true, "ApplicationName": "MyApplication" }, - "EnvironmentName": "MyEnvironment", + "BeanstalkEnvironment": { + "CreateNew": true, + "EnvironmentName": "MyEnvironment" + }, "InstanceType": "MyInstance", "EnvironmentType": "SingleInstance", "LoadBalancerType": "application", - "ApplicationIAMRole": - { + "ApplicationIAMRole": { "CreateNew": true }, "ElasticBeanstalkPlatformArn": "MyPlatformArn", - "ElasticBeanstalkManagedPlatformUpdates": - { + "ElasticBeanstalkManagedPlatformUpdates": { "ManagedActionsEnabled": true, "PreferredStartTime": "Mon:12:00", "UpdateLevel": "minor" @@ -30,4 +29,3 @@ } } } - \ No newline at end of file diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index 861473d92..0eb82440c 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -255,7 +255,7 @@ public async Task ApplyProjectNameToSettings() var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); - var beanstalEnvNameSetting = beanstalkRecommendation.Recipe.OptionSettings.FirstOrDefault(x => string.Equals("EnvironmentName", x.Id)); + var beanstalEnvNameSetting = beanstalkRecommendation.GetOptionSetting("BeanstalkEnvironment.EnvironmentName"); beanstalkRecommendation.AddReplacementToken("{StackName}", "MyAppStack"); Assert.Equal("MyAppStack-dev", beanstalkRecommendation.GetOptionSettingValue(beanstalEnvNameSetting)); diff --git a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json index 773f940c4..68d6abac4 100644 --- a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json +++ b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile.json @@ -1,18 +1,18 @@ { "StackName": "ElasticBeanStalk{Suffix}", "RecipeId": "AspNetAppElasticBeanstalkLinux", - "OptionSettingsConfig": - { - "BeanstalkApplication": - { + "OptionSettingsConfig": { + "BeanstalkApplication": { "CreateNew": true, "ApplicationName": "MyApplication{Suffix}" }, - "EnvironmentName": "MyEnvironment{Suffix}", + "BeanstalkEnvironment": { + "CreateNew": true, + "EnvironmentName": "MyEnvironment{Suffix}" + }, "EnvironmentType": "LoadBalanced", "LoadBalancerType": "application", - "ApplicationIAMRole": - { + "ApplicationIAMRole": { "CreateNew": true } } From 424db366de9a26ab00e183333e8e196d26bdd734 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Mon, 10 Jan 2022 15:10:35 -0500 Subject: [PATCH 6/9] chore: update to latest version on AWSSDK.Core --- src/AWS.Deploy.Common/AWS.Deploy.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj index 324a1133c..067479da6 100644 --- a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj +++ b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj @@ -8,7 +8,7 @@ - + From 091c4cc03cdd8820f292695f8b25b5d3f9ba6746 Mon Sep 17 00:00:00 2001 From: Christopher Christou Date: Tue, 11 Jan 2022 13:47:27 -0800 Subject: [PATCH 7/9] Fix typo --- src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index 390948099..eff889504 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -111,7 +111,7 @@ public async Task> EvaluateSystemCapabilities(Recommendat capabilities.Add(new SystemCapability("Docker", false, false) { InstallationUrl = "https://docs.docker.com/engine/install/", - Message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for you OS: https://docs.docker.com/engine/install/" + Message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for your OS: https://docs.docker.com/engine/install/" }); } else if (!systemCapabilities.DockerInfo.DockerContainerType.Equals("linux", StringComparison.OrdinalIgnoreCase)) From 8ef07dc2d1a5d45efd5ea500c701289314a3e411 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 11 Jan 2022 13:26:00 -0500 Subject: [PATCH 8/9] fix: deployment bundle settings not appearing for redeployment --- src/AWS.Deploy.Common/Recommendation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 8180d1170..98c89cf3c 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -29,7 +29,7 @@ public class Recommendation : IUserInputOption public DeploymentBundle DeploymentBundle { get; } - private readonly List DeploymentBundleSettings = new (); + public readonly List DeploymentBundleSettings = new (); private readonly Dictionary _replacementTokens = new(); From e2a1c6da2183ea5357a9a954a9acf298a6e8ccb1 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 12 Jan 2022 16:32:15 -0500 Subject: [PATCH 9/9] fix: InstanceType hint returns 500 in server mode --- .../Commands/TypeHints/InstanceTypeCommand.cs | 10 ++++----- .../ServerModeSession.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs index 098b2b800..33c0ecea7 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs @@ -23,24 +23,24 @@ public InstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtili _consoleUtilities = consoleUtilities; } - private async Task?> GetData(Recommendation recommendation, OptionSettingItem optionSetting) + private async Task?> GetData() { return await _awsResourceQueryer.ListOfAvailableInstanceTypes(); } public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { - var instanceType = await GetData(recommendation, optionSetting); + var instanceType = await GetData(); + return instanceType? + .OrderBy(x => x.InstanceType.Value) .Select(x => new TypeHintResource(x.InstanceType.Value, x.InstanceType.Value)) - .Distinct() - .OrderBy(x => x) .ToList(); } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { - var instanceTypes = await GetData(recommendation, optionSetting); + var instanceTypes = await GetData(); var instanceTypeDefaultValue = recommendation.GetOptionSettingDefaultValue(optionSetting); if (instanceTypes == null) { diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 642930f57..84667a9c9 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -186,6 +186,27 @@ public bool TryGetRestAPIClient(Func> credentialsGenerator, return true; } + /// + /// Builds client based on a deploy tool server mode running on the specified port. + /// + public static bool TryGetRestAPIClient(int port, Aes? aes, Func> credentialsGenerator, out IRestAPIClient? restApiClient) + { + // This ensures that deploy tool CLI doesn't try on the in-use port + // because server availability task will return success response for + // an in-use port + if (!IsPortInUse(port)) + { + restApiClient = null; + throw new PortUnavailableException($"There is no running process on port {port}."); + } + + var baseUrl = $"http://localhost:{port}"; + + var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(credentialsGenerator, aes); + restApiClient = new RestAPIClient(baseUrl, httpClient); + return true; + } + public bool TryGetDeploymentCommunicationClient(out IDeploymentCommunicationClient? deploymentCommunicationClient) { if (_baseUrl == null || _aes == null)