From 81c35f6455d43a10b08baa8efc6e68e0934bd06e Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Wed, 10 Aug 2022 15:05:24 -0400 Subject: [PATCH] fix: deployment workflow fails if there is no default vpc --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 2 + .../TypeHints/ElasticBeanstalkVpcCommand.cs | 190 ++++++++++++++++++ .../Commands/TypeHints/ExistingVpcCommand.cs | 36 +--- .../TypeHints/TypeHintCommandFactory.cs | 1 + .../Commands/TypeHints/VPCConnectorCommand.cs | 26 ++- .../Commands/TypeHints/VpcCommand.cs | 46 ++--- .../Extensions/TypeHintUtilities.cs | 27 +++ .../ElasticBeanstalkVpcTypeHintResponse.cs | 22 ++ .../VPCConnectorTypeHintResponse.cs | 1 + .../OptionSettingItem.ValueOverride.cs | 32 ++- .../Recipes/OptionSettingItem.cs | 8 +- .../Recipes/OptionSettingTypeHint.cs | 3 +- .../OptionSettingItemValidatorList.cs | 6 +- .../VpcExistsValidator.cs | 78 +++++++ .../Recipes/Validation/ValidatorFactory.cs | 3 +- src/AWS.Deploy.Common/Recommendation.cs | 8 +- src/AWS.Deploy.Constants/RecipeIdentifier.cs | 2 + .../Data/AWSResourceQueryer.cs | 2 +- src/AWS.Deploy.Orchestration/Orchestrator.cs | 18 +- .../RecommendationEngine.cs | 4 +- .../AWSElasticBeanstalkHandler.cs | 2 +- .../VPCConnectorConfiguration.cs | 5 + .../AspNetAppAppRunner/Generated/Recipe.cs | 53 ++++- .../Configurations/VPCConfiguration.cs | 5 + .../Generated/Recipe.cs | 88 ++++++-- .../Configurations/VPCConfiguration.cs | 5 + .../Generated/Recipe.cs | 89 ++++++-- .../ASP.NETAppAppRunner.recipe | 44 +++- .../ASP.NETAppECSFargate.recipe | 30 ++- .../ASP.NETAppElasticBeanstalkLinux.recipe | 45 ++++- .../ASP.NETAppElasticBeanstalkWindows.recipe | 45 ++++- .../ConsoleAppECSFargateScheduleTask.recipe | 30 ++- .../ConsoleAppECSFargateService.recipe | 30 ++- .../aws-deploy-recipe-schema.json | 31 ++- .../DockerfilePathValidationTests.cs | 2 +- ...FargateOptionSettingItemValidationTests.cs | 2 +- .../DeploymentBundleHandlerTests.cs | 12 +- .../GetOptionSettingTests.cs | 25 ++- .../RecommendationTests.cs | 3 + .../VPCConnectorCommandTest.cs | 1 + .../DeployedApplicationQueryerTests.cs | 4 +- 41 files changed, 917 insertions(+), 149 deletions(-) create mode 100644 src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs create mode 100644 src/AWS.Deploy.CLI/Extensions/TypeHintUtilities.cs create mode 100644 src/AWS.Deploy.CLI/TypeHintResponses/ElasticBeanstalkVpcTypeHintResponse.cs create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VpcExistsValidator.cs diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index a99e9d837..94010ba5c 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -343,6 +343,8 @@ private async Task GetSelectedRecommendationFromPreviousDeployme else previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication); + await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, deployedApplication.Name); + selectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(selectedRecommendation, previousSettings); var header = $"Loading {deployedApplication.DisplayName} settings:"; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs new file mode 100644 index 000000000..aa5878df4 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs @@ -0,0 +1,190 @@ +// 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.EC2.Model; +using Amazon.ECS.Model; +using AWS.Deploy.CLI.Extensions; +using AWS.Deploy.CLI.TypeHintResponses; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.TypeHintData; +using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.Data; +using Newtonsoft.Json; + +namespace AWS.Deploy.CLI.Commands.TypeHints +{ + /// + /// The type hint orchestrates the VPC object in Elastic Beanstalk environments. + /// + public class ElasticBeanstalkVpcCommand : ITypeHintCommand + { + private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly IConsoleUtilities _consoleUtilities; + private readonly IToolInteractiveService _toolInteractiveService; + private readonly IOptionSettingHandler _optionSettingHandler; + + public ElasticBeanstalkVpcCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IToolInteractiveService toolInteractiveService, IOptionSettingHandler optionSettingHandler) + { + _awsResourceQueryer = awsResourceQueryer; + _consoleUtilities = consoleUtilities; + _toolInteractiveService = toolInteractiveService; + _optionSettingHandler = optionSettingHandler; + } + + private async Task> GetData() + { + return await _awsResourceQueryer.GetListOfVpcs(); + } + + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + { + var vpcs = await GetData(); + var resourceTable = new TypeHintResourceTable(); + + resourceTable.Rows = vpcs.ToDictionary(x => x.VpcId, x => x.GetDisplayableVpc()).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); + + return resourceTable; + } + + public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + _toolInteractiveService.WriteLine(); + + // Ask user if they want to Use a VPC + var useVpcOptionSetting = optionSetting.ChildOptionSettings.First(x => x.Id.Equals("UseVPC")); + var useVpcValue = _optionSettingHandler.GetOptionSettingValue(recommendation, useVpcOptionSetting) ?? "false"; + var useVpcAnswer = _consoleUtilities.AskYesNoQuestion(useVpcOptionSetting.Description, useVpcValue); + var useVpc = useVpcAnswer == YesNo.Yes; + + // If user doesn't want to Use VPC, no need to continue + if (!useVpc) + return new ElasticBeanstalkVpcTypeHintResponse() + { + UseVPC = false + }; + + // Retrieve all the available VPCs + var vpcs = await GetData(); + + // If there are no VPCs, create a new one + if (!vpcs.Any()) + { + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteLine("There are no VPCs in the selected account. The only option is to create a new one."); + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = true + }; + } + + // Ask user to select a VPC from the available ones + _toolInteractiveService.WriteLine(); + var currentVpcTypeHintResponse = optionSetting.GetTypeHintData(); + var vpcOptionSetting = optionSetting.ChildOptionSettings.First(x => x.Id.Equals("VpcId")); + var currentVpcValue = _optionSettingHandler.GetOptionSettingValue(recommendation, vpcOptionSetting).ToString(); + var userInputConfigurationVPCs = new UserInputConfiguration( + idSelector: vpc => vpc.VpcId, + displaySelector: vpc => vpc.GetDisplayableVpc(), + defaultSelector: vpc => + !string.IsNullOrEmpty(currentVpcTypeHintResponse?.VpcId) + ? vpc.VpcId == currentVpcTypeHintResponse.VpcId + : vpc.IsDefault) + { + CanBeEmpty = false, + CreateNew = true + }; + var vpc = _consoleUtilities.AskUserToChooseOrCreateNew(vpcs, "Select a VPC:", userInputConfigurationVPCs); + + // Create a new VPC if the user wants to do that + if (vpc.CreateNew) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = true + }; + + // If for some reason an option was not selected, don't use a VPC + if (vpc.SelectedOption == null) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = false + }; + + // Retrieve available Subnets based on the selected VPC + var availableSubnets = (await _awsResourceQueryer.DescribeSubnets(vpc.SelectedOption.VpcId)).OrderBy(x => x.SubnetId).ToList(); + + // If there are no subnets, don't use a VPC + if (!availableSubnets.Any()) + { + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteLine("The selected VPC does not have any Subnets. Please select a VPC with Subnets."); + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = false + }; + } + + // Ask user to select subnets based on the selected VPC + var userInputConfigurationSubnets = new UserInputConfiguration( + idSelector: subnet => subnet.SubnetId, + displaySelector: subnet => $"{subnet.SubnetId.PadRight(24)} | {subnet.VpcId.PadRight(21)} | {subnet.AvailabilityZone}", + defaultSelector: subnet => false) + { + CanBeEmpty = false, + CreateNew = false + }; + var subnetsOptionSetting = optionSetting.ChildOptionSettings.First(x => x.Id.Equals("Subnets")); + _toolInteractiveService.WriteLine($"{subnetsOptionSetting.Id}:"); + _toolInteractiveService.WriteLine(subnetsOptionSetting.Description); + var subnets = _consoleUtilities.AskUserForList(userInputConfigurationSubnets, availableSubnets, subnetsOptionSetting, recommendation); + + // Retrieve available security groups based on the selected VPC + var availableSecurityGroups = (await _awsResourceQueryer.DescribeSecurityGroups(vpc.SelectedOption.VpcId)).OrderBy(x => x.VpcId).ToList(); + if (!availableSecurityGroups.Any()) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = false, + VpcId = vpc.SelectedOption.VpcId, + Subnets = subnets + }; + + // Get the length of the longest group name to do padding when displaying the security groups + var groupNamePadding = 0; + availableSecurityGroups.ForEach(x => + { + if (x.GroupName.Length > groupNamePadding) + groupNamePadding = x.GroupName.Length; + }); + + // Ask user to select security groups + var userInputConfigurationSecurityGroups = new UserInputConfiguration( + idSelector: securityGroup => securityGroup.GroupId, + displaySelector: securityGroup => $"{securityGroup.GroupName.PadRight(groupNamePadding)} | {securityGroup.GroupId.PadRight(20)} | {securityGroup.VpcId}", + defaultSelector: securityGroup => false) + { + CanBeEmpty = false, + CreateNew = false + }; + var securityGroupsOptionSetting = optionSetting.ChildOptionSettings.First(x => x.Id.Equals("SecurityGroups")); + _toolInteractiveService.WriteLine($"{securityGroupsOptionSetting.Id}:"); + _toolInteractiveService.WriteLine(securityGroupsOptionSetting.Description); + var securityGroups = _consoleUtilities.AskUserForList(userInputConfigurationSecurityGroups, availableSecurityGroups, securityGroupsOptionSetting, recommendation); + + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = false, + VpcId = vpc.SelectedOption.VpcId, + Subnets = subnets, + SecurityGroups = securityGroups + }; + } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs index 513973849..d1e3ba2b1 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.EC2.Model; +using AWS.Deploy.CLI.Extensions; using AWS.Deploy.Common; using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; @@ -13,6 +14,9 @@ namespace AWS.Deploy.CLI.Commands.TypeHints { + /// + /// The type hint lists existing VPC in an account for an option setting of type . + /// public class ExistingVpcCommand : ITypeHintCommand { private readonly IAWSResourceQueryer _awsResourceQueryer; @@ -37,21 +41,7 @@ public async Task GetResources(Recommendation recommendat var resourceTable = new TypeHintResourceTable(); - resourceTable.Rows = vpcs.ToDictionary(x => x.VpcId, x => - { - var name = x.Tags?.FirstOrDefault(x => x.Key == "Name")?.Value ?? string.Empty; - var namePart = - string.IsNullOrEmpty(name) - ? "" - : $" ({name}) "; - - var isDefaultPart = - x.IsDefault - ? " *** Account Default VPC ***" - : ""; - - return $"{x.VpcId}{namePart}{isDefaultPart}"; - }).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); + resourceTable.Rows = vpcs.ToDictionary(x => x.VpcId, x => x.GetDisplayableVpc()).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); return resourceTable; } @@ -63,21 +53,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var userInputConfig = new UserInputConfiguration( idSelector: vpc => vpc.VpcId, - displaySelector: vpc => - { - var name = vpc.Tags?.FirstOrDefault(x => x.Key == "Name")?.Value ?? string.Empty; - var namePart = - string.IsNullOrEmpty(name) - ? "" - : $" ({name}) "; - - var isDefaultPart = - vpc.IsDefault - ? " *** Account Default VPC ***" - : ""; - - return $"{vpc.VpcId}{namePart}{isDefaultPart}"; - }, + displaySelector: vpc => vpc.GetDisplayableVpc(), defaultSelector: vpc => !string.IsNullOrEmpty(currentVpcValue) ? vpc.VpcId == currentVpcValue diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index ccc7d80f7..148f7b18c 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -71,6 +71,7 @@ public TypeHintCommandFactory(IServiceProvider serviceProvider, IToolInteractive { OptionSettingTypeHint.ExistingSecurityGroups, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.VPCConnector, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.FilePath, ActivatorUtilities.CreateInstance(serviceProvider) }, + { OptionSettingTypeHint.ElasticBeanstalkVpc, ActivatorUtilities.CreateInstance(serviceProvider) }, }; } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs index 64024da77..648d4e656 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs @@ -76,6 +76,30 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt _toolInteractiveService.WriteLine(); if (createNewVPCConnector) { + var availableVpcs = await _awsResourceQueryer.GetListOfVpcs(); + if (!availableVpcs.Any()) + { + _toolInteractiveService.WriteLine("Your account does not contain a VPC, so we will create one for you and assign its Subnets and Security Groups to the VPC Connector."); + return new VPCConnectorTypeHintResponse + { + UseVPCConnector = true, + CreateNew = true, + CreateNewVpc = true + }; + } + + var createNewVpcOptionSetting = optionSetting.ChildOptionSettings.First(x => x.Id.Equals("CreateNewVpc")); + var createNewVpcOptionSettingValue = _optionSettingHandler.GetOptionSettingValue(recommendation, createNewVpcOptionSetting) ?? "false"; + var createNewVpcAnswer = _consoleUtilities.AskYesNoQuestion(createNewVpcOptionSetting.Description, createNewVpcOptionSettingValue); + var createNewVpc = createNewVpcAnswer == YesNo.Yes; + if (createNewVpc) + return new VPCConnectorTypeHintResponse + { + UseVPCConnector = true, + CreateNew = true, + CreateNewVpc = true + }; + _toolInteractiveService.WriteLine("In order to create a new VPC Connector, you need to select 1 or more Subnets as well as 1 or more Security groups."); _toolInteractiveService.WriteLine(); @@ -89,7 +113,6 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt CanBeEmpty = false, CreateNew = false }; - var availableVpcs = await _awsResourceQueryer.GetListOfVpcs(); var vpc = _consoleUtilities.AskUserToChooseOrCreateNew(availableVpcs, "Select a VPC:", userInputConfigurationVPCs); if (vpc.SelectedOption == null) @@ -136,6 +159,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt { UseVPCConnector = true, CreateNew = true, + CreateNewVpc = false, VpcId = vpc.SelectedOption.VpcId, Subnets = subnets, SecurityGroups = securityGroups diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs index 6f023ee63..f84a5f75b 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.EC2.Model; using Amazon.ECS.Model; +using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; using AWS.Deploy.Common.Data; @@ -17,15 +18,20 @@ namespace AWS.Deploy.CLI.Commands.TypeHints { + /// + /// The type hint orchestrates the VPC object in ECS Fargate environments. + /// public class VpcCommand : ITypeHintCommand { private readonly IAWSResourceQueryer _awsResourceQueryer; private readonly IConsoleUtilities _consoleUtilities; + private readonly IToolInteractiveService _toolInteractiveService; - public VpcCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities) + public VpcCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IToolInteractiveService toolInteractiveService) { _awsResourceQueryer = awsResourceQueryer; _consoleUtilities = consoleUtilities; + _toolInteractiveService = toolInteractiveService; } private async Task> GetData() @@ -38,20 +44,7 @@ public async Task GetResources(Recommendation recommendat var vpcs = await GetData(); var resourceTable = new TypeHintResourceTable(); - resourceTable.Rows = vpcs.ToDictionary(x => x.VpcId, x => { - var name = x.Tags?.FirstOrDefault(x => x.Key == "Name")?.Value ?? string.Empty; - var namePart = - string.IsNullOrEmpty(name) - ? "" - : $" ({name}) "; - - var isDefaultPart = - x.IsDefault - ? " *** Account Default VPC ***" - : ""; - - return $"{x.VpcId}{namePart}{isDefaultPart}"; - }).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); + resourceTable.Rows = vpcs.ToDictionary(x => x.VpcId, x => x.GetDisplayableVpc()).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); return resourceTable; } @@ -62,23 +55,16 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var vpcs = await GetData(); + if (!vpcs.Any()) + { + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteLine("There are no VPCs in the selected account. The only option is to create a new one."); + return new VpcTypeHintResponse(false, true, string.Empty); + } + var userInputConfig = new UserInputConfiguration( idSelector: vpc => vpc.VpcId, - displaySelector: vpc => - { - var name = vpc.Tags?.FirstOrDefault(x => x.Key == "Name")?.Value ?? string.Empty; - var namePart = - string.IsNullOrEmpty(name) - ? "" - : $" ({name}) "; - - var isDefaultPart = - vpc.IsDefault - ? " *** Account Default VPC ***" - : ""; - - return $"{vpc.VpcId}{namePart}{isDefaultPart}"; - }, + displaySelector: vpc => vpc.GetDisplayableVpc(), defaultSelector: vpc => !string.IsNullOrEmpty(currentVpcTypeHintResponse?.VpcId) ? vpc.VpcId == currentVpcTypeHintResponse.VpcId diff --git a/src/AWS.Deploy.CLI/Extensions/TypeHintUtilities.cs b/src/AWS.Deploy.CLI/Extensions/TypeHintUtilities.cs new file mode 100644 index 000000000..1114df174 --- /dev/null +++ b/src/AWS.Deploy.CLI/Extensions/TypeHintUtilities.cs @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using Amazon.EC2.Model; + +namespace AWS.Deploy.CLI.Extensions +{ + public static class TypeHintUtilities + { + public static string GetDisplayableVpc(this Vpc vpc) + { + var name = vpc.Tags?.FirstOrDefault(x => x.Key == "Name")?.Value ?? string.Empty; + var namePart = + string.IsNullOrEmpty(name) + ? "" + : $" ({name}) "; + + var isDefaultPart = + vpc.IsDefault + ? " *** Account Default VPC ***" + : ""; + + return $"{vpc.VpcId}{namePart}{isDefaultPart}"; + } + } +} diff --git a/src/AWS.Deploy.CLI/TypeHintResponses/ElasticBeanstalkVpcTypeHintResponse.cs b/src/AWS.Deploy.CLI/TypeHintResponses/ElasticBeanstalkVpcTypeHintResponse.cs new file mode 100644 index 000000000..ce5ce56e5 --- /dev/null +++ b/src/AWS.Deploy.CLI/TypeHintResponses/ElasticBeanstalkVpcTypeHintResponse.cs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using AWS.Deploy.Common.Recipes; + +namespace AWS.Deploy.CLI.TypeHintResponses +{ + /// + /// type hint response + /// + public class ElasticBeanstalkVpcTypeHintResponse : IDisplayable + { + public bool UseVPC { get; set; } + public bool CreateNew { get; set; } + public string? VpcId { get; set; } + public SortedSet Subnets { get; set; } = new SortedSet(); + public SortedSet SecurityGroups { get; set; } = new SortedSet(); + + public string? ToDisplayString() => null; + } +} diff --git a/src/AWS.Deploy.CLI/TypeHintResponses/VPCConnectorTypeHintResponse.cs b/src/AWS.Deploy.CLI/TypeHintResponses/VPCConnectorTypeHintResponse.cs index c0243814d..c5ea32983 100644 --- a/src/AWS.Deploy.CLI/TypeHintResponses/VPCConnectorTypeHintResponse.cs +++ b/src/AWS.Deploy.CLI/TypeHintResponses/VPCConnectorTypeHintResponse.cs @@ -15,6 +15,7 @@ public class VPCConnectorTypeHintResponse : IDisplayable public string? VpcConnectorId { get; set; } public bool UseVPCConnector { get; set; } public bool CreateNew { get; set; } + public bool CreateNewVpc { get; set; } public string? VpcId { get; set; } public SortedSet Subnets { get; set; } = new SortedSet(); public SortedSet SecurityGroups { get; set; } = new SortedSet(); diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 6b4697b47..d6a170e74 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -13,14 +13,14 @@ namespace AWS.Deploy.Common.Recipes /// , and methods public partial class OptionSettingItem { - public T? GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null) + public T? GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null) { var value = GetValue(replacementTokens, displayableOptionSettings); return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value)); } - public object GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null) + public object GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null) { if (_value != null) { @@ -60,7 +60,7 @@ public object GetValue(IDictionary replacementTokens, IDictionar return DefaultValue; } - public T? GetDefaultValue(IDictionary replacementTokens) + public T? GetDefaultValue(IDictionary replacementTokens) { var value = GetDefaultValue(replacementTokens); if (value == null) @@ -71,7 +71,7 @@ public object GetValue(IDictionary replacementTokens, IDictionar return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value)); } - public object? GetDefaultValue(IDictionary replacementTokens) + public object? GetDefaultValue(IDictionary replacementTokens) { if (DefaultValue == null) { @@ -179,14 +179,30 @@ public async Task SetValue(IOptionSettingHandler optionSettingHandler, object va } } - private string ApplyReplacementTokens(IDictionary replacementTokens, string defaultValue) + private object ApplyReplacementTokens(IDictionary replacementTokens, object defaultValue) { - foreach (var token in replacementTokens) + var defaultValueString = defaultValue?.ToString(); + if (!string.IsNullOrEmpty(defaultValueString)) { - defaultValue = defaultValue.Replace(token.Key, token.Value); + if (Type != OptionSettingValueType.String) + { + foreach (var token in replacementTokens) + { + if (defaultValueString.Equals(token.Key)) + defaultValue = token.Value; + } + } + else + { + foreach (var token in replacementTokens) + { + defaultValueString = defaultValueString.Replace(token.Key, token.Value.ToString()); + } + defaultValue = defaultValueString; + } } - return defaultValue; + return defaultValue ?? string.Empty; } } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index b9cd04b6a..14f3ab010 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -19,22 +19,22 @@ public interface IOptionSettingItem /// /// Retrieve the value of an as a specified type. /// - T? GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null); + T? GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null); /// /// Retrieve the value of an as an object. /// - object GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null); + object GetValue(IDictionary replacementTokens, IDictionary? displayableOptionSettings = null); /// /// Retrieve the default value of an as a specified type. /// - T? GetDefaultValue(IDictionary replacementTokens); + T? GetDefaultValue(IDictionary replacementTokens); /// /// Retrieve the default value of an as an object. /// - object? GetDefaultValue(IDictionary replacementTokens); + object? GetDefaultValue(IDictionary replacementTokens); /// /// Set the value of an while validating the provided input. diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs index b1f8e888e..4d9690238 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs @@ -38,6 +38,7 @@ public enum OptionSettingTypeHint ExistingSubnets, ExistingSecurityGroups, VPCConnector, - FilePath + FilePath, + ElasticBeanstalkVpc }; } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs index c0f8ab2a4..943fc503f 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs @@ -68,6 +68,10 @@ public enum OptionSettingItemValidatorList /// /// Must be paired with /// - VPCSubnetsInDifferentAZs + VPCSubnetsInDifferentAZs, + /// + /// Must be paired with + /// + VpcExists } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VpcExistsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VpcExistsValidator.cs new file mode 100644 index 000000000..6fa9d792c --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VpcExistsValidator.cs @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.Common.Data; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validates that a VPC exists in the account + /// + public class VpcExistsValidator : IOptionSettingItemValidator + { + private static readonly string defaultValidationFailedMessage = "A VPC could not be found."; + public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; + + /// + /// The value of the option setting that will cause the validator to fail if a VPC is not found. + /// + public object FailValue { get; set; } = true; + + /// + /// The value type of the option setting and . + /// + public OptionSettingValueType ValueType { get; set; } = OptionSettingValueType.Bool; + + /// + /// Indicates whether this validator will only check for the existence of the default VPC. + /// + public bool DefaultVpc { get; set; } = false; + + private readonly IAWSResourceQueryer _awsResourceQueryer; + + public VpcExistsValidator(IAWSResourceQueryer awsResourceQueryer) + { + _awsResourceQueryer = awsResourceQueryer; + } + + public async Task Validate(object input, Recommendation recommendation, OptionSettingItem optionSettingItem) + { + // Check for the existence of VPCs which will cause the Validator to pass if VPCs exist + if (DefaultVpc) + { + var vpc = await _awsResourceQueryer.GetDefaultVpc(); + if (vpc != null) + return ValidationResult.Valid(); + } + else + { + var vpcs = await _awsResourceQueryer.GetListOfVpcs(); + if (vpcs.Any()) + return ValidationResult.Valid(); + } + + // If VPCs don't exist, based on the type, check if the option setting value is equal to the FailValue + var inputString = input?.ToString() ?? string.Empty; + if (ValueType == OptionSettingValueType.Bool) + { + if (bool.TryParse(inputString, out var inputBool) && FailValue is bool FailValueBool) + { + if (inputBool == FailValueBool) + return ValidationResult.Failed(ValidationFailedMessage); + else + return ValidationResult.Valid(); + } + else + { + return ValidationResult.Failed($"The option setting value or '{nameof(FailValue)}' are not of type '{ValueType}'."); + } + } + else + { + return ValidationResult.Failed($"The value '{ValueType}' for '{nameof(ValueType)}' is not supported."); + } + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 48a9b8d45..1e86e792f 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -61,7 +61,8 @@ public ValidatorFactory(IServiceProvider serviceProvider) { OptionSettingItemValidatorList.SecurityGroupsInVpc, typeof(SecurityGroupsInVpcValidator) }, { OptionSettingItemValidatorList.Uri, typeof(UriValidator) }, { OptionSettingItemValidatorList.Comparison, typeof(ComparisonValidator) }, - { OptionSettingItemValidatorList.VPCSubnetsInDifferentAZs, typeof(VPCSubnetsInDifferentAZsValidator) } + { OptionSettingItemValidatorList.VPCSubnetsInDifferentAZs, typeof(VPCSubnetsInDifferentAZsValidator) }, + { OptionSettingItemValidatorList.VpcExists, typeof(VpcExistsValidator) } }; private static readonly Dictionary _recipeValidatorTypeMapping = new() diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 5becf6bea..6526ea5dd 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -33,11 +33,11 @@ public class Recommendation : IUserInputOption public DeploymentBundle DeploymentBundle { get; } - public readonly Dictionary ReplacementTokens = new(); + public readonly Dictionary ReplacementTokens = new(); - public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefinition, int computedPriority, Dictionary additionalReplacements) + public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefinition, int computedPriority, Dictionary additionalReplacements) { - additionalReplacements ??= new Dictionary(); + additionalReplacements ??= new Dictionary(); Recipe = recipe; ComputedPriority = computedPriority; @@ -106,7 +106,7 @@ private void CollectRecommendationReplacementTokens(List opti } } - public void AddReplacementToken(string key, string value) + public void AddReplacementToken(string key, object value) { ReplacementTokens[key] = value; } diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs index 5436ee454..5bf175713 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -17,6 +17,8 @@ internal static class RecipeIdentifier public const string REPLACE_TOKEN_ECR_IMAGE_TAG = "{DefaultECRImageTag}"; public const string REPLACE_TOKEN_DOCKERFILE_PATH = "{DockerfilePath}"; public const string REPLACE_TOKEN_DEFAULT_VPC_ID = "{DefaultVpcId}"; + public const string REPLACE_TOKEN_HAS_DEFAULT_VPC = "{HasDefaultVpc}"; + public const string REPLACE_TOKEN_HAS_NOT_VPCS = "{HasNotVpcs}"; /// /// Id for the 'dotnet publish --configuration' recipe option diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 644a28685..a54774e4b 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -487,7 +487,7 @@ public async Task GetDefaultVpc() } } }) - .Vpcs.FirstAsync()); + .Vpcs.FirstOrDefaultAsync()); } public async Task> GetElasticBeanstalkPlatformArns(params BeanstalkPlatformType[]? platformTypes) diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 0237c48e0..f7a2a3a8a 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -220,7 +220,23 @@ public async Task ApplyAllReplacementTokens(Recommendation recommendation, strin throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); var defaultVPC = await _awsResourceQueryer.GetDefaultVpc(); - recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_DEFAULT_VPC_ID, defaultVPC.VpcId); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_DEFAULT_VPC_ID, defaultVPC?.VpcId ?? string.Empty); + } + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_HAS_DEFAULT_VPC)) + { + if (_awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); + + var defaultVPC = await _awsResourceQueryer.GetDefaultVpc(); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_HAS_DEFAULT_VPC, defaultVPC != null); + } + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_HAS_NOT_VPCS)) + { + if (_awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); + + var vpcs = await _awsResourceQueryer.GetListOfVpcs(); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_HAS_NOT_VPCS, !vpcs.Any()); } } diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index 03cbbd29d..da55d0243 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -24,9 +24,9 @@ public RecommendationEngine(OrchestratorSession orchestratorSession, IRecipeHand _recipeHandler = recipeHandler; } - public async Task> ComputeRecommendations(List? recipeDefinitionPaths = null, Dictionary? additionalReplacements = null) + public async Task> ComputeRecommendations(List? recipeDefinitionPaths = null, Dictionary? additionalReplacements = null) { - additionalReplacements ??= new Dictionary(); + additionalReplacements ??= new Dictionary(); var recommendations = new List(); var availableRecommendations = await _recipeHandler.GetRecipeDefinitions(recipeDefinitionPaths); diff --git a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs index 95f51eab6..d6f429bdf 100644 --- a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs +++ b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs @@ -90,7 +90,7 @@ public List GetEnvironmentConfigurationSettings(Reco if (!optionSetting.Updatable) continue; - var optionSettingValue = optionSetting.GetValue(new Dictionary()); + var optionSettingValue = optionSetting.GetValue(new Dictionary()); additionalSettings.Add(new ConfigurationOptionSetting { diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Configurations/VPCConnectorConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Configurations/VPCConnectorConfiguration.cs index 2a029c109..cf561cc30 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Configurations/VPCConnectorConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Configurations/VPCConnectorConfiguration.cs @@ -28,6 +28,11 @@ public partial class VPCConnectorConfiguration /// public string? VpcConnectorId { get; set; } + /// + /// If set, creates a new VPC whose Subnets and Security Groups will be used to create a new VPC Connector. + /// + public bool CreateNewVpc { get; set; } + /// /// The VPC ID to use for the App Runner service. /// diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs index 023b9109b..beb5dd561 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs @@ -15,6 +15,7 @@ using CfnServiceProps = Amazon.CDK.AWS.AppRunner.CfnServiceProps; using Constructs; using System.Collections.Generic; +using Amazon.CDK.AWS.EC2; // 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. @@ -33,6 +34,7 @@ public class Recipe : Construct public IRole? TaskRole { get; private set; } public CfnVpcConnector? VPCConnector { get; private set; } + public Vpc? AppVpc { get; private set; } public Recipe(Construct scope, IRecipeProps props) // The "Recipe" construct ID will be used as part of the CloudFormation logical ID. If the value is changed this will @@ -44,20 +46,57 @@ public Recipe(Construct scope, IRecipeProps props) ConfigureAppRunnerService(props); } + private void ConfigureVpc(Configuration settings) + { + if (settings.VPCConnector.UseVPCConnector) + { + if (settings.VPCConnector.CreateNew) + { + if (settings.VPCConnector.CreateNewVpc) + { + AppVpc = new Vpc(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcProps + { + MaxAzs = 2 + })); + } + } + } + } + private void ConfigureVPCConnector(Configuration settings) { if (settings.VPCConnector.CreateNew) { - if (settings.VPCConnector.Subnets.Count == 0) - throw new InvalidOrMissingConfigurationException("The provided list of Subnets is null or empty."); + if (settings.VPCConnector.CreateNewVpc) + { + ConfigureVpc(settings); + + if (AppVpc == null) + throw new InvalidOperationException($"{nameof(AppVpc)} has not been set."); + + VPCConnector = new CfnVpcConnector(this, nameof(VPCConnector), InvokeCustomizeCDKPropsEvent(nameof(VPCConnector), this, new CfnVpcConnectorProps + { + Subnets = AppVpc.PrivateSubnets.Select(x => x.SubnetId).ToArray(), + + // the properties below are optional + SecurityGroups = new string[] { AppVpc.VpcDefaultSecurityGroup } + })); - VPCConnector = new CfnVpcConnector(this, nameof(VPCConnector), InvokeCustomizeCDKPropsEvent(nameof(VPCConnector), this, new CfnVpcConnectorProps + VPCConnector.Node.AddDependency(AppVpc); + } + else { - Subnets = settings.VPCConnector.Subnets.ToArray(), + if (settings.VPCConnector.Subnets.Count == 0) + throw new InvalidOrMissingConfigurationException("The provided list of Subnets is null or empty."); - // the properties below are optional - SecurityGroups = settings.VPCConnector.SecurityGroups.ToArray() - })); + VPCConnector = new CfnVpcConnector(this, nameof(VPCConnector), InvokeCustomizeCDKPropsEvent(nameof(VPCConnector), this, new CfnVpcConnectorProps + { + Subnets = settings.VPCConnector.Subnets.ToArray(), + + // the properties below are optional + SecurityGroups = settings.VPCConnector.SecurityGroups.ToArray() + })); + } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/VPCConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/VPCConfiguration.cs index b18cf1edb..19f5e276a 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/VPCConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/VPCConfiguration.cs @@ -14,6 +14,11 @@ public partial class VPCConfiguration /// public bool UseVPC { get; set; } + /// + /// Creates a new VPC if set to true. + /// + public bool CreateNew { get; set; } + /// /// The VPC ID to use for the Elastic Beanstalk service. /// diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs index 99b8670ec..cdd5d5a1b 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs @@ -33,6 +33,8 @@ public class Recipe : Construct public const string ENHANCED_HEALTH_REPORTING = "enhanced"; + public Vpc? AppVpc { get; private set; } + public IRole? AppIAMRole { get; private set; } public IRole? BeanstalkServiceRole { get; private set; } @@ -62,11 +64,26 @@ public Recipe(Construct scope, IRecipeProps props) Path = props.DotnetPublishZipPath }); + ConfigureVpc(settings); ConfigureIAM(settings); var beanstalkApplicationName = ConfigureApplication(settings); ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName); } + private void ConfigureVpc(Configuration settings) + { + if (settings.VPC.UseVPC) + { + if (settings.VPC.CreateNew) + { + AppVpc = new Vpc(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcProps + { + MaxAzs = 2 + })); + } + } + } + private void ConfigureIAM(Configuration settings) { if (settings.ApplicationIAMRole.CreateNew) @@ -377,30 +394,77 @@ private void ConfigureBeanstalkEnvironment(Configuration settings, string beanst if (settings.VPC.UseVPC) { - optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + if (settings.VPC.CreateNew) { - Namespace = "aws:ec2:vpc", - OptionName = "VPCId", - Value = settings.VPC.VpcId - }); + if (AppVpc == null) + throw new InvalidOperationException($"{nameof(AppVpc)} has not been set. The {nameof(ConfigureVpc)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}"); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "VPCId", + Value = AppVpc.VpcId + }); + + if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_SINGLEINSTANCE)) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", AppVpc.PublicSubnets.Select(x => x.SubnetId)) + }); + } + else if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_LOADBALANCED)) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", AppVpc.PrivateSubnets.Select(x => x.SubnetId)) + }); + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "ELBSubnets", + Value = string.Join(",", AppVpc.PublicSubnets.Select(x => x.SubnetId)) + }); + } - if (settings.VPC.Subnets.Any()) + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "SecurityGroups", + Value = AppVpc.VpcDefaultSecurityGroup + }); + } + else { optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { Namespace = "aws:ec2:vpc", - OptionName = "Subnets", - Value = string.Join(",", settings.VPC.Subnets) + OptionName = "VPCId", + Value = settings.VPC.VpcId }); - if (settings.VPC.SecurityGroups.Any()) + if (settings.VPC.Subnets.Any()) { optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { - Namespace = "aws:autoscaling:launchconfiguration", - OptionName = "SecurityGroups", - Value = string.Join(",", settings.VPC.SecurityGroups) + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", settings.VPC.Subnets) }); + + if (settings.VPC.SecurityGroups.Any()) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "SecurityGroups", + Value = string.Join(",", settings.VPC.SecurityGroups) + }); + } } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs index 3ab003091..9219ffb90 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs @@ -14,6 +14,11 @@ public partial class VPCConfiguration /// public bool UseVPC { get; set; } + /// + /// Creates a new VPC if set to true. + /// + public bool CreateNew { get; set; } + /// /// The VPC ID to use for the Elastic Beanstalk service. /// diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs index e6c06b93e..53250f788 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json; using Amazon.CDK; +using Amazon.CDK.AWS.EC2; using Amazon.CDK.AWS.ElasticBeanstalk; using Amazon.CDK.AWS.IAM; using Amazon.CDK.AWS.S3.Assets; @@ -35,6 +36,8 @@ public class Recipe : Construct public const string ENHANCED_HEALTH_REPORTING = "enhanced"; + public Vpc? AppVpc { get; private set; } + public IRole? AppIAMRole { get; private set; } public IRole? BeanstalkServiceRole { get; private set; } @@ -67,11 +70,26 @@ public Recipe(Construct scope, IRecipeProps props) Path = props.DotnetPublishZipPath }); + ConfigureVpc(settings); ConfigureIAM(settings); var beanstalkApplicationName = ConfigureApplication(settings); ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName); } + private void ConfigureVpc(Configuration settings) + { + if (settings.VPC.UseVPC) + { + if (settings.VPC.CreateNew) + { + AppVpc = new Vpc(this, nameof(AppVpc), InvokeCustomizeCDKPropsEvent(nameof(AppVpc), this, new VpcProps + { + MaxAzs = 2 + })); + } + } + } + private void ConfigureIAM(Configuration settings) { if (settings.ApplicationIAMRole.CreateNew) @@ -370,30 +388,77 @@ private void ConfigureBeanstalkEnvironment(Configuration settings, string beanst if (settings.VPC.UseVPC) { - optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + if (settings.VPC.CreateNew) { - Namespace = "aws:ec2:vpc", - OptionName = "VPCId", - Value = settings.VPC.VpcId - }); + if (AppVpc == null) + throw new InvalidOperationException($"{nameof(AppVpc)} has not been set. The {nameof(ConfigureVpc)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}"); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "VPCId", + Value = AppVpc.VpcId + }); + + if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_SINGLEINSTANCE)) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", AppVpc.PublicSubnets.Select(x => x.SubnetId)) + }); + } + else if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_LOADBALANCED)) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", AppVpc.PrivateSubnets.Select(x => x.SubnetId)) + }); + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "ELBSubnets", + Value = string.Join(",", AppVpc.PublicSubnets.Select(x => x.SubnetId)) + }); + } - if (settings.VPC.Subnets.Any()) + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "SecurityGroups", + Value = AppVpc.VpcDefaultSecurityGroup + }); + } + else { optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { Namespace = "aws:ec2:vpc", - OptionName = "Subnets", - Value = string.Join(",", settings.VPC.Subnets) + OptionName = "VPCId", + Value = settings.VPC.VpcId }); - if (settings.VPC.SecurityGroups.Any()) + if (settings.VPC.Subnets.Any()) { optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { - Namespace = "aws:autoscaling:launchconfiguration", - OptionName = "SecurityGroups", - Value = string.Join(",", settings.VPC.SecurityGroups) + Namespace = "aws:ec2:vpc", + OptionName = "Subnets", + Value = string.Join(",", settings.VPC.Subnets) }); + + if (settings.VPC.SecurityGroups.Any()) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "SecurityGroups", + Value = string.Join(",", settings.VPC.SecurityGroups) + }); + } } } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 3fa06af0b..3aeb840a9 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppAppRunner", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "ASP.NET Core App to AWS App Runner", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -468,6 +468,36 @@ } ] }, + { + "Id": "CreateNewVpc", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC to use for the VPC Connector?", + "Type": "Bool", + "DefaultValue": "{HasNotVpcs}", + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], + "DependsOn": [ + { + "Id": "VPCConnector.UseVPCConnector", + "Value": true + }, + { + "Id": "VPCConnector.CreateNew", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", @@ -494,6 +524,10 @@ { "Id": "VPCConnector.CreateNew", "Value": true + }, + { + "Id": "VPCConnector.CreateNewVpc", + "Value": false } ] }, @@ -533,6 +567,10 @@ "Id": "VPCConnector.CreateNew", "Value": true }, + { + "Id": "VPCConnector.CreateNewVpc", + "Value": false + }, { "Id": "VPCConnector.VpcId", "Operation": "NotEmpty" @@ -575,6 +613,10 @@ "Id": "VPCConnector.CreateNew", "Value": true }, + { + "Id": "VPCConnector.CreateNewVpc", + "Value": false + }, { "Id": "VPCConnector.VpcId", "Operation": "NotEmpty" diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 390e32bda..a429c95c3 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppEcsFargate", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "ASP.NET Core App to Amazon ECS using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -289,18 +289,40 @@ "Name": "Use default VPC", "Description": "Do you want to use the default VPC for the deployment?", "Type": "Bool", - "DefaultValue": true, + "DefaultValue": "{HasDefaultVpc}", "AdvancedSetting": false, - "Updatable": false + "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": true, + "DefaultVpc": true, + "ValueType": "Bool", + "ValidationFailedMessage": "A default VPC could not be found." + } + } + ] }, { "Id": "CreateNew", "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": "{HasNotVpcs}", "AdvancedSetting": false, "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], "DependsOn": [ { "Id": "Vpc.IsDefault", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe index 35bfba1ef..ce13a3bcd 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppElasticBeanstalkLinux", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "ASP.NET Core App to AWS Elastic Beanstalk on Linux", "DeploymentType": "CdkProject", "DeploymentBundle": "DotnetPublishZipFile", @@ -744,6 +744,7 @@ "Category": "VPC", "Description": "A VPC enables you to launch the application into a virtual network that you've defined", "Type": "Object", + "TypeHint": "ElasticBeanstalkVpc", "AdvancedSetting": true, "Updatable": true, "ChildOptionSettings": [ @@ -756,10 +757,36 @@ "AdvancedSetting": true, "Updatable": false }, + { + "Id": "CreateNew", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC?", + "Type": "Bool", + "DefaultValue": "{HasNotVpcs}", + "AdvancedSetting": true, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], + "DependsOn": [ + { + "Id": "VPC.UseVPC", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", - "Description": "A list of VPC IDs that App Runner should use when it associates your service with a custom Amazon VPC.", + "Description": "A list of VPC IDs that Elastic Beanstalk should use when it associates your service with a custom Amazon VPC.", "Type": "String", "TypeHint": "ExistingVpc", "DefaultValue": "{DefaultVpcId}", @@ -778,12 +805,16 @@ { "Id": "VPC.UseVPC", "Value": true + }, + { + "Id": "VPC.CreateNew", + "Value": false } ] }, { "Id": "Subnets", - "Name": "Subnets", + "Name": "EC2 Instance Subnets", "Description": "A list of IDs of subnets that Elastic Beanstalk should use when it associates your environment with a custom Amazon VPC. Specify IDs of subnets of a single Amazon VPC.", "Type": "List", "TypeHint": "ExistingSubnets", @@ -813,6 +844,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" @@ -851,6 +886,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe index f0400a88a..98e4e517c 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppElasticBeanstalkWindows", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "ASP.NET Core App to AWS Elastic Beanstalk on Windows", "DeploymentType": "CdkProject", "DeploymentBundle": "DotnetPublishZipFile", @@ -739,6 +739,7 @@ "Category": "VPC", "Description": "A VPC enables you to launch the application into a virtual network that you've defined", "Type": "Object", + "TypeHint": "ElasticBeanstalkVpc", "AdvancedSetting": true, "Updatable": true, "ChildOptionSettings": [ @@ -751,10 +752,36 @@ "AdvancedSetting": true, "Updatable": false }, + { + "Id": "CreateNew", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC?", + "Type": "Bool", + "DefaultValue": "{HasNotVpcs}", + "AdvancedSetting": true, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], + "DependsOn": [ + { + "Id": "VPC.UseVPC", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", - "Description": "A list of VPC IDs that App Runner should use when it associates your service with a custom Amazon VPC.", + "Description": "A list of VPC IDs that Elastic Beanstalk should use when it associates your service with a custom Amazon VPC.", "Type": "String", "TypeHint": "ExistingVpc", "DefaultValue": "{DefaultVpcId}", @@ -773,12 +800,16 @@ { "Id": "VPC.UseVPC", "Value": true + }, + { + "Id": "VPC.CreateNew", + "Value": false } ] }, { "Id": "Subnets", - "Name": "Subnets", + "Name": "EC2 Instance Subnets", "Description": "A list of IDs of subnets that Elastic Beanstalk should use when it associates your environment with a custom Amazon VPC. Specify IDs of subnets of a single Amazon VPC.", "Type": "List", "TypeHint": "ExistingSubnets", @@ -808,6 +839,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" @@ -846,6 +881,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index f5c196aa7..af5d8ffec 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateScheduleTask", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "Scheduled Task on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -272,18 +272,40 @@ "Name": "Use default VPC", "Description": "Do you want to use the default VPC?", "Type": "Bool", - "DefaultValue": true, + "DefaultValue": "{HasDefaultVpc}", "AdvancedSetting": false, - "Updatable": false + "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": true, + "DefaultVpc": true, + "ValueType": "Bool", + "ValidationFailedMessage": "A default VPC could not be found." + } + } + ] }, { "Id": "CreateNew", "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": "{HasNotVpcs}", "AdvancedSetting": false, "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], "DependsOn": [ { "Id": "Vpc.IsDefault", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 5f31416e5..3976518ba 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateService", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "Service on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -341,18 +341,40 @@ "Name": "Use default VPC", "Description": "Do you want to use the default VPC for the deployment?", "Type": "Bool", - "DefaultValue": true, + "DefaultValue": "{HasDefaultVpc}", "AdvancedSetting": false, - "Updatable": false + "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": true, + "DefaultVpc": true, + "ValueType": "Bool", + "ValidationFailedMessage": "A default VPC could not be found." + } + } + ] }, { "Id": "CreateNew", "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": "{HasNotVpcs}", "AdvancedSetting": false, "Updatable": false, + "Validators": [ + { + "ValidatorType": "VpcExists", + "Configuration": { + "FailValue": false, + "DefaultVpc": false, + "ValueType": "Bool", + "ValidationFailedMessage": "You must create a new VPC since there are no existing VPCs to be used." + } + } + ], "DependsOn": [ { "Id": "Vpc.IsDefault", 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 503328a1a..af240cc3d 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -544,7 +544,8 @@ "DynamoDBTableName", "SQSQueueUrl", "SNSTopicArn", - "FilePath" + "FilePath", + "ElasticBeanstalkVpc" ] }, "DefaultValue": { @@ -645,7 +646,8 @@ "SecurityGroupsInVpc", "Uri", "Comparison", - "VPCSubnetsInDifferentAZs" + "VPCSubnetsInDifferentAZs", + "VpcExists" ] } }, @@ -750,6 +752,31 @@ } } } + }, + { + "if": { + "properties": { "ValidatorType": { "const": "VpcExists" } } + }, + "then": { + "properties": { + "Configuration": { + "properties": { + "FailValue": { + "type": "boolean" + }, + "DefaultVpc": { + "type": "boolean" + }, + "ValueType": { + "type": "string" + }, + "ValidationFailedMessage": { + "type": "string" + } + } + } + } + } } ] } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs index bc25b7346..97faf8526 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs @@ -86,7 +86,7 @@ public async Task DockerfilePathValidationHelperAsync(string dockerfilePath, str new OptionSettingItem("DockerfilePath", "", "", "") }; var projectDefintion = new ProjectDefinition(null, projectPath, "", ""); - var recommendation = new Recommendation(_recipeDefinition, projectDefintion, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, projectDefintion, 100, new Dictionary()); var validator = new DockerfilePathValidator(_directoryManager, _fileManager); recommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; 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 7349f3385..cdf381939 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -42,7 +42,7 @@ public ECSFargateOptionSettingItemValidationTests() _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); _recipe = new RecipeDefinition("Fargate", "0.1", "Fargate", DeploymentTypes.CdkProject, DeploymentBundleTypes.Container, "", "", "", "", ""); - _recommendation = new Recommendation(_recipe, null, 100, new Dictionary()); + _recommendation = new Recommendation(_recipe, null, 100, new Dictionary()); } [Theory] diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 8535ed6c9..5e58aec60 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -79,7 +79,7 @@ public async Task BuildDockerImage_DockerExecutionDirectoryNotSet() { new OptionSettingItem("DockerfilePath", "", "", "") }; - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var imageTag = "imageTag"; @@ -103,7 +103,7 @@ public async Task BuildDockerImage_DockerExecutionDirectorySet() { new OptionSettingItem("DockerfilePath", "", "", "") }; - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); recommendation.DeploymentBundle.DockerExecutionDirectory = projectPath; @@ -131,7 +131,7 @@ public async Task BuildDockerImage_AlternativeDockerfilePathSet() { new OptionSettingItem("DockerfilePath", "", "", "") }; - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); var dockerfilePath = Path.Combine(projectPath, "Docker", "Dockerfile"); var expectedDockerExecutionDirectory = Directory.GetParent(Path.GetFullPath(recommendation.ProjectPath)).Parent.Parent; @@ -153,7 +153,7 @@ public async Task PushDockerImage_RepositoryNameCheck() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); var repositoryName = "repository"; await _deploymentBundleHandler.PushDockerImageToECR(recommendation, repositoryName, "ConsoleAppTask:latest"); @@ -166,7 +166,7 @@ public async Task CreateDotnetPublishZip_NotSelfContained() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = false; recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = "Release"; @@ -189,7 +189,7 @@ public async Task CreateDotnetPublishZip_SelfContained() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = true; recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = "Release"; diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index 22589cd4a..a77d75ca3 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -130,17 +130,23 @@ public async Task GetOptionSettingTests_ListType_InvalidValue() var appRunnerRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_APPRUNNER_ID); + var testVpc = new Vpc { VpcId = "vpc1" }; + var listVpcs = new List { testVpc }; + _awsResourceQueryer.Setup(x => x.GetDefaultVpc()).ReturnsAsync(testVpc); + _awsResourceQueryer.Setup(x => x.GetListOfVpcs()).ReturnsAsync(listVpcs); _awsResourceQueryer.Setup(x => x.DescribeSubnets(It.IsAny())).ReturnsAsync(new List()); _awsResourceQueryer.Setup(x => x.DescribeSecurityGroups(It.IsAny())).ReturnsAsync(new List()); var useVpcConnector = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.UseVPCConnector"); var createNew = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.CreateNew"); + var createNewVpc = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.CreateNewVpc"); var vpcId = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.VpcId"); var subnets = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.Subnets"); var securityGroups = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.SecurityGroups"); await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, useVpcConnector, true); await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNew, true); + await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNewVpc, false); await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, vpcId, "vpc-1234abcd"); await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet1" })); await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, securityGroups, new SortedSet(){ "securityGroup1" })); @@ -153,6 +159,7 @@ public async Task GetOptionSettingTests_VPCConnector_DisplayableItems() var SETTING_ID_USEVPCCONNECTOR = "UseVPCConnector"; var SETTING_ID_CREATENEW = "CreateNew"; var SETTING_ID_VPCCONNECTORID = "VpcConnectorId"; + var SETTING_ID_CREATENEWVPC = "CreateNewVpc"; var SETTING_ID_VPCID = "VpcId"; var SETTING_ID_SUBNETS = "Subnets"; var SETTING_ID_SECURITYGROUPS = "SecurityGroups"; @@ -163,6 +170,10 @@ public async Task GetOptionSettingTests_VPCConnector_DisplayableItems() var appRunnerRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_APPRUNNER_ID); + var testVpc = new Vpc { VpcId = "vpc1" }; + var listVpcs = new List { testVpc }; + _awsResourceQueryer.Setup(x => x.GetDefaultVpc()).ReturnsAsync(testVpc); + _awsResourceQueryer.Setup(x => x.GetListOfVpcs()).ReturnsAsync(listVpcs); _awsResourceQueryer.Setup(x => x.DescribeSubnets(It.IsAny())).ReturnsAsync(new List()); _awsResourceQueryer.Setup(x => x.DescribeSecurityGroups(It.IsAny())).ReturnsAsync(new List()); @@ -179,20 +190,30 @@ public async Task GetOptionSettingTests_VPCConnector_DisplayableItems() Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEW))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_VPCCONNECTORID))); + var createNewVpc = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, $"{SETTING_ID_VPCCONNECTOR}.{SETTING_ID_CREATENEWVPC}"); + await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNewVpc, true); + vpcConnectorChildren = vpcConnector.ChildOptionSettings.Where(x => _optionSettingHandler.IsOptionSettingDisplayable(appRunnerRecommendation, x)).ToList(); + Assert.Equal(3, vpcConnectorChildren.Count); + Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_USEVPCCONNECTOR))); + Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEW))); + var createNew = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, $"{SETTING_ID_VPCCONNECTOR}.{SETTING_ID_CREATENEW}"); await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNew, true); + await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNewVpc, false); vpcConnectorChildren = vpcConnector.ChildOptionSettings.Where(x => _optionSettingHandler.IsOptionSettingDisplayable(appRunnerRecommendation, x)).ToList(); - Assert.Equal(3, vpcConnectorChildren.Count); + Assert.Equal(4, vpcConnectorChildren.Count); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_USEVPCCONNECTOR))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEW))); + Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEWVPC))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_VPCID))); var vpcId = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, $"{SETTING_ID_VPCCONNECTOR}.{SETTING_ID_VPCID}"); await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, vpcId, "vpc-abcd1234"); vpcConnectorChildren = vpcConnector.ChildOptionSettings.Where(x => _optionSettingHandler.IsOptionSettingDisplayable(appRunnerRecommendation, x)).ToList(); - Assert.Equal(5, vpcConnectorChildren.Count); + Assert.Equal(6, vpcConnectorChildren.Count); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_USEVPCCONNECTOR))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEW))); + Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_CREATENEWVPC))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_VPCID))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_SUBNETS))); Assert.NotNull(vpcConnectorChildren.First(x => x.Id.Equals(SETTING_ID_SECURITYGROUPS))); diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index 27e5bd1f3..d9036c815 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -462,16 +462,19 @@ public async Task IsDisplayable_NotEmptyOperation() var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var useVpcOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.UseVPC"); + var createNewOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.CreateNew"); var vpcIdOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.VpcId"); var subnetsSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.Subnets"); // Before dependency aren't satisfied Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, useVpcOptionSetting)); + Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, createNewOptionSetting)); Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, vpcIdOptionSetting)); Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); // Satisfy dependencies await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, useVpcOptionSetting, true); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, createNewOptionSetting, false); await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, vpcIdOptionSetting)); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs index b17c3fbc3..afb93f7ad 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs @@ -151,6 +151,7 @@ public async Task Execute_NewVPCConnector() { "y", "y", + "n", "1", "1", "1", diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs index c6384cde6..5e75e95b6 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs @@ -111,7 +111,7 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() var recommendations = new List { - new Recommendation(new RecipeDefinition("AspNetAppEcsFargate", "0.2.0", "ASP.NET Core ECS", DeploymentTypes.CdkProject, DeploymentBundleTypes.Container, "", "", "", "", "" ), null, 100, new Dictionary()) + new Recommendation(new RecipeDefinition("AspNetAppEcsFargate", "0.2.0", "ASP.NET Core ECS", DeploymentTypes.CdkProject, DeploymentBundleTypes.Container, "", "", "", "", "" ), null, 100, new Dictionary()) { } @@ -180,7 +180,7 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() PersistedDeploymentProject = true, BaseRecipeId = "AspNetAppEcsFargate" }, - null, 100, new Dictionary()) + null, 100, new Dictionary()) { }