diff --git a/README.md b/README.md index 1bd332fc7..9236fbce2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,26 @@ # AWS .NET deployment tool [![nuget](https://img.shields.io/nuget/v/AWS.Deploy.Tools.svg) ![downloads](https://img.shields.io/nuget/dt/AWS.Deploy.Tools.svg)](https://www.nuget.org/packages/AWS.Deploy.Tools/) -This repository contains the AWS .NET deployment tool for .NET CLI - the opinionated tooling that simplifies deployment of .NET applications. The tool suggests the right AWS compute service to deploy your application to. It then builds and packages your application as required by the chosen compute service, generates the deployment infrastructure, deploys your application by using the appropriate deployment engine (Cloud Development Kit (CDK) or native service APIs), and displays the endpoint. +## Overview +This repository contains the AWS Deploy Tool for .NET CLI - the opinionated tooling that simplifies deployment of .NET applications. The tool suggests the right AWS compute service to deploy your application to. It then builds and packages your application as required by the chosen compute service, generates the deployment infrastructure, deploys your application by using the appropriate deployment engine (Cloud Development Kit (CDK) or native service APIs), and displays the endpoint. The tool assumes minimal knowledge of AWS. It is designed to guide you through the deployment process and provides suggested defaults. The tool will show you all compute service options available to deploy your application, and will recommend a default with information about why it was chosen. The other compute service options will be shown with an explanation of their differences. If the selected compute option does not match your needs, you can select a different compute service. The goal of the deployment tool is to deploy cloud-native .NET applications that are built with .NET Core 3.1 and above. A cloud-native .NET application is written in .NET with the intent to deploy to Linux. It is not tied to any Windows specific technology such as Windows registry, IIS or MSMQ, and can be deployed on virtualized compute. The tool **cannot** be used to deploy .NET Framework, Desktop, Xamarin, or other applications that do not fit the "cloud-native" criteria. -## Project Status -We are looking for feedback on the type of applications users want to deploy to AWS and what features are important to them. Please provide your feedback by opening an [issue in this repository](https://github.com/aws/aws-dotnet-deploy/issues). +We welcome your feedback! Please let us know what you think by opening an [issue](https://github.com/aws/aws-dotnet-deploy/issues). + + +## Useful Links +* [Complete Documentation Guide on GitHub.io](https://aws.github.io/aws-dotnet-deploy/) +* [AWS Deploy Tool for .NET on NuGet](https://www.nuget.org/packages/AWS.Deploy.Tools) +* Blog posts: + * [AWS Streamlines Deployment experience for .NET applications](https://aws.amazon.com/blogs/developer/aws-announces-a-streamlined-deployment-experience-for-net-applications/) + * [Reimagining the AWS .NET deployment experience](http://aws.amazon.com/blogs/developer/reimagining-the-aws-net-deployment-experience/) + * [Update on our new AWS .NET Deployment Experience](https://aws.amazon.com/blogs/developer/update-new-net-deployment-experience/) + * [Deployment Projects with the new AWS .NET Deployment Experience](https://aws.amazon.com/blogs/developer/dotnet-deployment-projects/) +* Youtube videos: + * [AWS On Air ft. New .NET Deployment Experience - Command Line](https://www.youtube.com/watch?v=5uyL8MXxljc) + * [Re:Invent 2021: “What’s new with .NET development and deployment on AWS”](https://www.youtube.com/watch?v=UvTJ_Inb634) ## Pre-requisites @@ -24,6 +37,7 @@ To take advantage of this library you’ll need: * Used when deploying to a container based service like Amazon Elastic Container Service (Amazon ECS) * (optional) The zip cli tool * Mac / Linux only. Used when creating zip packages for deployment bundles. The zip cli is used to maintain Linux file permissions. + ## Getting started The deployment tool is distributed as a .NET Tool from NuGet.org. The installation of the tool is managed with the `dotnet` CLI. @@ -135,15 +149,10 @@ We welcome community contributions and pull requests. See [CONTRIBUTING](https:/ ## Additional Resources -* [AWS Developer Guide](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/deployment-tool.html) Find additional information about developing, deploying, and maintaining your applications using the AWS .Net deployment tool. * [AWS Developer Center - Explore .NET on AWS](https://aws.amazon.com/developer/language/net/) Find all the .NET code samples, step-by-step guides, videos, blog content, tools, and information about live events that you need in one place. * [AWS Developer Blog - .NET](https://aws.amazon.com/blogs/developer/category/programing-language/dot-net/) Come see what .NET developers at AWS are up to! Learn about new .NET software announcements, guides, and how-to's. -* [AWS re:Invent 2021 - What’s new with .NET development and deployment on AWS](https://www.youtube.com/watch?v=UvTJ_Inb634) New deployment tooling incorporates best AWS practices right from the start, providing you with recommendations and the optimal deployment option for your .NET application. * [@dotnetonaws](https://twitter.com/dotnetonaws) Follow us on twitter! -* Deployment tool blog posts - * [Reimagining the AWS .NET deployment experience](http://aws.amazon.com/blogs/developer/reimagining-the-aws-net-deployment-experience/) - * [Update on our new AWS .NET Deployment Experience](https://aws.amazon.com/blogs/developer/update-new-net-deployment-experience/) - * [Deployment Projects with the new AWS .NET Deployment Experience](https://aws.amazon.com/blogs/developer/dotnet-deployment-projects/) + ## License 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()) { } diff --git a/version.json b/version.json index ea546f012..d0ce798ac 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.1", + "version": "1.2", "publicReleaseRefSpec": [ ".*" ],