From 7821440637143b3e99eed508775d6fcac21798b5 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 12 Jul 2022 11:12:36 -0400 Subject: [PATCH] fix: deployment workflow fails if there is no default vpc --- .../TypeHints/ElasticBeanstalkVpcCommand.cs | 192 ++++++++++++++++++ .../TypeHints/TypeHintCommandFactory.cs | 1 + .../Commands/TypeHints/VPCConnectorCommand.cs | 26 ++- .../Commands/TypeHints/VpcCommand.cs | 11 +- .../ElasticBeanstalkVpcTypeHintResponse.cs | 22 ++ .../VPCConnectorTypeHintResponse.cs | 1 + .../OptionSettingItem.ValueOverride.cs | 32 ++- .../Recipes/OptionSettingItem.cs | 8 +- .../Recipes/OptionSettingTypeHint.cs | 3 +- src/AWS.Deploy.Common/Recommendation.cs | 8 +- src/AWS.Deploy.Constants/RecipeIdentifier.cs | 1 + .../Data/AWSResourceQueryer.cs | 2 +- src/AWS.Deploy.Orchestration/Orchestrator.cs | 10 +- .../RecommendationEngine.cs | 4 +- .../AWSElasticBeanstalkHandler.cs | 2 +- .../VPCConnectorConfiguration.cs | 5 + .../AspNetAppAppRunner/Generated/Recipe.cs | 57 +++++- .../Configurations/VPCConfiguration.cs | 5 + .../Generated/Recipe.cs | 75 +++++-- .../Configurations/VPCConfiguration.cs | 5 + .../Generated/Recipe.cs | 76 +++++-- .../ASP.NETAppAppRunner.recipe | 31 +++ .../ASP.NETAppECSFargate.recipe | 4 +- .../ASP.NETAppElasticBeanstalkLinux.recipe | 28 +++ .../ASP.NETAppElasticBeanstalkWindows.recipe | 28 +++ .../ConsoleAppECSFargateScheduleTask.recipe | 4 +- .../ConsoleAppECSFargateService.recipe | 4 +- .../aws-deploy-recipe-schema.json | 3 +- .../DockerfilePathValidationTests.cs | 2 +- ...FargateOptionSettingItemValidationTests.cs | 2 +- .../DeploymentBundleHandlerTests.cs | 12 +- .../GetOptionSettingTests.cs | 15 +- .../VPCConnectorCommandTest.cs | 1 + .../DeployedApplicationQueryerTests.cs | 4 +- 34 files changed, 610 insertions(+), 74 deletions(-) create mode 100644 src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs create mode 100644 src/AWS.Deploy.CLI/TypeHintResponses/ElasticBeanstalkVpcTypeHintResponse.cs 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..0f439f6e7 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ElasticBeanstalkVpcCommand.cs @@ -0,0 +1,192 @@ +// 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.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 +{ + 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 => { + 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(); + + return resourceTable; + } + + public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + _toolInteractiveService.WriteLine(); + 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 (!useVpc) + return new ElasticBeanstalkVpcTypeHintResponse() + { + UseVPC = false + }; + + var currentVpcTypeHintResponse = optionSetting.GetTypeHintData(); + + 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 ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = true + }; + } + + _toolInteractiveService.WriteLine(); + 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 => + { + 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}"; + }, + 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); + if (vpc.CreateNew) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = true + }; + if (vpc.SelectedOption == null) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = false + }; + + var availableSubnets = (await _awsResourceQueryer.DescribeSubnets(vpc.SelectedOption.VpcId)).OrderBy(x => x.SubnetId).ToList(); + if (!availableSubnets.Any()) + return new ElasticBeanstalkVpcTypeHintResponse + { + UseVPC = true, + CreateNew = false, + VpcId = vpc.SelectedOption.VpcId + }; + 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); + + 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 + }; + var groupNamePadding = 0; + availableSecurityGroups.ForEach(x => + { + if (x.GroupName.Length > groupNamePadding) + groupNamePadding = x.GroupName.Length; + }); + 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/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..463eae06e 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 it's 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..8ff155611 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs @@ -21,11 +21,13 @@ 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() @@ -62,6 +64,13 @@ 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 => 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 92ec8bac9..baf5ed162 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) { @@ -167,14 +167,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/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..044106290 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -17,6 +17,7 @@ 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}"; /// /// 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 f97e3c020..4fd7e9b22 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -210,7 +210,15 @@ 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); } } 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..31bd2855a 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,61 @@ 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."); + + var vpcSubnets = new SortedSet(); + foreach (var subnet in AppVpc.PublicSubnets) + vpcSubnets.Add(subnet.SubnetId); + foreach (var subnet in AppVpc.PrivateSubnets) + vpcSubnets.Add(subnet.SubnetId); + + VPCConnector = new CfnVpcConnector(this, nameof(VPCConnector), InvokeCustomizeCDKPropsEvent(nameof(VPCConnector), this, new CfnVpcConnectorProps + { + Subnets = vpcSubnets.ToArray(), - VPCConnector = new CfnVpcConnector(this, nameof(VPCConnector), InvokeCustomizeCDKPropsEvent(nameof(VPCConnector), this, new CfnVpcConnectorProps + // the properties below are optional + SecurityGroups = new string[] { AppVpc.VpcDefaultSecurityGroup } + })); + } + 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..109f1a349 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,64 @@ 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)}"); - if (settings.VPC.Subnets.Any()) - { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "VPCId", + Value = AppVpc.VpcId + }); + + var vpcSubnets = new SortedSet(); + foreach (var subnet in AppVpc.PublicSubnets) + vpcSubnets.Add(subnet.SubnetId); + foreach (var subnet in AppVpc.PrivateSubnets) + vpcSubnets.Add(subnet.SubnetId); optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { Namespace = "aws:ec2:vpc", OptionName = "Subnets", - Value = string.Join(",", settings.VPC.Subnets) + Value = string.Join(",", vpcSubnets) + }); + + 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 = "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..d15228e22 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,64 @@ 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)}"); - if (settings.VPC.Subnets.Any()) - { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "VPCId", + Value = AppVpc.VpcId + }); + + var vpcSubnets = new SortedSet(); + foreach (var subnet in AppVpc.PublicSubnets) + vpcSubnets.Add(subnet.SubnetId); + foreach (var subnet in AppVpc.PrivateSubnets) + vpcSubnets.Add(subnet.SubnetId); optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { Namespace = "aws:ec2:vpc", OptionName = "Subnets", - Value = string.Join(",", settings.VPC.Subnets) + Value = string.Join(",", vpcSubnets) + }); + + 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 = "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..d869c1c50 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -468,6 +468,25 @@ } ] }, + { + "Id": "CreateNewVpc", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC to use for the VPC Connector?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "VPCConnector.UseVPCConnector", + "Value": true + }, + { + "Id": "VPCConnector.CreateNew", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", @@ -494,6 +513,10 @@ { "Id": "VPCConnector.CreateNew", "Value": true + }, + { + "Id": "VPCConnector.CreateNewVpc", + "Value": false } ] }, @@ -533,6 +556,10 @@ "Id": "VPCConnector.CreateNew", "Value": true }, + { + "Id": "VPCConnector.CreateNewVpc", + "Value": false + }, { "Id": "VPCConnector.VpcId", "Operation": "NotEmpty" @@ -575,6 +602,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..a36b57554 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -289,7 +289,7 @@ "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 }, @@ -298,7 +298,7 @@ "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": true, "AdvancedSetting": false, "Updatable": false, "DependsOn": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe index 35bfba1ef..a06dd5889 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe @@ -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,6 +757,21 @@ "AdvancedSetting": true, "Updatable": false }, + { + "Id": "CreateNew", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": true, + "Updatable": false, + "DependsOn": [ + { + "Id": "VPC.UseVPC", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", @@ -778,6 +794,10 @@ { "Id": "VPC.UseVPC", "Value": true + }, + { + "Id": "VPC.CreateNew", + "Value": false } ] }, @@ -813,6 +833,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" @@ -851,6 +875,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..771879427 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe @@ -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,6 +752,21 @@ "AdvancedSetting": true, "Updatable": false }, + { + "Id": "CreateNew", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": true, + "Updatable": false, + "DependsOn": [ + { + "Id": "VPC.UseVPC", + "Value": true + } + ] + }, { "Id": "VpcId", "Name": "VPC ID", @@ -773,6 +789,10 @@ { "Id": "VPC.UseVPC", "Value": true + }, + { + "Id": "VPC.CreateNew", + "Value": false } ] }, @@ -808,6 +828,10 @@ "Id": "VPC.UseVPC", "Value": true }, + { + "Id": "VPC.CreateNew", + "Value": false + }, { "Id": "VPC.VpcId", "Operation": "NotEmpty" @@ -846,6 +870,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..fb2ed431a 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -272,7 +272,7 @@ "Name": "Use default VPC", "Description": "Do you want to use the default VPC?", "Type": "Bool", - "DefaultValue": true, + "DefaultValue": "{HasDefaultVpc}", "AdvancedSetting": false, "Updatable": false }, @@ -281,7 +281,7 @@ "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": true, "AdvancedSetting": false, "Updatable": false, "DependsOn": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 5f31416e5..630211648 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -341,7 +341,7 @@ "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 }, @@ -350,7 +350,7 @@ "Name": "Create New VPC", "Description": "Do you want to create a new VPC?", "Type": "Bool", - "DefaultValue": false, + "DefaultValue": true, "AdvancedSetting": false, "Updatable": false, "DependsOn": [ 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 37d57821a..98e4f1204 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -516,7 +516,8 @@ "ExistingVpcConnector", "ExistingECSCluster", "ExistingApplicationLoadBalancer", - "S3BucketName" + "S3BucketName", + "ElasticBeanstalkVpc" ] }, "DefaultValue": { 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..11a944d17 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -153,6 +153,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"; @@ -179,20 +180,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/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()) { }