From 061b9ac27f6523669721b8d9f4b69dd628b7e368 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 16 Jun 2022 15:02:23 -0700 Subject: [PATCH 01/18] build: use PackageOutputAbsolutePath instead to match dotnet pack --output dir ## Motivation The pipeline uses custom location for packing the NuGet package and therefore the local nuget cache doesn't have the expected package. ## Change PackageOutputAbsolutePath MSBuild property is set with the same value provided in `dotnet pack --output` command, therefore, this change aligns them both. ## Result On executing `dotnet pack -o "C:\Users\jangirg\temp"`, the package is created at the output path. --- .../AWS.Deploy.Recipes.CDK.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj index 4a881f27b..1bf3dd73d 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj +++ b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj @@ -76,7 +76,7 @@ - + From c4cc71f0a2cf497c38df8f1b5edbd36025b0e120 Mon Sep 17 00:00:00 2001 From: Irene Kors Date: Wed, 15 Jun 2022 14:03:53 -0400 Subject: [PATCH 02/18] Revised RecipeDefinition descriptions --- .../RecipeDefinitions/ASP.NETAppAppRunner.recipe | 4 ++-- .../RecipeDefinitions/ASP.NETAppECSFargate.recipe | 6 +++--- .../RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe | 4 ++-- .../ASP.NETAppExistingBeanstalkEnvironment.recipe | 4 ++-- src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe | 6 +++--- .../ConsoleAppECSFargateScheduleTask.recipe | 6 +++--- .../RecipeDefinitions/ConsoleAppECSFargateService.recipe | 6 +++--- .../RecipeDefinitions/PushContainerImageECR.recipe | 6 +++--- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 095ef4f2b..44a6bac5c 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/AspNetAppAppRunner", "CdkProjectTemplateId": "netdeploy.AspNetAppAppRunner", - "ShortDescription": "ASP.NET Core application deployed to AWS App Runner", - "Description": "This ASP.NET Core application will be built as a container image and deployed to AWS App Runner. If your project does not contain a Dockerfile, it will be automatically generated. Recommended if you want to deploy your application as a container image on a fully managed environment.", + "ShortDescription": ""Deploys as Linux container image to a fully managed service. Dockerfile will be automatically generated if needed.", + "Description": "This ASP.NET Core application will be built as a container image on Linux and deployed to AWS App Runner, a fully managed service for web applications and APIs. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy your web application as a Linux container image on a fully managed environment.", "TargetService": "AWS App Runner", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 7cb287c74..763b21c4e 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -2,13 +2,13 @@ "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppEcsFargate", "Version": "0.1.0", - "Name": "ASP.NET Core App to Amazon ECS using Fargate", + "Name": "ASP.NET Core App to Amazon ECS using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/AspNetAppEcsFargate", "CdkProjectTemplateId": "netdeploy.AspNetAppEcsFargate", - "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated. Recommended if you want to deploy your application as a container image on Linux.", - "ShortDescription": "ASP.NET Core application deployed to Amazon Elastic Container Service (Amazon ECS).", + "ShortDescription": "Deploys as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", + "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy your application as a container image on Linux.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index e2f9f9a5a..80deb9b8d 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "DotnetPublishZipFile", "CdkProjectTemplate": "../CdkTemplates/AspNetAppElasticBeanstalkLinux", "CdkProjectTemplateId": "netdeploy.AspNetAppElasticBeanstalkLinux", - "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you do not want to deploy your application as a container image.", - "ShortDescription": "ASP.NET Core application deployed to AWS Elastic Beanstalk on Linux.", + "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", + "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", "TargetService": "AWS Elastic Beanstalk", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe index d3be44912..9d65e2a50 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe @@ -6,8 +6,8 @@ "DisableNewDeployments": true, "DeploymentType": "BeanstalkEnvironment", "DeploymentBundle": "DotnetPublishZipFile", - "Description": "This ASP.NET Core application will be built and deployed to existing AWS Elastic Beanstalk environment. Recommended if you do not want to deploy your application as a container image.", - "ShortDescription": "ASP.NET Core application deployed to AWS Elastic Beanstalk on Linux.", + "Description": "This ASP.NET Core application will be built and deployed to an existing AWS Elastic Beanstalk environment. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", + "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", "TargetService": "AWS Elastic Beanstalk", "RecipePriority": 0, diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe index ebc40f8b8..3f4dd45e6 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe @@ -7,9 +7,9 @@ "DeploymentBundle": "DotnetPublishZipFile", "CdkProjectTemplate": "../CdkTemplates/BlazorWasm", "CdkProjectTemplateId": "netdeploy.BlazorWasm", - "Description": "This Blazor WebAssembly application will be built and deployed to a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor applications will be exposed publicly through a CloudFront distribution using the S3 bucket as the origin.", - "ShortDescription": "Blazor WebAssembly application deployed in an Amazon S3 bucket and CloudFront.", - "TargetService": "Amazon S3", + "Description": "This Blazor WebAssembly application will be built and deployed to a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor applications will be exposed publicly through a CloudFront distribution using the Amazon S3 bucket as the origin.", + "ShortDescription": "Hosts Blazor WebAssembly applicationin an Amazon S3 bucket with Amazon CloudFront as a content delivery network.", + "TargetService": "Amazon S3 and Amazon CloudFront", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index 346e40782..29360677a 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -2,13 +2,13 @@ "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateScheduleTask", "Version": "0.2.0", - "Name": "Scheduled Task on Amazon ECS using Fargate", + "Name": "Scheduled Task on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/ConsoleAppECSFargateScheduleTask", "CdkProjectTemplateId": "netdeploy.ConsoleAppECSFargateScheduleTask", - "Description": "This .NET Console application will be built using a Docker file and deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated. Recommended if you want to deploy a scheduled task as a container image on Linux.", - "ShortDescription": "Console application deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS).", + "Description": "This .NET Console application will be built using a Docker file and deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a scheduled task as a container image on Linux.", + "ShortDescription": "Deploys a scheduled task as a linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 015b1d266..de46478f9 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -2,13 +2,13 @@ "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateService", "Version": "0.2.0", - "Name": "Service on Amazon ECS using Fargate", + "Name": "Service on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/ConsoleAppECSFargateService", "CdkProjectTemplateId": "netdeploy.ConsoleAppEcsFargateService", - "Description": "This .NET Console application will be built using a Docker file and deployed as a service to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated. Recommended if you want to deploy a service as a container image on Linux.", - "ShortDescription": "Console application deployed as a service to Amazon Elastic Container Service (Amazon ECS).", + "Description": "This .NET Console application will be built using a Dockerfile and deployed as a service to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy a service as a container image on Linux.", + "ShortDescription": "Deploys a service as a linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe index 30b042562..26eb0c540 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe @@ -2,11 +2,11 @@ "$schema": "./aws-deploy-recipe-schema.json", "Id": "PushContainerImageEcr", "Version": "0.1.0", - "Name": "Push Container Images to Amazon Elastic Container Registry", + "Name": "Container Image to Amazon Elastic Container Registry (ECR)", "DeploymentType": "ElasticContainerRegistryImage", "DeploymentBundle": "Container", - "Description": "This will push the Docker container image to Amazon Elastic Container Registry (Amazon ECR)", - "ShortDescription": "Push container images to Amazon Elastic Container Registry", + "Description": "This .NET application will be built using an existing Dockerfile. The Docker container image will then be pushed to Amazon ECR - a fully managed container registry.", + "ShortDescription": "Pushes container image to Amazon ECR - a fully managed container registry.", "TargetService": "Amazon Elastic Container Service", "RecipePriority": 0, From c265776e1e4d0c42fd6bf3a34dd5660237b537dd Mon Sep 17 00:00:00 2001 From: Irene Kors Date: Thu, 16 Jun 2022 17:30:22 -0400 Subject: [PATCH 03/18] docs: Modified short descriptions and missing dependencies text --- .../SystemCapabilityEvaluator.cs | 12 ++++++------ .../RecipeDefinitions/ASP.NETAppAppRunner.recipe | 4 ++-- .../RecipeDefinitions/ASP.NETAppECSFargate.recipe | 2 +- .../ASP.NETAppElasticBeanstalk.recipe | 2 +- .../ASP.NETAppExistingBeanstalkEnvironment.recipe | 2 +- .../RecipeDefinitions/BlazorWasm.recipe | 4 ++-- .../ConsoleAppECSFargateScheduleTask.recipe | 4 ++-- .../ConsoleAppECSFargateService.recipe | 4 ++-- .../RecipeDefinitions/PushContainerImageECR.recipe | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index c582dd7b8..2ed189072 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -97,15 +97,15 @@ public async Task> EvaluateSystemCapabilities(Recommendat { if (systemCapabilities.NodeJsVersion == null) { - message = $"The selected deployment uses the AWS CDK, which requires Node.js. AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended. " + - "Please restart your IDE/Shell after installing Node.js."; + + message = $"Install Node.js {MinimumNodeJSVersion} or later and restart your IDE/Shell. The latest Node.js LTS version is recommended. This deployment option uses the AWS CDK, which requires Node.js."; capabilities.Add(new SystemCapability(NODEJS_DEPENDENCY_NAME, message, NODEJS_INSTALLATION_URL)); } else if (systemCapabilities.NodeJsVersion < MinimumNodeJSVersion) { - message = $"The selected deployment uses the AWS CDK, which requires a version of Node.js higher than your current installation ({systemCapabilities.NodeJsVersion}). " + - $"AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended. Please restart your IDE/Shell after installing Node.js"; + message = $"Install Node.js {MinimumNodeJSVersion} or later and restart your IDE/Shell. The latest Node.js LTS version is recommended. This deployment option uses the AWS CDK, which requires Node.js version higher than your current installation ({systemCapabilities.NodeJsVersion}). "; + capabilities.Add(new SystemCapability(NODEJS_DEPENDENCY_NAME, message, NODEJS_INSTALLATION_URL)); } @@ -115,12 +115,12 @@ public async Task> EvaluateSystemCapabilities(Recommendat { if (!systemCapabilities.DockerInfo.DockerInstalled) { - message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for your OS."; + message = "Install and start Docker version appropriate for your OS. This deployment option requires Docker, which was not detected."; capabilities.Add(new SystemCapability(DOCKER_DEPENDENCY_NAME, message, DOCKER_INSTALLATION_URL)); } else if (!systemCapabilities.DockerInfo.DockerContainerType.Equals("linux", StringComparison.OrdinalIgnoreCase)) { - message = "The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue."; + message = "This is Linux-based deployment. Switch your Docker from Windows to Linux container mode."; capabilities.Add(new SystemCapability(DOCKER_DEPENDENCY_NAME, message)); } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 44a6bac5c..6604ce8d1 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/AspNetAppAppRunner", "CdkProjectTemplateId": "netdeploy.AspNetAppAppRunner", - "ShortDescription": ""Deploys as Linux container image to a fully managed service. Dockerfile will be automatically generated if needed.", - "Description": "This ASP.NET Core application will be built as a container image on Linux and deployed to AWS App Runner, a fully managed service for web applications and APIs. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy your web application as a Linux container image on a fully managed environment.", + "ShortDescription": "Deploys as Linux container image to a fully managed environment. Dockerfile will be automatically generated if needed.", + "Description": "This ASP.NET Core application will be built as a container image on Linux and deployed to AWS App Runner, a fully managed service for web applications and APIs. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your web application as a Linux container image on a fully managed environment.", "TargetService": "AWS App Runner", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 763b21c4e..129db936b 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -8,7 +8,7 @@ "CdkProjectTemplate": "../CdkTemplates/AspNetAppEcsFargate", "CdkProjectTemplateId": "netdeploy.AspNetAppEcsFargate", "ShortDescription": "Deploys as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", - "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy your application as a container image on Linux.", + "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your application as a container image on Linux.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index 80deb9b8d..5191c4101 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "DotnetPublishZipFile", "CdkProjectTemplate": "../CdkTemplates/AspNetAppElasticBeanstalkLinux", "CdkProjectTemplateId": "netdeploy.AspNetAppElasticBeanstalkLinux", - "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", + "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "TargetService": "AWS Elastic Beanstalk", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe index 9d65e2a50..76cb5eccd 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe @@ -6,8 +6,8 @@ "DisableNewDeployments": true, "DeploymentType": "BeanstalkEnvironment", "DeploymentBundle": "DotnetPublishZipFile", - "Description": "This ASP.NET Core application will be built and deployed to an existing AWS Elastic Beanstalk environment. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", + "Description": "This ASP.NET Core application will be built and deployed to an existing AWS Elastic Beanstalk environment. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "TargetService": "AWS Elastic Beanstalk", "RecipePriority": 0, diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe index 3f4dd45e6..a73c6b182 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "DotnetPublishZipFile", "CdkProjectTemplate": "../CdkTemplates/BlazorWasm", "CdkProjectTemplateId": "netdeploy.BlazorWasm", - "Description": "This Blazor WebAssembly application will be built and deployed to a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor applications will be exposed publicly through a CloudFront distribution using the Amazon S3 bucket as the origin.", - "ShortDescription": "Hosts Blazor WebAssembly applicationin an Amazon S3 bucket with Amazon CloudFront as a content delivery network.", + "ShortDescription": "Hosts Blazor WebAssembly application in an Amazon S3 bucket with Amazon CloudFront as a content delivery network.", + "Description": "This Blazor WebAssembly application will be built and hosted in a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor application will be exposed publicly through a CloudFront distribution using the Amazon S3 bucket as the origin.", "TargetService": "Amazon S3 and Amazon CloudFront", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index 29360677a..171459778 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/ConsoleAppECSFargateScheduleTask", "CdkProjectTemplateId": "netdeploy.ConsoleAppECSFargateScheduleTask", - "Description": "This .NET Console application will be built using a Docker file and deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a scheduled task as a container image on Linux.", - "ShortDescription": "Deploys a scheduled task as a linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", + "Description": "This .NET Console application will be built using a Dockerfile and deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a scheduled task as a container image on Linux.", + "ShortDescription": "Deploys a scheduled task as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index de46478f9..5001ccf7c 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -7,8 +7,8 @@ "DeploymentBundle": "Container", "CdkProjectTemplate": "../CdkTemplates/ConsoleAppECSFargateService", "CdkProjectTemplateId": "netdeploy.ConsoleAppEcsFargateService", - "Description": "This .NET Console application will be built using a Dockerfile and deployed as a service to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used.\nRecommended if you want to deploy a service as a container image on Linux.", - "ShortDescription": "Deploys a service as a linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", + "Description": "This .NET Console application will be built using a Dockerfile and deployed as a service to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a service as a container image on Linux.", + "ShortDescription": "Deploys a service as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", "DisplayedResources": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe index 26eb0c540..abfef7716 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe @@ -5,8 +5,8 @@ "Name": "Container Image to Amazon Elastic Container Registry (ECR)", "DeploymentType": "ElasticContainerRegistryImage", "DeploymentBundle": "Container", - "Description": "This .NET application will be built using an existing Dockerfile. The Docker container image will then be pushed to Amazon ECR - a fully managed container registry.", - "ShortDescription": "Pushes container image to Amazon ECR - a fully managed container registry.", + "Description": "This .NET application will be built using an existing Dockerfile. The Docker container image will then be pushed to Amazon ECR, a fully managed container registry.", + "ShortDescription": "Pushes container image to a fully managed container registry.", "TargetService": "Amazon Elastic Container Service", "RecipePriority": 0, From 515b70ca91b137a917d01e0ccb07d7a9bd7d62f2 Mon Sep 17 00:00:00 2001 From: Irene Kors Date: Thu, 16 Jun 2022 21:05:35 -0400 Subject: [PATCH 04/18] docs: modified short descriptions --- src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe index a73c6b182..166fd14cf 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe @@ -9,7 +9,7 @@ "CdkProjectTemplateId": "netdeploy.BlazorWasm", "ShortDescription": "Hosts Blazor WebAssembly application in an Amazon S3 bucket with Amazon CloudFront as a content delivery network.", "Description": "This Blazor WebAssembly application will be built and hosted in a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor application will be exposed publicly through a CloudFront distribution using the Amazon S3 bucket as the origin.", - "TargetService": "Amazon S3 and Amazon CloudFront", + "TargetService": "Amazon S3", "DisplayedResources": [ { From aee54ec87d82e671be8e420ff2fc890262c4b88f Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Fri, 10 Jun 2022 17:22:40 +0000 Subject: [PATCH 05/18] build: version bump to 0.48 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 54e313fc8..5db4b2865 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.47", + "version": "0.48", "publicReleaseRefSpec": [ ".*" ], From 9974447c16d297672d799ed3c46f6359a84e4e9d Mon Sep 17 00:00:00 2001 From: Philippe El Asmar <53088140+philasmar@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:48:17 -0400 Subject: [PATCH 06/18] build: update the PR check infra to support node 14 --- buildtools/ci.buildspec.yml | 3 +-- buildtools/ci.template.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml index f3827bd9a..de465f79d 100644 --- a/buildtools/ci.buildspec.yml +++ b/buildtools/ci.buildspec.yml @@ -3,10 +3,9 @@ version: 0.2 phases: install: runtime-versions: - nodejs: 12 + nodejs: 14 commands: # install .NET SDK - - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 5.0 - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 6.0 - export PATH="$PATH:$HOME/.dotnet" pre_build: diff --git a/buildtools/ci.template.yml b/buildtools/ci.template.yml index af25bad0b..f967cf09d 100644 --- a/buildtools/ci.template.yml +++ b/buildtools/ci.template.yml @@ -95,7 +95,7 @@ Resources: ComputeType: BUILD_GENERAL1_LARGE Type: LINUX_CONTAINER ImagePullCredentialsType: CODEBUILD - Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 + Image: aws/codebuild/standard:5.0 EnvironmentVariables: - Name: TEST_RUNNER_ROLE_ARN Type: PLAINTEXT From 2c91516166250c0c781ce083b756758449f27c99 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 15 Jun 2022 10:08:13 -0400 Subject: [PATCH 07/18] feat: add validator to check if VPC has subnets in different AZs --- .../OptionSettingItemValidatorList.cs | 6 ++- .../VPCSubnetsInDifferentAZsValidator.cs | 42 ++++++++++++++++ .../Recipes/Validation/ValidatorFactory.cs | 3 +- .../ASP.NETAppECSFargate.recipe | 3 ++ .../aws-deploy-recipe-schema.json | 3 +- ...FargateOptionSettingItemValidationTests.cs | 48 +++++++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VPCSubnetsInDifferentAZsValidator.cs diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs index 2bf5af7fe..37441b339 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs @@ -60,6 +60,10 @@ public enum OptionSettingItemValidatorList /// /// Must be paired with /// - Comparison + Comparison, + /// + /// Must be paired with + /// + VPCSubnetsInDifferentAZs } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VPCSubnetsInDifferentAZsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VPCSubnetsInDifferentAZsValidator.cs new file mode 100644 index 000000000..0f0b6fd99 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/VPCSubnetsInDifferentAZsValidator.cs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Threading.Tasks; +using AWS.Deploy.Common.Data; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validates that the selected VPC must have at least two subnets in two different Availability Zones + /// + public class VPCSubnetsInDifferentAZsValidator : IOptionSettingItemValidator + { + private static readonly string defaultValidationFailedMessage = "Selected VPC must have at least two subnets in two different Availability Zones."; + public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; + + private readonly IAWSResourceQueryer _awsResourceQueryer; + + public VPCSubnetsInDifferentAZsValidator(IAWSResourceQueryer awsResourceQueryer) + { + _awsResourceQueryer = awsResourceQueryer; + } + + public async Task Validate(object input, Recommendation recommendation, OptionSettingItem optionSettingItem) + { + var vpcId = input?.ToString(); + if (string.IsNullOrEmpty(vpcId)) + return ValidationResult.Failed("A VPC ID is not specified. Please select a valid VPC ID."); + + var subnets = await _awsResourceQueryer.DescribeSubnets(vpcId); + var availabilityZones = new HashSet(); + foreach (var subnet in subnets) + availabilityZones.Add(subnet.AvailabilityZoneId); + + if (availabilityZones.Count >= 2) + return ValidationResult.Valid(); + else + return ValidationResult.Failed(ValidationFailedMessage); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 3634bc65b..5301550ba 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -59,7 +59,8 @@ public ValidatorFactory(IServiceProvider serviceProvider) { OptionSettingItemValidatorList.SubnetsInVpc, typeof(SubnetsInVpcValidator) }, { OptionSettingItemValidatorList.SecurityGroupsInVpc, typeof(SecurityGroupsInVpcValidator) }, { OptionSettingItemValidatorList.Uri, typeof(UriValidator) }, - { OptionSettingItemValidatorList.Comparison, typeof(ComparisonValidator) } + { OptionSettingItemValidatorList.Comparison, typeof(ComparisonValidator) }, + { OptionSettingItemValidatorList.VPCSubnetsInDifferentAZs, typeof(VPCSubnetsInDifferentAZsValidator) } }; private static readonly Dictionary _recipeValidatorTypeMapping = new() diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 129db936b..f3f51d70c 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -324,6 +324,9 @@ "Regex": "^vpc-([0-9a-f]{8}|[0-9a-f]{17})$", "ValidationFailedMessage": "Invalid VPC ID. The VPC ID must start with the \"vpc-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example vpc-abc88de9 is a valid VPC ID." } + }, + { + "ValidatorType": "VPCSubnetsInDifferentAZs" } ], "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 83a79de20..a6e822682 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -614,7 +614,8 @@ "SubnetsInVpc", "SecurityGroupsInVpc", "Uri", - "Comparison" + "Comparison", + "VPCSubnetsInDifferentAZs" ] } }, 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 f5c6e6310..7349f3385 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -200,6 +200,44 @@ public async Task ECSClusterNameValidationTest_Valid() await Validate(optionSettingItem, "WebApp1", true); } + [Fact] + public async Task VpcIdHasSubnetsInDifferentAZs_DifferentZones_Valid() + { + _awsResourceQueryer.Setup(x => x.DescribeSubnets(It.IsAny())).ReturnsAsync( + new List { + new Amazon.EC2.Model.Subnet { AvailabilityZoneId = "AZ1"}, + new Amazon.EC2.Model.Subnet { AvailabilityZoneId = "AZ2"} + }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetVPCSubnetsInDifferentAZsValidatorConfig()); + await Validate(optionSettingItem, "vpc-1234abcd", true); + } + + [Fact] + public async Task VpcIdHasSubnetsInDifferentAZs_SingleSubnet_Invalid() + { + _awsResourceQueryer.Setup(x => x.DescribeSubnets(It.IsAny())).ReturnsAsync( + new List { + new Amazon.EC2.Model.Subnet { AvailabilityZoneId = "AZ1"} + }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetVPCSubnetsInDifferentAZsValidatorConfig()); + await Validate(optionSettingItem, "vpc-1234abcd", false); + } + + [Fact] + public async Task VpcIdHasSubnetsInDifferentAZs_SingleZone_Invalid() + { + _awsResourceQueryer.Setup(x => x.DescribeSubnets(It.IsAny())).ReturnsAsync( + new List { + new Amazon.EC2.Model.Subnet { AvailabilityZoneId = "AZ1"}, + new Amazon.EC2.Model.Subnet { AvailabilityZoneId = "AZ1"} + }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetVPCSubnetsInDifferentAZsValidatorConfig()); + await Validate(optionSettingItem, "vpc-1234abcd", false); + } + [Fact] public async Task ECSClusterNameValidationTest_Invalid() { @@ -280,6 +318,16 @@ private OptionSettingItemValidatorConfig GetExistingResourceValidatorConfig(stri return existingResourceValidatorConfig; } + private OptionSettingItemValidatorConfig GetVPCSubnetsInDifferentAZsValidatorConfig() + { + var vpcSubnetsInDifferentAZsValidatorConfig = new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.VPCSubnetsInDifferentAZs, + Configuration = new VPCSubnetsInDifferentAZsValidator(_awsResourceQueryer.Object) + }; + return vpcSubnetsInDifferentAZsValidatorConfig; + } + private OptionSettingItemValidatorConfig GetRangeValidatorConfig(int min, int max) { var rangeValidatorConfig = new OptionSettingItemValidatorConfig From 32b4cf5d697f7266c5569471afb989f5874f5257 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 15 Jun 2022 14:17:46 -0400 Subject: [PATCH 08/18] chore: update the supported NodeJS version from 10 to 14 --- README.md | 2 +- .../SystemCapabilityEvaluator.cs | 2 +- .../SaveCdkDeploymentProject/RecommendationTests.cs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1e439e483..078a203da 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ To take advantage of this library you’ll need: * Note: You need to make sure to add the appropriate CloudFormation permissions to your credentials's profile / assumed role. * For SSO, please visit the [.NET SDK Reference Guide](https://docs.aws.amazon.com/sdkref/latest/guide/access-sso.html). * [.NET Core 3.1](https://dotnet.microsoft.com/download) or later -* [Node.js 10.3](https://nodejs.org/en/download/) or later +* [Node.js 14](https://nodejs.org/en/download/) or later * The [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) is used by this tool to create the AWS infrastructure to run applications. The CDK requires Node.js to function. This dependency is needed for deployments that are CDK based. If you will be using deployments that are not CDK based, you are not required to have this dependency. * (optional) [Docker](https://docs.docker.com/get-docker/) * Used when deploying to a container based service like Amazon Elastic Container Service (Amazon ECS) diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index 2ed189072..2782f4452 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -25,7 +25,7 @@ public class SystemCapabilityEvaluator : ISystemCapabilityEvaluator private const string DOCKER_INSTALLATION_URL = "https://docs.docker.com/engine/install/"; private readonly ICommandLineWrapper _commandLineWrapper; - private static readonly Version MinimumNodeJSVersion = new Version(10,13,0); + private static readonly Version MinimumNodeJSVersion = new Version(14,17,0); public SystemCapabilityEvaluator(ICommandLineWrapper commandLineWrapper) { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 0eab218db..a5620faf2 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -75,11 +75,11 @@ public async Task GenerateRecommendationsWithoutCustomRecipes() // ASSERT recommendations.Count.ShouldEqual(5); - recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe + recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe recommendations[1].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe recommendations[3].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[4].Name.ShouldEqual("Push Container Images to Amazon Elastic Container Registry"); // default recipe + recommendations[4].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe } [Fact] @@ -114,11 +114,11 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() recommendations.Count.ShouldEqual(7); recommendations[0].Name.ShouldEqual(customEcsRecipeName); // custom recipe recommendations[1].Name.ShouldEqual(customEbsRecipeName); // custom recipe - recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe + recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[6].Name.ShouldEqual("Push Container Images to Amazon Elastic Container Registry"); // default recipe + recommendations[6].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -165,7 +165,7 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() recommendations[0].Name.ShouldEqual(customEbsRecipeName); recommendations[1].Name.ShouldEqual(customEcsRecipeName); recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); - recommendations[3].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); + recommendations[3].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); From ddc1fd46dad974051b38a97a083bca5cb507f361 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 14 Jun 2022 21:06:59 -0400 Subject: [PATCH 09/18] feat: Allow users to override the default deploy tool workspace directory --- mkdocs.yml | 1 + .../docs/getting-started/custom-workspace.md | 11 ++ .../troubleshooting-guide/other-issues.md | 8 +- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 9 +- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 8 +- .../CustomServiceCollectionExtension.cs | 2 + .../Controllers/DeploymentController.cs | 6 +- src/AWS.Deploy.Common/Exceptions.cs | 3 +- src/AWS.Deploy.Constants/CDK.cs | 23 --- src/AWS.Deploy.Constants/CLI.cs | 1 + .../CDK/CDKManager.cs | 6 +- .../CdkProjectHandler.cs | 13 +- .../DeployToolWorkspaceMetadata.cs | 56 ++++++++ .../CdkDeploymentCommand.cs | 4 +- src/AWS.Deploy.Orchestration/Exceptions.cs | 8 ++ .../LocalUserSettingsEngine.cs | 6 +- src/AWS.Deploy.Orchestration/Orchestrator.cs | 5 +- .../Utilities/EnvironmentVariableManager.cs | 34 +++++ .../Utilities/Helpers.cs | 91 ++++++++++++ .../Utilities/WaitUntilHelper.cs | 35 ----- .../LocalUserSettingsTests.cs | 15 +- .../ServerModeTests.cs | 4 +- .../Helpers/HttpHelper.cs | 2 +- .../RecommendationTests.cs | 7 +- .../ServerModeTests.cs | 4 +- .../Utilities/ServerModeUtilities.cs | 2 +- .../WebAppNoDockerFileTests.cs | 43 ++++++ .../ApplyPreviousSettingsTests.cs | 2 +- .../CDK/CDKManagerTests.cs | 12 +- .../CDK/CDKProjectHandlerTests.cs | 14 +- .../DeployToolWorkspaceTests.cs | 133 ++++++++++++++++++ 31 files changed, 474 insertions(+), 94 deletions(-) create mode 100644 site/content/docs/getting-started/custom-workspace.md create mode 100644 src/AWS.Deploy.Orchestration/DeployToolWorkspaceMetadata.cs create mode 100644 src/AWS.Deploy.Orchestration/Utilities/EnvironmentVariableManager.cs create mode 100644 src/AWS.Deploy.Orchestration/Utilities/Helpers.cs delete mode 100644 src/AWS.Deploy.Orchestration/Utilities/WaitUntilHelper.cs create mode 100644 test/AWS.Deploy.Orchestration.UnitTests/DeployToolWorkspaceTests.cs diff --git a/mkdocs.yml b/mkdocs.yml index 37e2de254..1a8a8dfb5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ nav: - Pre-requisites: docs/getting-started/pre-requisites.md - How to install: docs/getting-started/installation.md - Set up Credentials: docs/getting-started/setup-creds.md + - Set up custom workspace: docs/getting-started/custom-workspace.md - Deploy "Hello World": docs/getting-started/run-tool.md - Support matrix: docs/support.md # - Supported deployments: diff --git a/site/content/docs/getting-started/custom-workspace.md b/site/content/docs/getting-started/custom-workspace.md new file mode 100644 index 000000000..be0bdcf94 --- /dev/null +++ b/site/content/docs/getting-started/custom-workspace.md @@ -0,0 +1,11 @@ +The default workspace used by AWS.Deploy.Tools is `$USERPROFILE/.aws-dotnet-deploy` on Windows and `$USER/.aws-dotnet-deploy` on Unix based OS. This workspace is used to create the CDK project and any other temporary files used by the tool. + +You can override the default workspace by the setting the `AWS_DOTNET_DEPLOYTOOL_WORKSPACE` environment variable. + +It must satisfy the following constraints: + +* It must point to a valid directory that exists on the disk. +* The directory path must not have any whitespace characters in it. + +**Setting up a custom workspace is optional for most users.** However, on Windows OS, if the `$USERPROFILE` path contains a whitespace character then the deployment will fail. +In that case, users are required to set up a custom workspace that satisfies the above constraints. \ No newline at end of file diff --git a/site/content/troubleshooting-guide/other-issues.md b/site/content/troubleshooting-guide/other-issues.md index 12b139b67..75f29ac8d 100644 --- a/site/content/troubleshooting-guide/other-issues.md +++ b/site/content/troubleshooting-guide/other-issues.md @@ -61,4 +61,10 @@ AWS.Deploy.Tools, internally uses a variety of different services to host your . **Resolution**: You can refer to the official AWS documentation on IAM policies: * See [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_managed-policies.html) for a tutorial on how to create customer managed IAM policies. -* See [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_policies.html) for troubleshooting IAM policies. \ No newline at end of file +* See [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_policies.html) for troubleshooting IAM policies. + +## Deployment failure due to whitespace character in USERPROFILE path + +**Why is this happening**: This happens due to a know issue with the AWS Cloud Development Kit (CDK). The CDK is used to AWS.Deploy.Tools under the covers and it cannot cannot access the `$TEMP` directory inside the `$USERPROFILE` path if it contains a whitespace character. + +**Resolution**: See [here](../../docs/getting-started/custom-workspace) for guidance on setting a custom workspace that will be used by AWS.Deploy.tools. \ No newline at end of file diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 32527926c..d2e01a4f6 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -77,6 +77,7 @@ public class CommandFactory : ICommandFactory private readonly IOptionSettingHandler _optionSettingHandler; private readonly IValidatorFactory _validatorFactory; private readonly IRecipeHandler _recipeHandler; + private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata; public CommandFactory( IServiceProvider serviceProvider, @@ -105,7 +106,9 @@ public CommandFactory( IAWSServiceHandler awsServiceHandler, IOptionSettingHandler optionSettingHandler, IValidatorFactory validatorFactory, - IRecipeHandler recipeHandler) + IRecipeHandler recipeHandler, + IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata + ) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -134,6 +137,7 @@ public CommandFactory( _optionSettingHandler = optionSettingHandler; _validatorFactory = validatorFactory; _recipeHandler = recipeHandler; + _deployToolWorkspaceMetadata = deployToolWorkspaceMetadata; } public Command BuildRootCommand() @@ -236,7 +240,8 @@ private Command BuildDeployCommand() _awsServiceHandler, _optionSettingHandler, _validatorFactory, - _recipeHandler); + _recipeHandler, + _deployToolWorkspaceMetadata); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; if (!string.IsNullOrEmpty(deploymentProjectPath)) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 562cfdcf5..542c4bae7 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -54,6 +54,7 @@ public class DeployCommand private readonly IOptionSettingHandler _optionSettingHandler; private readonly IValidatorFactory _validatorFactory; private readonly IRecipeHandler _recipeHandler; + private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata; public DeployCommand( IServiceProvider serviceProvider, @@ -79,7 +80,8 @@ public DeployCommand( IAWSServiceHandler awsServiceHandler, IOptionSettingHandler optionSettingHandler, IValidatorFactory validatorFactory, - IRecipeHandler recipeHandler) + IRecipeHandler recipeHandler, + IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -105,6 +107,7 @@ public DeployCommand( _optionSettingHandler = optionSettingHandler; _validatorFactory = validatorFactory; _recipeHandler = recipeHandler; + _deployToolWorkspaceMetadata = deployToolWorkspaceMetadata; } public async Task ExecuteAsync(string applicationName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) @@ -170,7 +173,8 @@ private void DisplayOutputResources(List displayedResourc _fileManager, _directoryManager, _awsServiceHandler, - _optionSettingHandler); + _optionSettingHandler, + _deployToolWorkspaceMetadata); // Determine what recommendations are possible for the project. var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath); diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 464cc7ab8..7290d6e10 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -67,6 +67,8 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IOptionSettingHandler), typeof(OptionSettingHandler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IValidatorFactory), typeof(ValidatorFactory), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IRecipeHandler), typeof(RecipeHandler), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentVariableManager), typeof(EnvironmentVariableManager), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeployToolWorkspaceMetadata), typeof(DeployToolWorkspaceMetadata), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IPackageJsonGenerator), (serviceProvider) => new PackageJsonGenerator(packageJsonTemplate), lifetime)); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 1dbb2cea1..aa1d5fc39 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -724,7 +724,8 @@ private CdkProjectHandler CreateCdkProjectHandler(SessionState state, IServicePr serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService() + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() ); } @@ -754,7 +755,8 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService() + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() ); } } diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 074fc39b8..cc67972b2 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -117,7 +117,8 @@ public enum DeployToolErrorCode InvalidCloudApplicationName = 10009500, SelectedValueIsNotAllowed = 10009600, MissingValidatorConfiguration = 10009700, - InvalidFilePath = 10009800 + InvalidFilePath = 10009800, + InvalidDeployToolWorkspace = 10009900 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Constants/CDK.cs b/src/AWS.Deploy.Constants/CDK.cs index a2c5c1563..562f8e7ac 100644 --- a/src/AWS.Deploy.Constants/CDK.cs +++ b/src/AWS.Deploy.Constants/CDK.cs @@ -8,34 +8,11 @@ namespace AWS.Deploy.Constants { internal static class CDK { - /// - /// Deployment tool workspace directory to create CDK app during the deployment. - /// - public static string DeployToolWorkspaceDirectoryRoot { - get - { - var deployToolWorkspace = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aws-dotnet-deploy"); - if (!Directory.Exists(deployToolWorkspace)) - Directory.CreateDirectory(deployToolWorkspace); - return deployToolWorkspace; - } - } - - /// - /// Directory that contains CDK projects - /// - public static string ProjectsDirectory => Path.Combine(DeployToolWorkspaceDirectoryRoot, "Projects"); - /// /// Default version of CDK CLI /// public static readonly Version DefaultCDKVersion = Version.Parse("2.13.0"); - /// - /// The file path of the CDK bootstrap template to be used - /// - public static string CDKBootstrapTemplatePath => Path.Combine(DeployToolWorkspaceDirectoryRoot, "CDKBootstrapTemplate.yaml"); - /// /// The version number CDK bootstrap specified in CDKBootstrapTemplate.yaml /// diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index e576facdc..c39d84a77 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -22,5 +22,6 @@ internal static class CLI public const string PROMPT_CHOOSE_DEPLOYMENT_TARGET = "Choose deployment target"; public const string CLI_APP_NAME = "AWS .NET Deployment Tool"; + public const string WORKSPACE_ENV_VARIABLE = "AWS_DOTNET_DEPLOYTOOL_WORKSPACE"; } } diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs b/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs index 8946acb15..1d66adb44 100644 --- a/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs +++ b/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs @@ -35,14 +35,16 @@ public class CDKManager : ICDKManager private readonly ICDKInstaller _cdkInstaller; private readonly INPMPackageInitializer _npmPackageInitializer; private readonly IOrchestratorInteractiveService _interactiveService; + private readonly IDeployToolWorkspaceMetadata _workspaceMetadata; private const string TemplateIdentifier = "AWS.Deploy.Orchestration.CDK.CDKBootstrapTemplate.yaml"; - public CDKManager(ICDKInstaller cdkInstaller, INPMPackageInitializer npmPackageInitializer, IOrchestratorInteractiveService interactiveService) + public CDKManager(ICDKInstaller cdkInstaller, INPMPackageInitializer npmPackageInitializer, IOrchestratorInteractiveService interactiveService, IDeployToolWorkspaceMetadata workspaceMetadata) { _cdkInstaller = cdkInstaller; _npmPackageInitializer = npmPackageInitializer; _interactiveService = interactiveService; + _workspaceMetadata = workspaceMetadata; } private async Task CreateCDKBootstrapTemplate() @@ -50,7 +52,7 @@ private async Task CreateCDKBootstrapTemplate() // The CDK bootstrap template can be generated by running 'cdk bootstrap --show-template'. // We need to keep the template up to date while making sure that the 'Staging Bucket' retention policies are set to 'Delete'. var cdkBootstrapTemplate = typeof(CdkProjectHandler).Assembly.ReadEmbeddedFile(TemplateIdentifier); - await using var cdkBootstrapTemplateFile = new StreamWriter(Constants.CDK.CDKBootstrapTemplatePath); + await using var cdkBootstrapTemplateFile = new StreamWriter(_workspaceMetadata.CDKBootstrapTemplatePath); await cdkBootstrapTemplateFile.WriteAsync(cdkBootstrapTemplate); } diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index d1b661961..f559cc7db 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -35,13 +35,15 @@ public class CdkProjectHandler : ICdkProjectHandler private readonly IDirectoryManager _directoryManager; private readonly IAWSResourceQueryer _awsResourceQueryer; private readonly IFileManager _fileManager; + private readonly IDeployToolWorkspaceMetadata _workspaceMetadata; public CdkProjectHandler( IOrchestratorInteractiveService interactiveService, ICommandLineWrapper commandLineWrapper, IAWSResourceQueryer awsResourceQueryer, IFileManager fileManager, - IOptionSettingHandler optionSettingHandler) + IOptionSettingHandler optionSettingHandler, + IDeployToolWorkspaceMetadata workspaceMetadata) { _interactiveService = interactiveService; _commandLineWrapper = commandLineWrapper; @@ -49,6 +51,7 @@ public CdkProjectHandler( _appSettingsBuilder = new CdkAppSettingsSerializer(optionSettingHandler); _directoryManager = new DirectoryManager(); _fileManager = fileManager; + _workspaceMetadata = workspaceMetadata; } public async Task ConfigureCdkProject(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation) @@ -110,7 +113,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication if (await DetermineIfCDKBootstrapShouldRun()) { // Ensure region is bootstrapped - var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\" --template \"{Constants.CDK.CDKBootstrapTemplatePath}\"", + var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\" --template \"{_workspaceMetadata.CDKBootstrapTemplatePath}\"", workingDirectory: cdkProjectPath, needAwsCredentials: true, redirectIO: true, @@ -203,7 +206,7 @@ public async Task DetermineIfCDKBootstrapShouldRun() private async Task RetrieveStackId(CloudApplication cloudApplication, CancellationToken cancellationToken) { Stack? stack = null; - await WaitUntilHelper.WaitUntil(async () => + await Helpers.WaitUntil(async () => { try { @@ -256,7 +259,7 @@ public string CreateCdkProject(Recommendation recommendation, OrchestratorSessio { saveCdkDirectoryPath = Path.Combine( - Constants.CDK.ProjectsDirectory, + _workspaceMetadata.ProjectsDirectory, Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); assemblyName = recommendation.ProjectDefinition.AssemblyName; @@ -280,7 +283,7 @@ public string CreateCdkProject(Recommendation recommendation, OrchestratorSessio public void DeleteTemporaryCdkProject(string cdkProjectPath) { - var parentPath = Path.GetFullPath(Constants.CDK.ProjectsDirectory); + var parentPath = Path.GetFullPath(_workspaceMetadata.ProjectsDirectory); cdkProjectPath = Path.GetFullPath(cdkProjectPath); if (!cdkProjectPath.StartsWith(parentPath)) diff --git a/src/AWS.Deploy.Orchestration/DeployToolWorkspaceMetadata.cs b/src/AWS.Deploy.Orchestration/DeployToolWorkspaceMetadata.cs new file mode 100644 index 000000000..02f90d389 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/DeployToolWorkspaceMetadata.cs @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.Utilities; + +namespace AWS.Deploy.Orchestration +{ + public interface IDeployToolWorkspaceMetadata + { + /// + /// Deployment tool workspace directory to create CDK app during the deployment. + /// + string DeployToolWorkspaceDirectoryRoot { get; } + + /// + /// Directory that contains CDK projects + /// + string ProjectsDirectory { get; } + + /// + /// The file path of the CDK bootstrap template to be used + /// + string CDKBootstrapTemplatePath { get; } + } + + public class DeployToolWorkspaceMetadata : IDeployToolWorkspaceMetadata + { + private readonly IDirectoryManager _directoryManager; + private readonly IEnvironmentVariableManager _environmentVariableManager; + + public string DeployToolWorkspaceDirectoryRoot + { + get + { + var workspace = Helpers.GetDeployToolWorkspaceDirectoryRoot(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), _directoryManager, _environmentVariableManager); + if (!_directoryManager.Exists(workspace)) + _directoryManager.CreateDirectory(workspace); + return workspace; + } + } + + public string ProjectsDirectory => Path.Combine(DeployToolWorkspaceDirectoryRoot, "Projects"); + public string CDKBootstrapTemplatePath => Path.Combine(DeployToolWorkspaceDirectoryRoot, "CDKBootstrapTemplate.yaml"); + + public DeployToolWorkspaceMetadata(IDirectoryManager directoryManager, IEnvironmentVariableManager environmentVariableManager) + { + _directoryManager = directoryManager; + _environmentVariableManager = environmentVariableManager; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs index 446fb76a2..a5b08b923 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs @@ -30,6 +30,8 @@ public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloud throw new InvalidOperationException($"{nameof(orchestrator._cdkVersionDetector)} must not be null."); if (orchestrator._directoryManager == null) throw new InvalidOperationException($"{nameof(orchestrator._directoryManager)} must not be null."); + if (orchestrator._workspaceMetadata == null) + throw new InvalidOperationException($"{nameof(orchestrator._workspaceMetadata)} must not be null."); orchestrator._interactiveService.LogSectionStart("Configuring AWS Cloud Development Kit (CDK)", "Ensure a compatible CDK version is installed and the CDK bootstrap CloudFormation stack has been created. A CDK project to perform the deployment is generated unless an existing deployment project was selected."); @@ -38,7 +40,7 @@ public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloud var projFiles = orchestrator._directoryManager.GetProjFiles(cdkProject); var cdkVersion = orchestrator._cdkVersionDetector.Detect(projFiles); - await orchestrator._cdkManager.EnsureCompatibleCDKExists(recommendation.Recipe.PersistedDeploymentProject ? cdkProject : Constants.CDK.DeployToolWorkspaceDirectoryRoot, cdkVersion); + await orchestrator._cdkManager.EnsureCompatibleCDKExists(recommendation.Recipe.PersistedDeploymentProject ? cdkProject : orchestrator._workspaceMetadata.DeployToolWorkspaceDirectoryRoot, cdkVersion); try { diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index b800fe264..83c82374c 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -236,4 +236,12 @@ public class FailedToCreateCDKProjectException : DeployToolException { public FailedToCreateCDKProjectException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if the deploy tool workspace is invalid + /// + public class InvalidDeployToolWorkspaceException : DeployToolException + { + public InvalidDeployToolWorkspaceException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs index 502aa578d..9d3dc2378 100644 --- a/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs +++ b/src/AWS.Deploy.Orchestration/LocalUserSettings/LocalUserSettingsEngine.cs @@ -24,13 +24,15 @@ public class LocalUserSettingsEngine : ILocalUserSettingsEngine { private readonly IFileManager _fileManager; private readonly IDirectoryManager _directoryManager; + private readonly IDeployToolWorkspaceMetadata _workspaceMetadata; private const string LOCAL_USER_SETTINGS_FILE_NAME = "local-user-settings.json"; - public LocalUserSettingsEngine(IFileManager fileManager, IDirectoryManager directoryManager) + public LocalUserSettingsEngine(IFileManager fileManager, IDirectoryManager directoryManager, IDeployToolWorkspaceMetadata workspaceMetadata) { _fileManager = fileManager; _directoryManager = directoryManager; + _workspaceMetadata = workspaceMetadata; } /// @@ -213,7 +215,7 @@ private async Task WriteLocalUserSettingsFile(LocalUserSettings deployme /// private string GetLocalUserSettingsFilePath() { - var deployToolWorkspace = _directoryManager.GetDirectoryInfo(Constants.CDK.DeployToolWorkspaceDirectoryRoot).FullName; + var deployToolWorkspace = _directoryManager.GetDirectoryInfo(_workspaceMetadata.DeployToolWorkspaceDirectoryRoot).FullName; var localUserSettingsFileFullPath = Path.Combine(deployToolWorkspace, LOCAL_USER_SETTINGS_FILE_NAME); return localUserSettingsFileFullPath; } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 0dcb454b0..7bcd60755 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -44,6 +44,7 @@ public class Orchestrator internal readonly OrchestratorSession? _session; internal readonly IAWSServiceHandler? _awsServiceHandler; private readonly IOptionSettingHandler? _optionSettingHandler; + internal readonly IDeployToolWorkspaceMetadata? _workspaceMetadata; public Orchestrator( OrchestratorSession session, @@ -59,7 +60,8 @@ public Orchestrator( IFileManager fileManager, IDirectoryManager directoryManager, IAWSServiceHandler awsServiceHandler, - IOptionSettingHandler optionSettingHandler) + IOptionSettingHandler optionSettingHandler, + IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata) { _session = session; _interactiveService = interactiveService; @@ -75,6 +77,7 @@ public Orchestrator( _directoryManager = directoryManager; _awsServiceHandler = awsServiceHandler; _optionSettingHandler = optionSettingHandler; + _workspaceMetadata = deployToolWorkspaceMetadata; } public Orchestrator(OrchestratorSession session, IRecipeHandler recipeHandler) diff --git a/src/AWS.Deploy.Orchestration/Utilities/EnvironmentVariableManager.cs b/src/AWS.Deploy.Orchestration/Utilities/EnvironmentVariableManager.cs new file mode 100644 index 000000000..f9b424e87 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/Utilities/EnvironmentVariableManager.cs @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Orchestration.Utilities +{ + public interface IEnvironmentVariableManager + { + /// + /// Retrieves the environment variable + /// + string? GetEnvironmentVariable(string variable); + + /// + /// Stores the environment variable with the specified value. The variable is scoped to the current process + /// + void SetEnvironmentVariable(string variable, string? value); + } + + public class EnvironmentVariableManager : IEnvironmentVariableManager + { + public string? GetEnvironmentVariable(string variable) + { + return Environment.GetEnvironmentVariable(variable); + } + public void SetEnvironmentVariable(string variable, string? value) + { + Environment.SetEnvironmentVariable(variable, value); + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs b/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs new file mode 100644 index 000000000..90997b451 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Orchestration.Utilities +{ + /// + /// This class holds static helper methods + /// + public static class Helpers + { + /// + /// Wait for TimeSpan until isn't satisfied + /// + /// Termination condition for breaking the wait loop + /// Interval between the two executions of the task + /// Interval for timeout, if timeout passes, methods throws + /// Throws when timeout passes + public static async Task WaitUntil(Func> predicate, TimeSpan frequency, TimeSpan timeout, CancellationToken cancellationToken = default) + { + var waitTask = Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested && !await predicate()) + { + await Task.Delay(frequency); + } + }); + + if (waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout))) + { + throw new TimeoutException(); + } + } + + /// + /// This method returns the deployment tool workspace directory to create the CDK app during deployment. + /// It will first look for AWS_DOTNET_DEPLOYTOOL_WORKSPACE environment variable set by the user. It will be used as the deploy tool workspace if it points to a valid directory whithout whitespace characters in its path. + /// If the environment variable is set, it will also create a temp directory inside the workspace and set process scoped TEMP and TMP environment variables that point to the temp directory. + /// This additional configuration is required due to a known issue in the CDK - https://github.com/aws/aws-cdk/issues/2532 + /// If the override is not present, then it defaults to USERPROFILE/.aws-dotnet-deploy. + /// It will throw an exception if the USERPROFILE contains a whitespace character. + /// + public static string GetDeployToolWorkspaceDirectoryRoot(string userProfilePath, IDirectoryManager directoryManager, IEnvironmentVariableManager environmentVariableManager) + { + var overridenWorkspace = environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE); + + var defaultWorkSpace = Path.Combine(userProfilePath, ".aws-dotnet-deploy"); + + if (overridenWorkspace == null) + { + return !defaultWorkSpace.Contains(" ") + ? defaultWorkSpace + : throw new InvalidDeployToolWorkspaceException(DeployToolErrorCode.InvalidDeployToolWorkspace, $"The USERPROFILE path ({userProfilePath}) contains a whitespace character and cannot be used as a workspace by the deployment tool. " + + $"Please refer to the troubleshooting guide for setting the {Constants.CLI.WORKSPACE_ENV_VARIABLE} that specifies an alternative workspace directory."); + } + + if (!directoryManager.Exists(overridenWorkspace)) + { + throw new InvalidDeployToolWorkspaceException(DeployToolErrorCode.InvalidDeployToolWorkspace, + $"The {Constants.CLI.WORKSPACE_ENV_VARIABLE} environment variable has been set to \"{overridenWorkspace}\" but it does not point to a valid directory."); + } + + if (overridenWorkspace.Contains(" ")) + { + throw new InvalidDeployToolWorkspaceException(DeployToolErrorCode.InvalidDeployToolWorkspace, + $"The {Constants.CLI.WORKSPACE_ENV_VARIABLE} environment variable ({overridenWorkspace}) contains a whitespace character and cannot be used as a workspace by the deployment tool."); + } + + var tempDir = Path.Combine(overridenWorkspace, "temp"); + if (!directoryManager.Exists(tempDir)) + { + directoryManager.CreateDirectory(tempDir); + } + + // This is done to override the C:\Users\\AppData\Local\Temp directory. + // There is a known issue with CDK where it cannot access the Temp directory if the username contains a whitespace. + environmentVariableManager.SetEnvironmentVariable("TMP", tempDir); + environmentVariableManager.SetEnvironmentVariable("TEMP", tempDir); + + return overridenWorkspace; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Utilities/WaitUntilHelper.cs b/src/AWS.Deploy.Orchestration/Utilities/WaitUntilHelper.cs deleted file mode 100644 index 2cc18db73..000000000 --- a/src/AWS.Deploy.Orchestration/Utilities/WaitUntilHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AWS.Deploy.Orchestration.Utilities -{ - public static class WaitUntilHelper - { - /// - /// Wait for TimeSpan until isn't satisfied - /// - /// Termination condition for breaking the wait loop - /// Interval between the two executions of the task - /// Interval for timeout, if timeout passes, methods throws - /// Throws when timeout passes - public static async Task WaitUntil(Func> predicate, TimeSpan frequency, TimeSpan timeout, CancellationToken cancellationToken = default) - { - var waitTask = Task.Run(async () => - { - while (!cancellationToken.IsCancellationRequested && !await predicate()) - { - await Task.Delay(frequency); - } - }); - - if (waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout))) - { - throw new TimeoutException(); - } - } - } -} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs index 896ea2031..b72bd621a 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/LocalUserSettings/LocalUserSettingsTests.cs @@ -7,7 +7,10 @@ using System.Threading.Tasks; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.Utilities; +using Moq; using Xunit; namespace AWS.Deploy.CLI.Common.UnitTests.LocalUserSettings @@ -17,13 +20,23 @@ public class LocalUserSettingsTests private readonly IFileManager _fileManager; private readonly IDirectoryManager _directoryManager; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; + private readonly Mock _environmentVariableManager; + private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata; public LocalUserSettingsTests() { _fileManager = new TestFileManager(); _directoryManager = new DirectoryManager(); var targetApplicationPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); - _localUserSettingsEngine = new LocalUserSettingsEngine(_fileManager, _directoryManager); + + _environmentVariableManager = new Mock(); + _environmentVariableManager + .Setup(x => x.GetEnvironmentVariable(It.IsAny())) + .Returns(() => null); + + _deployToolWorkspaceMetadata = new DeployToolWorkspaceMetadata(_directoryManager, _environmentVariableManager.Object); + + _localUserSettingsEngine = new LocalUserSettingsEngine(_fileManager, _directoryManager, _deployToolWorkspaceMetadata); } [Fact] diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs index 953799d3a..272196ce6 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs @@ -98,7 +98,7 @@ private async Task WaitForDeployment(RestAPIClient restApiClie // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. await Task.Delay(TimeSpan.FromSeconds(3)); - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { DeploymentStatus status = (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; ; return status != DeploymentStatus.Executing; @@ -109,7 +109,7 @@ await WaitUntilHelper.WaitUntil(async () => private async Task WaitTillServerModeReady(RestAPIClient restApiClient) { - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { SystemStatus status = SystemStatus.Error; try diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/HttpHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/HttpHelper.cs index dc8b90985..6e609b37d 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/HttpHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/HttpHelper.cs @@ -26,7 +26,7 @@ public async Task WaitUntilSuccessStatusCode(string url, TimeSpan frequency, Tim try { - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { var httpResponseMessage = await client.GetAsync(url); return httpResponseMessage.IsSuccessStatusCode; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index a5620faf2..a6c6b7aca 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -233,7 +233,9 @@ private async Task GetOrchestrator(string targetApplicationProject var directoryManager = new DirectoryManager(); var fileManager = new FileManager(); var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); - var localUserSettingsEngine = new LocalUserSettingsEngine(fileManager, directoryManager); + var environmentVariableManager = new Mock().Object; + var deployToolWorkspaceMetadata = new DeployToolWorkspaceMetadata(directoryManager, environmentVariableManager); + var localUserSettingsEngine = new LocalUserSettingsEngine(fileManager, directoryManager, deployToolWorkspaceMetadata); var serviceProvider = new Mock(); var validatorFactory = new ValidatorFactory(serviceProvider.Object); var optionSettingHandler = new OptionSettingHandler(validatorFactory); @@ -254,7 +256,8 @@ private async Task GetOrchestrator(string targetApplicationProject fileManager, directoryManager, new Mock().Object, - new OptionSettingHandler(new Mock().Object)); + new OptionSettingHandler(new Mock().Object), + deployToolWorkspaceMetadata); } private async Task GetCustomRecipeId(string recipeFilePath) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index 69c66e9b8..749356852 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -483,7 +483,7 @@ private async Task WaitForDeployment(RestAPIClient restApiClie GetDeploymentStatusOutput output = null; - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { output = (await restApiClient.GetDeploymentStatusAsync(sessionId)); @@ -501,7 +501,7 @@ await WaitUntilHelper.WaitUntil(async () => private async Task WaitTillServerModeReady(RestAPIClient restApiClient) { - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { SystemStatus status = SystemStatus.Error; try diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs index 204ab3b49..01c678b75 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs @@ -21,7 +21,7 @@ public static class ServerModeExtensions { public static async Task WaitTillServerModeReady(this RestAPIClient restApiClient) { - await WaitUntilHelper.WaitUntil(async () => + await Orchestration.Utilities.Helpers.WaitUntil(async () => { SystemStatus status = SystemStatus.Error; try diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index a128eeac5..92f868a8d 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -11,7 +12,9 @@ using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; using AWS.Deploy.CLI.IntegrationTests.Services; +using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; using Environment = System.Environment; @@ -26,6 +29,7 @@ public class WebAppNoDockerFileTests : IDisposable private bool _isDisposed; private string _stackName; private readonly TestAppManager _testAppManager; + private readonly string _customWorkspace; public WebAppNoDockerFileTests() { @@ -34,8 +38,24 @@ public WebAppNoDockerFileTests() serviceCollection.AddCustomServices(); serviceCollection.AddTestServices(); + foreach (var item in serviceCollection) + { + if (item.ServiceType == typeof(IEnvironmentVariableManager)) + { + serviceCollection.Remove(item); + break; + } + } + + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentVariableManager), typeof(TestEnvironmentVariableManager), ServiceLifetime.Singleton)); + var serviceProvider = serviceCollection.BuildServiceProvider(); + _customWorkspace = Path.Combine(Path.GetTempPath(), $"deploy-tool-workspace{Guid.NewGuid().ToString().Split('-').Last()}"); + var environmentVariableManager = serviceProvider.GetRequiredService(); + environmentVariableManager.SetEnvironmentVariable("AWS_DOTNET_DEPLOYTOOL_WORKSPACE", _customWorkspace); + Directory.CreateDirectory(_customWorkspace); + _app = serviceProvider.GetService(); Assert.NotNull(_app); @@ -82,6 +102,11 @@ public async Task DefaultConfigurations() // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + // Check the overridden workspace + Assert.True(File.Exists(Path.Combine(_customWorkspace, "CDKBootstrapTemplate.yaml"))); + Assert.True(Directory.Exists(Path.Combine(_customWorkspace, "temp"))); + Assert.True(Directory.Exists(Path.Combine(_customWorkspace, "Projects"))); + // list var listArgs = new[] { "list-deployments", "--diagnostics" }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; @@ -130,4 +155,22 @@ protected virtual void Dispose(bool disposing) Dispose(false); } } + + public class TestEnvironmentVariableManager : IEnvironmentVariableManager + { + public readonly Dictionary store = new Dictionary(); + + public string GetEnvironmentVariable(string variable) + { + return store.ContainsKey(variable) ? store[variable] : null; + } + + public void SetEnvironmentVariable(string variable, string value) + { + if (string.Equals(variable, "AWS_DOTNET_DEPLOYTOOL_WORKSPACE")) + store[variable] = value; + else + Environment.SetEnvironmentVariable(variable, value); + } + } } diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index 30d8f0bd5..40b4ad4e9 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -55,7 +55,7 @@ public ApplyPreviousSettingsTests() var optionSettingHandler = new OptionSettingHandler(validatorFactory); _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); - _orchestrator = new Orchestrator(null, null, null, null, null, null, null, null, null, null, null, null, null, _optionSettingHandler); + _orchestrator = new Orchestrator(null, null, null, null, null, null, null, null, null, null, null, null, null, _optionSettingHandler, null); } private async Task BuildRecommendationEngine(string testProjectName) diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKManagerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKManagerTests.cs index 3ca8c50c6..60cc57c14 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKManagerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKManagerTests.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Orchestration.Utilities; using Moq; using Xunit; @@ -15,6 +16,8 @@ public class CDKManagerTests private readonly Mock _mockNodeInitializer; private readonly Mock _mockInteractiveService; private readonly CDKManager _cdkManager; + private readonly Mock _environmentVariableManager; + private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata; private const string _workingDirectory = @"c:\fake\path"; public CDKManagerTests() @@ -22,7 +25,14 @@ public CDKManagerTests() _mockCdkManager = new Mock(); _mockNodeInitializer = new Mock(); _mockInteractiveService = new Mock(); - _cdkManager = new CDKManager(_mockCdkManager.Object, _mockNodeInitializer.Object, _mockInteractiveService.Object); + + _environmentVariableManager = new Mock(); + _environmentVariableManager + .Setup(x => x.GetEnvironmentVariable(It.IsAny())) + .Returns(() => null); + + _deployToolWorkspaceMetadata = new DeployToolWorkspaceMetadata(new TestDirectoryManager(), _environmentVariableManager.Object); + _cdkManager = new CDKManager(_mockCdkManager.Object, _mockNodeInitializer.Object, _mockInteractiveService.Object, _deployToolWorkspaceMetadata); } [Theory] diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs index 502431d5f..1ccd5b27e 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs @@ -22,6 +22,7 @@ public class CDKProjectHandlerTests private readonly IOptionSettingHandler _optionSettingHandler; private readonly Mock _awsResourceQueryer; private readonly Mock _serviceProvider; + private readonly Mock _workspaceMetadata; public CDKProjectHandlerTests() { @@ -31,6 +32,7 @@ public CDKProjectHandlerTests() .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) .Returns(_awsResourceQueryer.Object); _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _workspaceMetadata = new Mock(); } [Fact] @@ -44,7 +46,7 @@ public async Task CheckCDKBootstrap_DoesNotExist() awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(null)); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -60,7 +62,7 @@ public async Task CheckCDKBootstrap_NoCFParameter() awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(new Stack { Parameters = new List() })); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -77,7 +79,7 @@ public async Task CheckCDKBootstrap_NoSSMParameter() new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -96,7 +98,7 @@ public async Task CheckCDKBootstrap_SSMParameterOld() awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("1")); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -115,7 +117,7 @@ public async Task CheckCDKBootstrap_SSMParameterNewer() awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("100")); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -134,7 +136,7 @@ public async Task CheckCDKBootstrap_SSMParameterSame() awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult(AWS.Deploy.Constants.CDK.CDKTemplateVersion.ToString())); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object); Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployToolWorkspaceTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployToolWorkspaceTests.cs new file mode 100644 index 000000000..a1ebee495 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployToolWorkspaceTests.cs @@ -0,0 +1,133 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using AWS.Deploy.Orchestration.Utilities; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class DeployToolWorkspaceTests + { + [Theory] + [InlineData("C:/Users/Bob")] + [InlineData("C:/Users/Bob/Alice")] + public void WithoutOverride_UserProfile_WithoutSpaces(string userProfile) + { + // ARRANGE + var directoryManager = new TestDirectoryManager(); + var environmentVariableManager = new TestEnvironmentVariableManager(); + + // ACT + var actualWorkspace = Helpers.GetDeployToolWorkspaceDirectoryRoot(userProfile, directoryManager, environmentVariableManager); + + // ASSERT + var expectedWorkspace = Path.Combine(userProfile, ".aws-dotnet-deploy"); + Assert.Equal(expectedWorkspace, actualWorkspace); + Assert.Null(environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE)); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TMP")); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TEMP")); + } + + [Theory] + [InlineData("C:/Users/Bob Mike")] + [InlineData("C:/My users/Bob/Alice")] + [InlineData("C:/Users/Bob Mike/Alice")] + public void WithoutOverride_UserProfile_WithSpaces_ThrowsException(string userProfile) + { + // ARRANGE + var directoryManager = new TestDirectoryManager(); + var environmentVariableManager = new TestEnvironmentVariableManager(); + + // ACT and ASSERT + Assert.Throws(() => Helpers.GetDeployToolWorkspaceDirectoryRoot(userProfile, directoryManager, environmentVariableManager)); + Assert.Null(environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE)); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TMP")); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TEMP")); + } + + [Theory] + [InlineData("C:/workspace/deploy-tool-workspace", "C:/Users/Bob")] + [InlineData("C:/workspace/deploy-tool-workspace", "C:/Users/Admin/Bob Alice")] + [InlineData("C:/aws-workspaces/deploy-tool-workspace", "C:/Users/Admin/Bob Alice")] + public void WithOverride_ValidWorkspace(string workspaceOverride, string userProfile) + { + // ARRANGE + var directoryManager = new TestDirectoryManager(); + var environmentVariableManager = new TestEnvironmentVariableManager(); + environmentVariableManager.store["AWS_DOTNET_DEPLOYTOOL_WORKSPACE"] = workspaceOverride; + directoryManager.CreateDirectory(workspaceOverride); + + // ACT + var actualWorkspace = Helpers.GetDeployToolWorkspaceDirectoryRoot(userProfile, directoryManager, environmentVariableManager); + + // ASSERT + var expectedWorkspace = workspaceOverride; + var expectedTempDir = Path.Combine(workspaceOverride, "temp"); + Assert.True(directoryManager.Exists(expectedWorkspace)); + Assert.True(directoryManager.Exists(expectedTempDir)); + Assert.Equal(expectedWorkspace, actualWorkspace); + Assert.Equal(expectedWorkspace, environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE)); + Assert.Equal(expectedTempDir, environmentVariableManager.GetEnvironmentVariable("TEMP")); + Assert.Equal(expectedTempDir, environmentVariableManager.GetEnvironmentVariable("TMP")); + } + + [Theory] + [InlineData("C:/workspace/deploy-tool-workspace", "C:/Users/Bob")] + [InlineData("C:/workspace/deploy-tool-workspace", "C:/Users/Admin/Bob Alice")] + [InlineData("C:/aws-workspaces/deploy-tool-workspace", "C:/Users/Admin/Bob Alice")] + public void WithOverride_DirectoryDoesNotExist_ThrowsException(string workspaceOverride, string userProfile) + { + // ARRANGE + var directoryManager = new TestDirectoryManager(); + var environmentVariableManager = new TestEnvironmentVariableManager(); + environmentVariableManager.store[Constants.CLI.WORKSPACE_ENV_VARIABLE] = workspaceOverride; + + // ACT and ASSERT + Assert.Throws(() => Helpers.GetDeployToolWorkspaceDirectoryRoot(userProfile, directoryManager, environmentVariableManager)); + Assert.False(directoryManager.Exists(workspaceOverride)); + Assert.Equal(workspaceOverride, environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE)); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TMP")); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TEMP")); + } + + [Theory] + [InlineData("C:/workspace/deploy tool workspace", "C:/Users/Bob")] + [InlineData("C:/workspace/deploy tool workspace", "C:/Users/Admin/Bob Alice")] + [InlineData("C:/aws workspaces/deploy-tool-workspace", "C:/Users/Admin/Bob Alice")] + public void WithOverride_DirectoryContainsSpaces_ThrowsException(string workspaceOverride, string userProfile) + { + // ARRANGE + var directoryManager = new TestDirectoryManager(); + var environmentVariableManager = new TestEnvironmentVariableManager(); + environmentVariableManager.store["AWS_DOTNET_DEPLOYTOOL_WORKSPACE"] = workspaceOverride; + directoryManager.CreateDirectory(workspaceOverride); + + // ACT and ASSERT + Assert.Throws(() => Helpers.GetDeployToolWorkspaceDirectoryRoot(userProfile, directoryManager, environmentVariableManager)); + Assert.True(directoryManager.Exists(workspaceOverride)); + Assert.False(directoryManager.Exists(Path.Combine(workspaceOverride, "temp"))); + Assert.Equal(workspaceOverride, environmentVariableManager.GetEnvironmentVariable(Constants.CLI.WORKSPACE_ENV_VARIABLE)); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TMP")); + Assert.Null(environmentVariableManager.GetEnvironmentVariable("TEMP")); + } + } + + public class TestEnvironmentVariableManager : IEnvironmentVariableManager + { + public readonly Dictionary store = new(); + + public string GetEnvironmentVariable(string variable) + { + return store.ContainsKey(variable) ? store[variable] : null; + } + + public void SetEnvironmentVariable(string variable, string value) + { + store[variable] = value; + } + } +} From aa3a247c3b25d83474e5e0ba9ae9429e59571694 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Sun, 19 Jun 2022 21:48:27 -0400 Subject: [PATCH 10/18] build: add readme to nuget packages --- src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj | 2 ++ .../AWS.Deploy.Recipes.CDK.Common.csproj | 2 ++ src/AWS.Deploy.Recipes.CDK.Common/README.md | 1 + 3 files changed, 5 insertions(+) create mode 100644 src/AWS.Deploy.Recipes.CDK.Common/README.md diff --git a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj index 6ef7a934c..11455a27e 100644 --- a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj +++ b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj @@ -17,6 +17,7 @@ true $(NoWarn);1570;1591;ASP0000 Major + README.md @@ -41,6 +42,7 @@ + diff --git a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj index 1bf3dd73d..edc7081f7 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj +++ b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj @@ -9,6 +9,7 @@ true icon.png https://github.com/aws/aws-dotnet-deploy + README.md @@ -24,6 +25,7 @@ + diff --git a/src/AWS.Deploy.Recipes.CDK.Common/README.md b/src/AWS.Deploy.Recipes.CDK.Common/README.md new file mode 100644 index 000000000..9f22a265e --- /dev/null +++ b/src/AWS.Deploy.Recipes.CDK.Common/README.md @@ -0,0 +1 @@ +The `AWS.Deploy.Recipes.CDK.Common` package is used by the .NET deployment tools `AWS.Deploy.Tools` package. \ No newline at end of file From e286873acb00807f96a04be9a5d6db313b30b758 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Mon, 20 Jun 2022 12:02:40 -0400 Subject: [PATCH 11/18] feat: change security groups setting in fargate to a list instead of string --- .../Generated/Configurations/Configuration.cs | 6 +++--- .../AspNetAppEcsFargate/Generated/Recipe.cs | 4 ++-- .../ASP.NETAppECSFargate.recipe | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs index 9d105a813..f15d8ebe6 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/Configuration.cs @@ -39,9 +39,9 @@ public partial class Configuration public VpcConfiguration Vpc { get; set; } /// - /// Comma-delimited list of security groups assigned to the ECS service. + /// List of security groups assigned to the ECS service. /// - public string AdditionalECSServiceSecurityGroups { get; set; } + public SortedSet AdditionalECSServiceSecurityGroups { get; set; } = new SortedSet(); /// /// The amount of CPU to allocate to the Fargate task @@ -77,7 +77,7 @@ public Configuration( string ecsServiceName, ECSClusterConfiguration ecsCluster, VpcConfiguration vpc, - string additionalECSServiceSecurityGroups, + SortedSet additionalECSServiceSecurityGroups, LoadBalancerConfiguration loadBalancer, AutoScalingConfiguration autoScaling, Dictionary ecsEnvironmentVariables diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs index 5de42030a..c075b12c6 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs @@ -169,10 +169,10 @@ private void ConfigureECSClusterAndService(IRecipeProps recipeCon EcsServiceSecurityGroups = new List(); EcsServiceSecurityGroups.Add(WebAccessSecurityGroup); - if (!string.IsNullOrEmpty(settings.AdditionalECSServiceSecurityGroups)) + if (settings.AdditionalECSServiceSecurityGroups.Any()) { var count = 1; - foreach (var securityGroupId in settings.AdditionalECSServiceSecurityGroups.Split(',')) + foreach (var securityGroupId in settings.AdditionalECSServiceSecurityGroups) { EcsServiceSecurityGroups.Add(SecurityGroup.FromSecurityGroupId(this, $"AdditionalGroup-{count++}", securityGroupId.Trim(), new SecurityGroupImportOptions { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index f3f51d70c..36a47fedb 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -346,11 +346,21 @@ "Id": "AdditionalECSServiceSecurityGroups", "Name": "ECS Service Security Groups", "Category": "Permissions", - "Description": "A comma-delimited list of EC2 security groups to assign to the ECS service. This is commonly used to provide access to Amazon RDS databases running in their own security groups.", - "Type": "String", - "DefaultValue": "", + "Description": "A list of EC2 security groups to assign to the ECS service. This is commonly used to provide access to Amazon RDS databases running in their own security groups.", + "Type": "List", + "TypeHint": "ExistingSecurityGroups", "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^sg-([0-9a-f]{8}|[0-9a-f]{17})$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid Security Group ID. The Security Group ID must start with the \"sg-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example sg-abc88de9 is a valid Security Group ID." + } + } + ] }, { "Id": "TaskCpu", From 2d6b5f3014877fbb8e2764198fce4107c63a651b Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Mon, 6 Jun 2022 09:30:18 -0400 Subject: [PATCH 12/18] feat: Add support for returning tables of options via GetConfigSettingResources in server mode. This allows wrapping tools to display richer grids/tables when presenting the user a list of options to select from. The subnets and security groups for App Runner's VPC Connector and Beanstalk Platform are supported initially. --- .../TypeHints/BeanstalkApplicationCommand.cs | 10 +++- .../TypeHints/BeanstalkEnvironmentCommand.cs | 10 +++- .../TypeHints/DockerBuildArgsCommand.cs | 2 +- .../DockerExecutionDirectoryCommand.cs | 2 +- .../DotnetBeanstalkPlatformArnCommand.cs | 37 ++++++++++---- .../TypeHints/DotnetPublishArgsCommand.cs | 2 +- .../DotnetPublishBuildConfigurationCommand.cs | 3 +- .../DotnetPublishSelfContainedBuildCommand.cs | 2 +- .../TypeHints/DynamoDBTableCommand.cs | 10 +++- .../Commands/TypeHints/EC2KeyPairCommand.cs | 10 +++- .../TypeHints/ECRRepositoryCommand.cs | 10 +++- .../Commands/TypeHints/ECSClusterCommand.cs | 10 +++- .../ExistingApplicationLoadBalancerCommand.cs | 10 +++- .../ExistingSecurityGroupsCommand.cs | 45 ++++++++++++---- .../TypeHints/ExistingSubnetsCommand.cs | 36 ++++++++++--- .../Commands/TypeHints/ExistingVpcCommand.cs | 9 +++- .../TypeHints/ExistingVpcConnectorCommand.cs | 10 +++- .../Commands/TypeHints/FilePathCommand.cs | 2 +- .../Commands/TypeHints/IAMRoleCommand.cs | 10 +++- .../Commands/TypeHints/InstanceTypeCommand.cs | 17 ++++--- .../Commands/TypeHints/S3BucketNameCommand.cs | 10 +++- .../Commands/TypeHints/SNSTopicArnsCommand.cs | 12 +++-- .../Commands/TypeHints/SQSQueueUrlCommand.cs | 12 +++-- .../TypeHints/TypeHintCommandFactory.cs | 3 +- .../Commands/TypeHints/VPCConnectorCommand.cs | 10 +++- .../Commands/TypeHints/VpcCommand.cs | 8 ++- .../Controllers/DeploymentController.cs | 8 +-- .../Models/GetConfigSettingResourcesOutput.cs | 17 +++++++ .../Models/TypeHintResourceColumn.cs | 21 ++++++++ .../Models/TypeHintResourceSummary.cs | 24 ++++++++- .../TypeHintData/TypeHintResource.cs | 22 +++++++- .../TypeHintData/TypeHintResourceColumn.cs | 21 ++++++++ .../TypeHintData/TypeHintResourceTable.cs | 26 ++++++++++ src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 29 +++++++++++ .../ServerMode/GetApplyOptionSettings.cs | 51 +++++++++++++++++++ .../ExistingSecurityGroubsCommandTest.cs | 6 +-- .../ExistingSubnetsCommandTest.cs | 6 +-- .../ExistingVpcCommandTest.cs | 6 +-- .../VPCConnectorCommandTest.cs | 6 +-- .../AWS.Deploy.CLI.UnitTests/TypeHintTests.cs | 40 +++++++-------- 40 files changed, 473 insertions(+), 112 deletions(-) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceColumn.cs create mode 100644 src/AWS.Deploy.Common/TypeHintData/TypeHintResourceColumn.cs create mode 100644 src/AWS.Deploy.Common/TypeHintData/TypeHintResourceTable.cs diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs index 626eff895..07f626d2e 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs @@ -32,10 +32,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfElasticBeanstalkApplications(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var applications = await GetData(); - return applications.Select(x => new TypeHintResource(x.ApplicationName, x.ApplicationName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = applications.Select(x => new TypeHintResource(x.ApplicationName, x.ApplicationName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs index bd5331725..5bace059e 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs @@ -35,10 +35,16 @@ private async Task> GetData(Recommendation recommen return await _awsResourceQueryer.ListOfElasticBeanstalkEnvironments(applicationName); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var environments = await GetData(recommendation, optionSetting); - return environments.Select(x => new TypeHintResource(x.EnvironmentName, x.EnvironmentName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = environments.Select(x => new TypeHintResource(x.EnvironmentName, x.EnvironmentName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs index 8fd17eff2..ac2159593 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs @@ -21,7 +21,7 @@ public DockerBuildArgsCommand(IConsoleUtilities consoleUtilities, IOptionSetting _optionSettingHandler = optionSettingHandler; } - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index 2fc78af78..ae5c64347 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -25,7 +25,7 @@ public DockerExecutionDirectoryCommand(IConsoleUtilities consoleUtilities, IDire _optionSettingHandler = optionSettingHandler; } - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs index b86676589..74574f655 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs @@ -33,28 +33,47 @@ private async Task> GetData() return await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var platformArns = await GetData(); - return platformArns.Select(x => new TypeHintResource(x.PlatformArn, $"{x.PlatformBranchName} v{x.PlatformVersion}")).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Columns = new List() + { + new TypeHintResourceColumn("Platform Branch"), + new TypeHintResourceColumn("Platform Version") + } + }; + + foreach (var platformArn in platformArns) + { + var row = new TypeHintResource(platformArn.PlatformArn, $"{platformArn.PlatformBranchName} v{platformArn.PlatformVersion}"); + row.ColumnValues.Add(platformArn.PlatformBranchName); + row.ColumnValues.Add(platformArn.PlatformVersion); + + resourceTable.Rows.Add(row); + } + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { var currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); - var platformArns = await GetData(); + var resourceTable = await GetResources(recommendation, optionSetting); - var userInputConfiguration = new UserInputConfiguration( - idSelector: platform => platform.PlatformArn, - displaySelector: platform => $"{platform.PlatformBranchName} v{platform.PlatformVersion}", - defaultSelector: platform => platform.PlatformArn.Equals(currentValue)) + var userInputConfiguration = new UserInputConfiguration( + idSelector: platform => platform.SystemName, + displaySelector: platform => platform.DisplayName, + defaultSelector: platform => platform.SystemName.Equals(currentValue)) { CreateNew = false }; - var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(platformArns, "Select the Platform to use:", userInputConfiguration); + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(resourceTable.Rows, "Select the Platform to use:", userInputConfiguration); - return userResponse.SelectedOption?.PlatformArn!; + return userResponse.SelectedOption?.SystemName!; } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index d9904b1ae..d2a70e040 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -21,7 +21,7 @@ public DotnetPublishArgsCommand(IConsoleUtilities consoleUtilities, IOptionSetti _optionSettingHandler = optionSettingHandler; } - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs index 8923fe730..a89b246e9 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs @@ -20,8 +20,7 @@ public DotnetPublishBuildConfigurationCommand(IConsoleUtilities consoleUtilities _optionSettingHandler = optionSettingHandler; } - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); - + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { var settingValue = diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs index bfa30cb3a..6600838de 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs @@ -20,7 +20,7 @@ public DotnetPublishSelfContainedBuildCommand(IConsoleUtilities consoleUtilities _optionSettingHandler = optionSettingHandler; } - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs index 6e0f8e795..be52e6768 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs @@ -31,10 +31,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfDyanmoDBTables(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var tables = await GetData(); - return tables.Select(tableName => new TypeHintResource(tableName, tableName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = tables.Select(tableName => new TypeHintResource(tableName, tableName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs index e69812e46..fd814f808 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs @@ -36,10 +36,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfEC2KeyPairs(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var keyPairs = await GetData(); - return keyPairs.Select(x => new TypeHintResource(x.KeyName, x.KeyName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = keyPairs.Select(x => new TypeHintResource(x.KeyName, x.KeyName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs index 1b371dee7..3b21f2507 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs @@ -53,10 +53,16 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt return userResponse.SelectedOption?.RepositoryName ?? userResponse.NewName ?? throw new UserPromptForNameReturnedNullException(DeployToolErrorCode.ECRRepositoryPromptForNameReturnedNull, "The user response for an ECR Repository was null"); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var repositories = await GetData(); - return repositories.Select(x => new TypeHintResource(x.RepositoryName, x.RepositoryName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = repositories.Select(x => new TypeHintResource(x.RepositoryName, x.RepositoryName)).ToList() + }; + + return resourceTable; } private async Task> GetData() diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs index ede9338db..86b22c4d9 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs @@ -34,10 +34,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfECSClusters(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var clusters = await GetData(); - return clusters.Select(x => new TypeHintResource(x.ClusterArn, x.ClusterName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = clusters.Select(x => new TypeHintResource(x.ClusterArn, x.ClusterName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs index 97a77f051..12bd304d9 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs @@ -35,10 +35,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfLoadBalancers(LoadBalancerTypeEnum.Application); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var loadBalancers = await GetData(); - return loadBalancers.Select(x => new TypeHintResource(x.LoadBalancerArn, x.LoadBalancerName)).ToList(); + + var resourceTable = new TypeHintResourceTable() + { + Rows = loadBalancers.Select(x => new TypeHintResource(x.LoadBalancerArn, x.LoadBalancerName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs index f29f70048..1dcd0f39f 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs @@ -9,7 +9,6 @@ using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; -using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { @@ -37,30 +36,54 @@ private async Task> GetData(Recommendation recommendation, O return await _awsResourceQueryer.DescribeSecurityGroups(vpcId); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var securityGroups = await GetData(recommendation, optionSetting); - return securityGroups.Select(securityGroup => new TypeHintResource(securityGroup.GroupId, securityGroup.GroupId)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Columns = new List() + { + new TypeHintResourceColumn("Group Name"), + new TypeHintResourceColumn("Group Id"), + new TypeHintResourceColumn("VPC Id") + } + }; + + foreach (var securityGroup in securityGroups.OrderBy(securityGroup => securityGroup.VpcId)) + { + var row = new TypeHintResource(securityGroup.GroupId, securityGroup.GroupId); + row.ColumnValues.Add(securityGroup.GroupName); + row.ColumnValues.Add(securityGroup.GroupId); + row.ColumnValues.Add(securityGroup.VpcId); + + resourceTable.Rows.Add(row); + } + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { - var availableSecurityGroups = (await GetData(recommendation, optionSetting)).OrderBy(x => x.VpcId).ToList(); + var resourceTable = await GetResources(recommendation, optionSetting); + var groupNamePadding = 0; - availableSecurityGroups.ForEach(x => + resourceTable.Rows.ForEach(row => { - if (x.GroupName.Length > groupNamePadding) - groupNamePadding = x.GroupName.Length; + if (row.ColumnValues[0].Length > groupNamePadding) + groupNamePadding = row.ColumnValues[0].Length; }); - var userInputConfigurationSecurityGroups = new UserInputConfiguration( - idSelector: securityGroup => securityGroup.GroupId, - displaySelector: securityGroup => $"{securityGroup.GroupName.PadRight(groupNamePadding)} | {securityGroup.GroupId.PadRight(20)} | {securityGroup.VpcId}", + + var userInputConfigurationSecurityGroups = new UserInputConfiguration( + idSelector: securityGroup => securityGroup.SystemName, + displaySelector: securityGroup => $"{securityGroup.ColumnValues[0].PadRight(groupNamePadding)} | {securityGroup.ColumnValues[1].PadRight(20)} | {securityGroup.ColumnValues[2]}", defaultSelector: securityGroup => false) { CanBeEmpty = true, CreateNew = false }; - return _consoleUtilities.AskUserForList(userInputConfigurationSecurityGroups, availableSecurityGroups, optionSetting, recommendation); + + return _consoleUtilities.AskUserForList(userInputConfigurationSecurityGroups, resourceTable.Rows, optionSetting, recommendation); } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs index cbc9f0fe0..b93fb3142 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs @@ -9,7 +9,6 @@ using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; -using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { @@ -37,25 +36,48 @@ private async Task> GetData(Recommendation recommendation, OptionSe return await _awsResourceQueryer.DescribeSubnets(vpcId); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var subnets = await GetData(recommendation, optionSetting); - return subnets.Select(subnet => new TypeHintResource(subnet.SubnetId, subnet.SubnetId)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Columns = new List() + { + new TypeHintResourceColumn("Subnet Id"), + new TypeHintResourceColumn("VPC Id"), + new TypeHintResourceColumn("Availability Zone") + } + }; + + foreach (var subnet in subnets.OrderBy(subnet => subnet.VpcId)) + { + var row = new TypeHintResource(subnet.SubnetId, subnet.SubnetId); + row.ColumnValues.Add(subnet.SubnetId); + row.ColumnValues.Add(subnet.VpcId); + row.ColumnValues.Add(subnet.AvailabilityZone); + + resourceTable.Rows.Add(row); + } + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { var availableSubnets = (await GetData(recommendation, optionSetting)).OrderBy(x => x.VpcId).ToList(); - var userInputConfigurationSubnets = new UserInputConfiguration( - idSelector: subnet => subnet.SubnetId, - displaySelector: subnet => $"{subnet.SubnetId.PadRight(24)} | {subnet.VpcId.PadRight(21)} | {subnet.AvailabilityZone}", + var resourceTable = await GetResources(recommendation, optionSetting); + + var userInputConfigurationSubnets = new UserInputConfiguration( + idSelector: subnet => subnet.SystemName, + displaySelector: subnet => $"{subnet.ColumnValues[0].PadRight(24)} | {subnet.ColumnValues[1].PadRight(21)} | {subnet.ColumnValues[2]}", defaultSelector: subnet => false) { CanBeEmpty = true, CreateNew = false }; - return _consoleUtilities.AskUserForList(userInputConfigurationSubnets, availableSubnets, optionSetting, recommendation); + return _consoleUtilities.AskUserForList(userInputConfigurationSubnets, resourceTable.Rows, optionSetting, recommendation); } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs index 088aee00d..513973849 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs @@ -31,10 +31,13 @@ private async Task> GetData() return await _awsResourceQueryer.GetListOfVpcs(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var vpcs = await GetData(); - return vpcs.ToDictionary(x => x.VpcId, x => + + 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 = @@ -49,6 +52,8 @@ private async Task> GetData() return $"{x.VpcId}{namePart}{isDefaultPart}"; }).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs index 670e52401..c5eeae520 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs @@ -31,10 +31,16 @@ private async Task> GetData() return await _awsResourceQueryer.DescribeAppRunnerVpcConnectors(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var vpcConnectors = await GetData(); - return vpcConnectors.Select(vpcConnector => new TypeHintResource(vpcConnector.VpcConnectorArn, vpcConnector.VpcConnectorName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = vpcConnectors.Select(vpcConnector => new TypeHintResource(vpcConnector.VpcConnectorArn, vpcConnector.VpcConnectorName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs index b1cc21920..f3db51123 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs @@ -32,7 +32,7 @@ public FilePathCommand(IConsoleUtilities consoleUtilities , IOptionSettingHandle /// Not implemented, specific files are not suggested to the user /// /// Empty list - public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + public Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable()); /// /// Prompts the user to enter a path to a file diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs index 6bbf2ea11..7dce64662 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs @@ -36,10 +36,16 @@ private async Task> GetData(OptionSettingItem optionSetting) return await _awsResourceQueryer.ListOfIAMRoles(typeHintData?.ServicePrincipal); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var existingRoles = await GetData(optionSetting); - return existingRoles.Select(x => new TypeHintResource(x.Arn, x.RoleName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = existingRoles.Select(x => new TypeHintResource(x.Arn, x.RoleName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs index 87d67cd67..41dc22687 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs @@ -31,14 +31,19 @@ public InstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtili return await _awsResourceQueryer.ListOfAvailableInstanceTypes(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { - var instanceType = await GetData(); + var instanceTypes = await GetData(); + var resourceTable = new TypeHintResourceTable(); + + if (instanceTypes != null) + { + resourceTable.Rows = instanceTypes.OrderBy(x => x.InstanceType.Value) + .Select(x => new TypeHintResource(x.InstanceType.Value, x.InstanceType.Value)) + .ToList(); + } - return instanceType? - .OrderBy(x => x.InstanceType.Value) - .Select(x => new TypeHintResource(x.InstanceType.Value, x.InstanceType.Value)) - .ToList(); + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs index b731ea0e1..2d2e3bafe 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs @@ -32,10 +32,16 @@ private async Task> GetData() return await _awsResourceQueryer.ListOfS3Buckets(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var buckets = await GetData(); - return buckets.Select(bucket => new TypeHintResource(bucket.BucketName, bucket.BucketName)).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = buckets.Select(bucket => new TypeHintResource(bucket.BucketName, bucket.BucketName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs index d449bea7c..109ee6dc4 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs @@ -26,10 +26,16 @@ public SNSTopicArnsCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtili _optionSettingHandler = optionSettingHandler; } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var topicArns = await _awsResourceQueryer.ListOfSNSTopicArns(); - return topicArns.Select(topicArn => new TypeHintResource(topicArn, topicArn.Substring(topicArn.LastIndexOf(':') + 1))).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = topicArns.Select(topicArn => new TypeHintResource(topicArn, topicArn.Substring(topicArn.LastIndexOf(':') + 1))).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) @@ -38,7 +44,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); var typeHintData = optionSetting.GetTypeHintData(); var currentValueStr = currentValue.ToString() ?? string.Empty; - var topicArns = await GetResources(recommendation, optionSetting); + var topicArns = (await GetResources(recommendation, optionSetting)).Rows; var topicNames = topicArns?.Select(topic => topic.DisplayName).ToList() ?? new List(); var currentName = string.Empty; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs index 56cf81495..0712d1403 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs @@ -26,10 +26,16 @@ public SQSQueueUrlCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilit _optionSettingHandler = optionSettingHandler; } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var queueUrls = await _awsResourceQueryer.ListOfSQSQueuesUrls(); - return queueUrls.Select(queueUrl => new TypeHintResource(queueUrl, queueUrl.Substring(queueUrl.LastIndexOf('/') + 1))).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Rows = queueUrls.Select(queueUrl => new TypeHintResource(queueUrl, queueUrl.Substring(queueUrl.LastIndexOf('/') + 1))).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) @@ -38,7 +44,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); var typeHintData = optionSetting.GetTypeHintData(); var currentValueStr = currentValue.ToString() ?? string.Empty; - var queueUrls = await GetResources(recommendation, optionSetting); + var queueUrls = (await GetResources(recommendation, optionSetting)).Rows; var queueNames = queueUrls?.Select(queue => queue.DisplayName).ToList() ?? new List(); var currentName = string.Empty; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index 8e3948ad4..8ebf28ad7 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -9,7 +9,6 @@ using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; -using AWS.Deploy.Orchestration.Data; using Microsoft.Extensions.DependencyInjection; namespace AWS.Deploy.CLI.Commands.TypeHints @@ -19,7 +18,7 @@ namespace AWS.Deploy.CLI.Commands.TypeHints /// public interface ITypeHintCommand { - Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting); + Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting); Task Execute(Recommendation recommendation, OptionSettingItem optionSetting); } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs index 2f00c36f2..64024da77 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs @@ -35,10 +35,16 @@ private async Task> GetData() return await _awsResourceQueryer.DescribeAppRunnerVpcConnectors(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var vpcConnectors = await GetData(); - return vpcConnectors.Select(vpcConnector => new TypeHintResource(vpcConnector.VpcConnectorArn, vpcConnector.VpcConnectorName)).ToList(); + + var resourceTable = new TypeHintResourceTable() + { + Rows = vpcConnectors.Select(vpcConnector => new TypeHintResource(vpcConnector.VpcConnectorArn, vpcConnector.VpcConnectorName)).ToList() + }; + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs index 29ad3d201..6f023ee63 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs @@ -33,10 +33,12 @@ private async Task> GetData() return await _awsResourceQueryer.GetListOfVpcs(); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var vpcs = await GetData(); - return vpcs.ToDictionary(x => x.VpcId, x => { + 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) @@ -50,6 +52,8 @@ private async Task> GetData() return $"{x.VpcId}{namePart}{isDefaultPart}"; }).Select(x => new TypeHintResource(x.Key, x.Value)).ToList(); + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index aa1d5fc39..a26a01b07 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -299,14 +299,14 @@ public async Task GetConfigSettingResources(string sessionId, str if (configSetting.TypeHint.HasValue && typeHintCommandFactory.GetCommand(configSetting.TypeHint.Value) is var typeHintCommand && typeHintCommand != null) { var output = new GetConfigSettingResourcesOutput(); - var resources = await typeHintCommand.GetResources(state.SelectedRecommendation, configSetting); + var resourceTable = await typeHintCommand.GetResources(state.SelectedRecommendation, configSetting); - if (resources == null) + if (resourceTable == null) { return NotFound("The Config Setting type hint is not recognized."); } - - output.Resources = resources.Select(x => new TypeHintResourceSummary(x.SystemName, x.DisplayName)).ToList(); + output.Columns = resourceTable.Columns?.Select(column => new Models.TypeHintResourceColumn(column.DisplayName)).ToList(); + output.Resources = resourceTable.Rows?.Select(resource => new TypeHintResourceSummary(resource.SystemName, resource.DisplayName, resource.ColumnValues)).ToList(); return Ok(output); } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/GetConfigSettingResourcesOutput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/GetConfigSettingResourcesOutput.cs index 102bfe14e..71671e657 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/GetConfigSettingResourcesOutput.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/GetConfigSettingResourcesOutput.cs @@ -5,8 +5,25 @@ namespace AWS.Deploy.CLI.ServerMode.Models { + /// + /// Represents a list or table of options, generally used when selecting from + /// a list of existing AWS resources to set an OptionSettingItem + /// public class GetConfigSettingResourcesOutput { + /// + /// Columns that should appear above the list of resources when + /// presenting the user a table to select from + /// + /// + /// If this is null or empty, it implies that there is only a single column. + /// This may be better suited for a simple dropdown as opposed to a table or modal select. + /// + public List? Columns { get; set; } + + /// + /// List of resources that the user could select from + /// public List? Resources { get; set; } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceColumn.cs b/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceColumn.cs new file mode 100644 index 000000000..5f65bae09 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceColumn.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + /// + /// Represents a column for a list/grid of rows + /// + public class TypeHintResourceColumn + { + /// + /// Name of the column to be displayed to users + /// + public string DisplayName { get; set; } + + public TypeHintResourceColumn(string displayName) + { + DisplayName = displayName; + } + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceSummary.cs index a947161ae..0582c0345 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/TypeHintResourceSummary.cs @@ -1,17 +1,39 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Generic; + namespace AWS.Deploy.CLI.ServerMode.Models { + /// + /// Represents a single type hint option, generally used when selecting from + /// a list of existing AWS resources to set an OptionSettingItem + /// public class TypeHintResourceSummary { + /// + /// Resource Id, used when saving a selected resource to an OptionSettingItem + /// public string SystemName { get; set; } + + /// + /// Resource name, used for display + /// public string DisplayName { get; set; } - public TypeHintResourceSummary(string systemName, string displayName) + /// + /// Additional data about the resource, which may be used when displaying a table + /// or grid of options for the user to select from. The indices into this list should + /// match the column indicies of a list of + /// + public List ColumnDisplayValues { get; set; } + + public TypeHintResourceSummary(string systemName, string displayName, List columnDisplayValues) { SystemName = systemName; DisplayName = displayName; + ColumnDisplayValues = new List(columnDisplayValues); + } } } diff --git a/src/AWS.Deploy.Common/TypeHintData/TypeHintResource.cs b/src/AWS.Deploy.Common/TypeHintData/TypeHintResource.cs index 40c0c3bee..499c36ec3 100644 --- a/src/AWS.Deploy.Common/TypeHintData/TypeHintResource.cs +++ b/src/AWS.Deploy.Common/TypeHintData/TypeHintResource.cs @@ -1,13 +1,33 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Generic; + namespace AWS.Deploy.Common.TypeHintData { + /// + /// Represents a single AWS resource, generally used when selecting from + /// a list of existing resources to set an OptionSettingItem + /// public class TypeHintResource { + /// + /// Resource id, used when saving a selected resource to an OptionSettingItem + /// public string SystemName { get; set; } + + /// + /// Resource name, used for display + /// public string DisplayName { get; set; } + /// + /// Additional data about the resource, which may be used when displaying a table + /// or grid of options for the user to select from. The indices into this list should + /// match the column indicies of + /// + public List ColumnValues { get; set; } = new List(); + public TypeHintResource(string systemName, string displayName) { SystemName = systemName; diff --git a/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceColumn.cs b/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceColumn.cs new file mode 100644 index 000000000..7c65e1683 --- /dev/null +++ b/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceColumn.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Common.TypeHintData +{ + /// + /// Represents the column for a list/grid of rows + /// + public class TypeHintResourceColumn + { + /// + /// Name of the column to be displayed to users + /// + public string DisplayName { get; set; } + + public TypeHintResourceColumn(string displayName) + { + DisplayName = displayName; + } + } +} diff --git a/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceTable.cs b/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceTable.cs new file mode 100644 index 000000000..10345d12c --- /dev/null +++ b/src/AWS.Deploy.Common/TypeHintData/TypeHintResourceTable.cs @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.Common.TypeHintData +{ + /// + /// Represents a list or table of existing AWS resources to allow selecting from + /// a list of existing resources when setting an OptionSettingItem + /// + public class TypeHintResourceTable + { + /// + /// Columns that should appear above the list of resources when presenting the + /// user a table or grid to select from + /// + /// If this is null or empty, it implies that there is only a single column + public List? Columns { get; set; } + + /// + /// List of AWS resources that the user could select from + /// + public List Rows { get; set; } = new List(); + } +} diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index a24d278aa..6184238ca 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1940,9 +1940,17 @@ public partial class GetCompatibilityOutput } + /// Represents a list or table of options, generally used when selecting from + /// a list of existing AWS resources to set an OptionSettingItem [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] public partial class GetConfigSettingResourcesOutput { + /// Columns that should appear above the list of resources when + /// presenting the user a table to select from + [Newtonsoft.Json.JsonProperty("columns", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection Columns { get; set; } + + /// List of resources that the user could select from [Newtonsoft.Json.JsonProperty("resources", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.ICollection Resources { get; set; } @@ -2268,15 +2276,36 @@ public enum SystemStatus } + /// Represents a column for a list/grid of AWS.Deploy.CLI.ServerMode.Models.TypeHintResourceSummary rows + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] + public partial class TypeHintResourceColumn + { + /// Name of the column to be displayed to users + [Newtonsoft.Json.JsonProperty("displayName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string DisplayName { get; set; } + + + } + + /// Represents a single type hint option, generally used when selecting from + /// a list of existing AWS resources to set an OptionSettingItem [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] public partial class TypeHintResourceSummary { + /// Resource Id, used when saving a selected resource to an OptionSettingItem [Newtonsoft.Json.JsonProperty("systemName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string SystemName { get; set; } + /// Resource name, used for display [Newtonsoft.Json.JsonProperty("displayName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string DisplayName { get; set; } + /// Additional data about the resource, which may be used when displaying a table + /// or grid of options for the user to select from. The indices into this list should + /// match the column indicies of a list of AWS.Deploy.CLI.ServerMode.Models.TypeHintResourceColumn + [Newtonsoft.Json.JsonProperty("columnDisplayValues", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection ColumnDisplayValues { get; set; } + } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index a10806c7a..c03f2f644 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -264,6 +264,57 @@ public async Task GetAppRunnerConfigSettings_TypeHintData() } } + /// + /// Tests that GetConfigSettingResourcesAsync for App Runner's + /// VPC Connector child settings return TypeHintResourceColumns + /// + [Fact] + public async Task GetConfigSettingResources_VpcConnectorOptions() + { + _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; + + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var portNumber = 4023; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); + + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var cancelSource = new CancellationTokenSource(); + + var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + try + { + var baseUrl = $"http://localhost:{portNumber}/"; + var restClient = new RestAPIClient(baseUrl, httpClient); + + await restClient.WaitTillServerModeReady(); + + var sessionId = await restClient.StartDeploymentSession(projectPath, _awsRegion); + + await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppAppRunner", _stackName); + + // Assert that the Subnets and SecurityGroups options are returning columns + var subnets = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.Subnets"); + Assert.Collection(subnets.Columns, + column => Assert.NotNull(column), // Subnet Id + column => Assert.NotNull(column), // VPC + column => Assert.NotNull(column)); // Availability Zone + + var securityGroups = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.SecurityGroups"); + Assert.Collection(securityGroups.Columns, + column => Assert.NotNull(column), // Name + column => Assert.NotNull(column), // Id + column => Assert.NotNull(column)); // VPC + + // This is using a real AWSResourceQueryer, + // so not asserting on the rows for these two options + } + finally + { + cancelSource.Cancel(); + _stackName = null; + } + } + public void Dispose() { Dispose(true); diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs index 25a8e6624..e11f16a19 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs @@ -74,9 +74,9 @@ public async Task GetResources() var resources = await command.GetResources(appRunnerRecommendation, securityGroupsOptionSetting); - Assert.Single(resources); - Assert.Equal("group1", resources[0].DisplayName); - Assert.Equal("group1", resources[0].SystemName); + Assert.Single(resources.Rows); + Assert.Equal("group1", resources.Rows[0].DisplayName); + Assert.Equal("group1", resources.Rows[0].SystemName); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs index 76136dff7..13d684ee4 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs @@ -74,9 +74,9 @@ public async Task GetResources() var resources = await command.GetResources(appRunnerRecommendation, subnetsOptionSetting); - Assert.Single(resources); - Assert.Equal("subnet1", resources[0].DisplayName); - Assert.Equal("subnet1", resources[0].SystemName); + Assert.Single(resources.Rows); + Assert.Equal("subnet1", resources.Rows[0].DisplayName); + Assert.Equal("subnet1", resources.Rows[0].SystemName); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs index cd89248ec..4e2b7038b 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs @@ -72,9 +72,9 @@ public async Task GetResources() var resources = await command.GetResources(beanstalkRecommendation, vpcOptionSetting); - Assert.Single(resources); - Assert.Equal("vpc1", resources[0].DisplayName); - Assert.Equal("vpc1", resources[0].SystemName); + Assert.Single(resources.Rows); + Assert.Equal("vpc1", resources.Rows[0].DisplayName); + Assert.Equal("vpc1", resources.Rows[0].SystemName); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs index 583c15187..b17c3fbc3 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs @@ -77,9 +77,9 @@ public async Task GetResources() var resources = await command.GetResources(appRunnerRecommendation, vpcConnectorOptionSetting); - Assert.Single(resources); - Assert.Equal("vpcConnectorName", resources[0].DisplayName); - Assert.Equal("arn:aws:apprunner:us-west-2:123456789010:vpcconnector/fakeVpcConnector", resources[0].SystemName); + Assert.Single(resources.Rows); + Assert.Equal("vpcConnectorName", resources.Rows[0].DisplayName); + Assert.Equal("arn:aws:apprunner:us-west-2:123456789010:vpcconnector/fakeVpcConnector", resources.Rows[0].SystemName); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs index 142517bcb..2a2073c43 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs @@ -65,11 +65,11 @@ public async Task TestDynamoDBTableNameTypeHint() var typeHintCommand = new DynamoDBTableCommand(awsResourceQueryer, null, _optionSettingHandler); var resources = await typeHintCommand.GetResources(null, null); - Assert.Equal(2, resources.Count); - Assert.Equal("Table1", resources[0].DisplayName); - Assert.Equal("Table1", resources[0].SystemName); - Assert.Equal("Table2", resources[1].DisplayName); - Assert.Equal("Table2", resources[1].SystemName); + Assert.Equal(2, resources.Rows.Count); + Assert.Equal("Table1", resources.Rows[0].DisplayName); + Assert.Equal("Table1", resources.Rows[0].SystemName); + Assert.Equal("Table2", resources.Rows[1].DisplayName); + Assert.Equal("Table2", resources.Rows[1].SystemName); } [Fact] @@ -95,11 +95,11 @@ public async Task TestSQSQueueUrlTypeHint() var typeHintCommand = new SQSQueueUrlCommand(awsResourceQueryer, null, _optionSettingHandler); var resources = await typeHintCommand.GetResources(null, null); - Assert.Equal(2, resources.Count); - Assert.Equal("queue1", resources[0].DisplayName); - Assert.Equal("https://sqs.us-west-2.amazonaws.com/123412341234/queue1", resources[0].SystemName); - Assert.Equal("queue2", resources[1].DisplayName); - Assert.Equal("https://sqs.us-west-2.amazonaws.com/123412341234/queue2", resources[1].SystemName); + Assert.Equal(2, resources.Rows.Count); + Assert.Equal("queue1", resources.Rows[0].DisplayName); + Assert.Equal("https://sqs.us-west-2.amazonaws.com/123412341234/queue1", resources.Rows[0].SystemName); + Assert.Equal("queue2", resources.Rows[1].DisplayName); + Assert.Equal("https://sqs.us-west-2.amazonaws.com/123412341234/queue2", resources.Rows[1].SystemName); } [Fact] @@ -125,11 +125,11 @@ public async Task TestSNSTopicArnTypeHint() var typeHintCommand = new SNSTopicArnsCommand(awsResourceQueryer, null, _optionSettingHandler); var resources = await typeHintCommand.GetResources(null, null); - Assert.Equal(2, resources.Count); - Assert.Equal("Topic1", resources[0].DisplayName); - Assert.Equal("arn:aws:sns:us-west-2:123412341234:Topic1", resources[0].SystemName); - Assert.Equal("Topic2", resources[1].DisplayName); - Assert.Equal("arn:aws:sns:us-west-2:123412341234:Topic2", resources[1].SystemName); + Assert.Equal(2, resources.Rows.Count); + Assert.Equal("Topic1", resources.Rows[0].DisplayName); + Assert.Equal("arn:aws:sns:us-west-2:123412341234:Topic1", resources.Rows[0].SystemName); + Assert.Equal("Topic2", resources.Rows[1].DisplayName); + Assert.Equal("arn:aws:sns:us-west-2:123412341234:Topic2", resources.Rows[1].SystemName); } [Fact] @@ -147,11 +147,11 @@ public async Task TestS3BucketNameTypeHint() var typeHintCommand = new S3BucketNameCommand(awsResourceQueryer, null, _optionSettingHandler); var resources = await typeHintCommand.GetResources(null, null); - Assert.Equal(2, resources.Count); - Assert.Equal("Bucket1", resources[0].DisplayName); - Assert.Equal("Bucket1", resources[0].SystemName); - Assert.Equal("Bucket2", resources[1].DisplayName); - Assert.Equal("Bucket2", resources[1].SystemName); + Assert.Equal(2, resources.Rows.Count); + Assert.Equal("Bucket1", resources.Rows[0].DisplayName); + Assert.Equal("Bucket1", resources.Rows[0].SystemName); + Assert.Equal("Bucket2", resources.Rows[1].DisplayName); + Assert.Equal("Bucket2", resources.Rows[1].SystemName); } } } From 2d126108c2e0986b9dc3ff9bf73380c7bf802ca3 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Mon, 20 Jun 2022 12:45:22 -0400 Subject: [PATCH 13/18] feat: add troubleshooting guide and cut a ticket links on exception --- mkdocs.yml | 2 +- .../troubleshooting-guide/{other-issues.md => index.md} | 2 +- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) rename site/content/troubleshooting-guide/{other-issues.md => index.md} (99%) diff --git a/mkdocs.yml b/mkdocs.yml index 1a8a8dfb5..4ae6bd1be 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,9 +52,9 @@ nav: # - Listing Deployments: docs/tutorials/list-deployments.md # - Deleting Deployment: docs/tutorials/delete-deployment.md - Troubleshooting Guide: + - troubleshooting-guide/index.md - Missing Dependencies: troubleshooting-guide/missing-dependencies.md - Docker Issues: troubleshooting-guide/docker-issues.md - - Other Issues: troubleshooting-guide/other-issues.md - Contributing to the project: contributing.md - FAQs: faq.md diff --git a/site/content/troubleshooting-guide/other-issues.md b/site/content/troubleshooting-guide/index.md similarity index 99% rename from site/content/troubleshooting-guide/other-issues.md rename to site/content/troubleshooting-guide/index.md index 75f29ac8d..6caf86e39 100644 --- a/site/content/troubleshooting-guide/other-issues.md +++ b/site/content/troubleshooting-guide/index.md @@ -1,4 +1,4 @@ -# Other Issues +# General Issues This section of the troubleshooting guide explains how to determine, diagnose, and fix common issues you might encounter during the deployment process. ## Invalid project path provided diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index d2e01a4f6..7bdf67e33 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -263,6 +263,10 @@ private Command BuildDeployCommand() _toolInteractiveService.WriteErrorLine(e.Message); } + _toolInteractiveService.WriteErrorLine(string.Empty); + _toolInteractiveService.WriteErrorLine("For more information, please visit our troubleshooting guide https://aws.github.io/aws-dotnet-deploy/troubleshooting-guide/."); + _toolInteractiveService.WriteErrorLine("If you are still unable to solve this issue and believe this is an issue with the tooling, please cut a ticket https://github.com/aws/aws-dotnet-deploy/issues/new/choose."); + // bail out with an non-zero return code. return CommandReturnCodes.USER_ERROR; } From 85012abbcbdf5e58ba542ddba8bde3a69adc451b Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Tue, 21 Jun 2022 17:55:26 -0700 Subject: [PATCH 14/18] docs: Doc changes for Windows support with Elastic Beanstalk --- site/content/docs/support.md | 10 +++++----- site/content/faq.md | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/site/content/docs/support.md b/site/content/docs/support.md index 102dfc2b8..df005dc4f 100644 --- a/site/content/docs/support.md +++ b/site/content/docs/support.md @@ -4,10 +4,10 @@ The table below provides a matrix of supported .NET application types and AWS Co | | .NET Console App | ASP.NET Core | Blazor WebAssembly | | :--- | :----: | :---: | :---: | -| Amazon Elastic Container Service (ECS) service| X | X | | -| Amazon Elastic Container Service (ECS) task | X | X | | -| AWS App Runner | | X | | -| AWS Elastic Beanstalk | | X | | +| Amazon Elastic Container Service (ECS) service (Linux)| X | X | | +| Amazon Elastic Container Service (ECS) task (Linux) | X | X | | +| AWS App Runner (Linux) | | X | | +| AWS Elastic Beanstalk (Linux and Windows) | | X | | | Amazon S3 & Amazon CloudFront | | | X | @@ -33,7 +33,7 @@ The table below provides a matrix of supported .NET application types and AWS Co ### AWS Elastic Beanstalk -* Supports deployments of ASP.NET Core applications to AWS Elastic Beanstalk on Linux. +* Supports deployments of ASP.NET Core applications to AWS Elastic Beanstalk on Linux and Windows. * Recommended if you want to deploy your application directly to EC2 hosts. [**AWS Elastic Beanstalk**](https://aws.amazon.com/elasticbeanstalk/) is an easy-to-use service for deploying and scaling web applications and services. AWS Elastic Beanstalk automatically handles the deployment, from capacity provisioning, load balancing, auto-scaling to application health monitoring. diff --git a/site/content/faq.md b/site/content/faq.md index 07fc4c516..9d20270b2 100644 --- a/site/content/faq.md +++ b/site/content/faq.md @@ -17,3 +17,6 @@ Yes. The AWS Deploy Tool saves your deployment settings, including the environme #### *FAQ: Can I choose a different AWS service to deploy my application?* The AWS Deploy 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 need, you can select a different compute service. + +#### *FAQ: I have an application that has dependency on Windows technology, Can I use the AWS Deploy Tool to deploy it to AWS?* +ASP.NET Core applications can be deployed to AWS Elastic Beanstalk picking the "ASP.NET Core App to AWS Elastic Beanstalk on Windows" recommendation. The deployment experience is very similar the "ASP.NET Core App to AWS Elastic Beanstalk on Linux" recommendation with additional settings for configuring the Internet Information Services (IIS) resource path and web site. From fb16e87acce82f0c0456b62d0ecf6345de4c354d Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 18 May 2022 10:00:44 -0700 Subject: [PATCH 15/18] feat: Add support to deploy to Elastic Beanstalk running Windows Server --- .../DotnetBeanstalkPlatformArnCommand.cs | 2 +- ...otnetWindowsBeanstalkPlatformArnCommand.cs | 60 ++ .../Commands/TypeHints/InstanceTypeCommand.cs | 45 +- .../TypeHints/TypeHintCommandFactory.cs | 4 +- .../Data/IAWSResourceQueryer.cs | 10 +- .../Recipes/OptionSettingTypeHint.cs | 2 + .../OptionSettingItemValidatorList.cs | 6 +- .../InstanceTypeValidator.cs | 35 +- .../Recipes/Validation/ValidatorFactory.cs | 3 +- .../AWS.Deploy.Constants.projitems | 1 + src/AWS.Deploy.Constants/EC2.cs | 15 + src/AWS.Deploy.Constants/RecipeIdentifier.cs | 1 + .../Data/AWSResourceQueryer.cs | 127 ++- src/AWS.Deploy.Orchestration/Orchestrator.cs | 10 +- .../Generated/Configurations/Configuration.cs | 2 - .../.template.config/template.json | 24 + .../AppStack.cs | 58 ++ .../AspNetAppElasticBeanstalkWindows.csproj | 47 + .../Configurations/Configuration.cs | 17 + .../BeanstalkApplicationConfiguration.cs | 34 + .../Generated/Configurations/Configuration.cs | 159 ++++ ...talkManagedPlatformUpdatesConfiguration.cs | 18 + ...ticBeanstalkRollingUpdatesConfiguration.cs | 21 + .../Configurations/IAMRoleConfiguration.cs | 25 + .../Configurations/VPCConfiguration.cs | 33 + .../Generated/Recipe.cs | 460 ++++++++++ .../GlobalSuppressions.cs | 1 + .../Program.cs | 37 + .../README.md | 55 ++ .../AspNetAppElasticBeanstalkWindows/cdk.json | 7 + .../CdkTemplates/CdkTemplates.sln | 16 +- ...=> ASP.NETAppElasticBeanstalkLinux.recipe} | 0 .../ASP.NETAppElasticBeanstalkWindows.recipe | 849 ++++++++++++++++++ .../aws-deploy-recipe-schema.json | 2 + .../Helpers/ElasticBeanstalkHelper.cs | 2 +- .../RecommendationTests.cs | 23 +- .../Utilities/TestToolAWSResourceQueryer.cs | 4 +- .../WebAppNoDockerFileTests.cs | 55 +- .../ApplyPreviousSettingsTests.cs | 6 +- test/AWS.Deploy.CLI.UnitTests/Constants.cs | 3 +- .../GetOptionSettingTests.cs | 6 +- .../RecommendationTests.cs | 34 +- .../SetOptionSettingTests.cs | 14 +- .../ExistingVpcCommandTest.cs | 4 +- .../InstanceTypeCommandTest.cs | 547 +++++++++++ .../Utilities/TestToolAWSResourceQueryer.cs | 4 +- .../AWSResourceQueryerTests.cs | 73 ++ .../ElasticBeanStalkConfigFile-Windows.json | 6 + 48 files changed, 2882 insertions(+), 85 deletions(-) create mode 100644 src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs create mode 100644 src/AWS.Deploy.Constants/EC2.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/.template.config/template.json create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AppStack.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Configurations/Configuration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/BeanstalkApplicationConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/Configuration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkManagedPlatformUpdatesConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkRollingUpdatesConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/IAMRoleConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/GlobalSuppressions.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Program.cs create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/README.md create mode 100644 src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/cdk.json rename src/AWS.Deploy.Recipes/RecipeDefinitions/{ASP.NETAppElasticBeanstalk.recipe => ASP.NETAppElasticBeanstalkLinux.recipe} (100%) create mode 100644 src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe create mode 100644 test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs create mode 100644 testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows.json diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs index 74574f655..598c373bc 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs @@ -30,7 +30,7 @@ public DotnetBeanstalkPlatformArnCommand(IAWSResourceQueryer awsResourceQueryer, private async Task> GetData() { - return await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(); + return await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(BeanstalkPlatformType.Linux); } public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs new file mode 100644 index 000000000..a9f08a6fa --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs @@ -0,0 +1,60 @@ +// 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.ElasticBeanstalk.Model; +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 DotnetWindowsBeanstalkPlatformArnCommand : ITypeHintCommand + { + private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly IConsoleUtilities _consoleUtilities; + private readonly IOptionSettingHandler _optionSettingHandler; + + public DotnetWindowsBeanstalkPlatformArnCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler) + { + _awsResourceQueryer = awsResourceQueryer; + _consoleUtilities = consoleUtilities; + _optionSettingHandler = optionSettingHandler; + } + + private async Task> GetData() + { + return await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(BeanstalkPlatformType.Windows); + } + + public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + { + var platformArns = await GetData(); + return platformArns.Select(x => new TypeHintResource(x.PlatformArn, $"{x.PlatformBranchName} v{x.PlatformVersion}")).ToList(); + } + + public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + var currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); + var platformArns = await GetData(); + + var userInputConfiguration = new UserInputConfiguration( + idSelector: platform => platform.PlatformArn, + displaySelector: platform => $"{platform.PlatformBranchName} v{platform.PlatformVersion}", + defaultSelector: platform => platform.PlatformArn.Equals(currentValue)) + { + CreateNew = false + }; + + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(platformArns, "Select the Platform to use:", userInputConfiguration); + + return userResponse.SelectedOption?.PlatformArn!; + } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs index 41dc22687..310845acb 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs @@ -9,26 +9,52 @@ using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; +using AWS.Deploy.Constants; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { - public class InstanceTypeCommand : ITypeHintCommand + public class WindowsInstanceTypeCommand : InstanceTypeCommand { + public WindowsInstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler) + : base(awsResourceQueryer, consoleUtilities, optionSettingHandler, EC2.FILTER_PLATFORM_WINDOWS) + { + } + } + + public class LinuxInstanceTypeCommand : InstanceTypeCommand + { + public LinuxInstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler) + : base(awsResourceQueryer, consoleUtilities, optionSettingHandler, EC2.FILTER_PLATFORM_LINUX) + { + } + } + + public abstract class InstanceTypeCommand : ITypeHintCommand + { + private readonly IAWSResourceQueryer _awsResourceQueryer; private readonly IConsoleUtilities _consoleUtilities; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly string _platform; - public InstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler) + public InstanceTypeCommand(IAWSResourceQueryer awsResourceQueryer, IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler, string platform) { _awsResourceQueryer = awsResourceQueryer; _consoleUtilities = consoleUtilities; _optionSettingHandler = optionSettingHandler; + _platform = platform; } private async Task?> GetData() { - return await _awsResourceQueryer.ListOfAvailableInstanceTypes(); + var instanceTypes = await _awsResourceQueryer.ListOfAvailableInstanceTypes(); + if (string.Equals(_platform, EC2.FILTER_PLATFORM_WINDOWS, System.StringComparison.OrdinalIgnoreCase)) + { + return instanceTypes.Where(x => x.ProcessorInfo.SupportedArchitectures.Contains(EC2.FILTER_ARCHITECTURE_X86_64)).ToList(); + } + + return instanceTypes; } public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) @@ -58,9 +84,16 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var freeTierEligibleAnswer = _consoleUtilities.AskYesNoQuestion("Do you want the EC2 instance to be free tier eligible?", "true"); var freeTierEligible = freeTierEligibleAnswer == YesNo.Yes; - var architectureAllowedValues = new List { "x86_64", "arm64"}; - - var architecture = _consoleUtilities.AskUserToChoose(architectureAllowedValues, "The architecture of the EC2 instances created for the environment.", "x86_64"); + string architecture; + if (string.Equals(_platform, EC2.FILTER_PLATFORM_WINDOWS, System.StringComparison.OrdinalIgnoreCase)) + { + architecture = EC2.FILTER_ARCHITECTURE_X86_64; + } + else + { + var architectureAllowedValues = new List { EC2.FILTER_ARCHITECTURE_X86_64, EC2.FILTER_ARCHITECTURE_ARM64 }; + architecture = _consoleUtilities.AskUserToChoose(architectureAllowedValues, "The architecture of the EC2 instances created for the environment.", EC2.FILTER_ARCHITECTURE_X86_64); + } var cpuCores = instanceTypes .Where(x => x.FreeTierEligible.Equals(freeTierEligible)) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index 8ebf28ad7..ccc7d80f7 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -45,6 +45,7 @@ public TypeHintCommandFactory(IServiceProvider serviceProvider, IToolInteractive { OptionSettingTypeHint.ExistingBeanstalkApplication, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.BeanstalkEnvironment, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.DotnetBeanstalkPlatformArn, ActivatorUtilities.CreateInstance(serviceProvider) }, + { OptionSettingTypeHint.DotnetWindowsBeanstalkPlatformArn, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.EC2KeyPair, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.IAMRole, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ExistingIAMRole, ActivatorUtilities.CreateInstance(serviceProvider) }, @@ -62,7 +63,8 @@ public TypeHintCommandFactory(IServiceProvider serviceProvider, IToolInteractive { OptionSettingTypeHint.SQSQueueUrl, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.SNSTopicArn, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.S3BucketName, ActivatorUtilities.CreateInstance(serviceProvider) }, - { OptionSettingTypeHint.InstanceType, ActivatorUtilities.CreateInstance(serviceProvider) }, + { OptionSettingTypeHint.InstanceType, ActivatorUtilities.CreateInstance(serviceProvider) }, + { OptionSettingTypeHint.WindowsInstanceType, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ECRRepository, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ExistingVpcConnector, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ExistingSubnets, ActivatorUtilities.CreateInstance(serviceProvider) }, diff --git a/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs index 2d12af14d..b93995e91 100644 --- a/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs +++ b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs @@ -21,6 +21,12 @@ namespace AWS.Deploy.Common.Data { + /// + /// Enum for filtering the type of Elastic Beanstalk platform to deploy to. + /// + public enum BeanstalkPlatformType { Linux, Windows } + + /// /// Retrieves AWS resources /// @@ -63,8 +69,8 @@ public interface IAWSResourceQueryer Task CreateEC2KeyPair(string keyName, string saveLocation); Task> ListOfIAMRoles(string? servicePrincipal); Task> GetListOfVpcs(); - Task> GetElasticBeanstalkPlatformArns(); - Task GetLatestElasticBeanstalkPlatformArn(); + Task> GetElasticBeanstalkPlatformArns(params BeanstalkPlatformType[]? platformTypes); + Task GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType platformType); Task> GetECRAuthorizationToken(); Task> GetECRRepositories(List? repositoryNames = null); Task CreateECRRepository(string repositoryName); diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs index 07435e6e0..b1f8e888e 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs @@ -8,6 +8,7 @@ public enum OptionSettingTypeHint BeanstalkApplication, BeanstalkEnvironment, InstanceType, + WindowsInstanceType, IAMRole, ECSCluster, ECSService, @@ -16,6 +17,7 @@ public enum OptionSettingTypeHint Vpc, ExistingApplicationLoadBalancer, DotnetBeanstalkPlatformArn, + DotnetWindowsBeanstalkPlatformArn, DotnetPublishSelfContainedBuild, DotnetPublishBuildConfiguration, DotnetPublishAdditionalBuildArguments, diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs index 37441b339..c0f8ab2a4 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs @@ -42,10 +42,14 @@ public enum OptionSettingItemValidatorList /// StringLength, /// - /// Must be paired with + /// Must be paired with /// InstanceType, /// + /// Must be paired with + /// + WindowsInstanceType, + /// /// Must be paired with /// SubnetsInVpc, diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/InstanceTypeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/InstanceTypeValidator.cs index b11d0a810..ea1d287a3 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/InstanceTypeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/InstanceTypeValidator.cs @@ -6,19 +6,38 @@ using Amazon.EC2; using Amazon.EC2.Model; using AWS.Deploy.Common.Data; +using AWS.Deploy.Constants; namespace AWS.Deploy.Common.Recipes.Validation { + public class WindowsInstanceTypeValidator : InstanceTypeValidator + { + public WindowsInstanceTypeValidator(IAWSResourceQueryer awsResourceQueryer) + : base(awsResourceQueryer, EC2.FILTER_PLATFORM_WINDOWS) + { + } + } + + public class LinuxInstanceTypeValidator : InstanceTypeValidator + { + public LinuxInstanceTypeValidator(IAWSResourceQueryer awsResourceQueryer) + : base(awsResourceQueryer, EC2.FILTER_PLATFORM_LINUX) + { + } + } + /// /// Validates that a given EC2 instance is valid for the deployment region /// - public class InstanceTypeValidator : IOptionSettingItemValidator + public abstract class InstanceTypeValidator : IOptionSettingItemValidator { private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly string _platform; - public InstanceTypeValidator(IAWSResourceQueryer awsResourceQueryer) + public InstanceTypeValidator(IAWSResourceQueryer awsResourceQueryer, string platform) { _awsResourceQueryer = awsResourceQueryer; + _platform = platform; } public async Task Validate(object input, Recommendation recommendation, OptionSettingItem optionSettingItem) @@ -48,14 +67,18 @@ public async Task Validate(object input, Recommendation recomm throw ex; } } - if (instanceTypeInfo != null) + + if(instanceTypeInfo == null) { - return ValidationResult.Valid(); + return ValidationResult.Failed($"The specified instance type {rawInstanceType} does not exist in the deployment region."); } - else + + if (string.Equals(_platform, EC2.FILTER_PLATFORM_WINDOWS) && !instanceTypeInfo.ProcessorInfo.SupportedArchitectures.Contains("x64_86")) { - return ValidationResult.Failed($"The specified instance type {rawInstanceType} does not exist in the deployment region."); + return ValidationResult.Failed($"The specified instance type {rawInstanceType} does not support x86_64."); } + + return ValidationResult.Valid(); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 5301550ba..48a9b8d45 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -55,7 +55,8 @@ public ValidatorFactory(IServiceProvider serviceProvider) { OptionSettingItemValidatorList.ExistingResource, typeof(ExistingResourceValidator) }, { OptionSettingItemValidatorList.FileExists, typeof(FileExistsValidator) }, { OptionSettingItemValidatorList.StringLength, typeof(StringLengthValidator) }, - { OptionSettingItemValidatorList.InstanceType, typeof(InstanceTypeValidator) }, + { OptionSettingItemValidatorList.InstanceType, typeof(LinuxInstanceTypeValidator) }, + { OptionSettingItemValidatorList.WindowsInstanceType, typeof(WindowsInstanceTypeValidator) }, { OptionSettingItemValidatorList.SubnetsInVpc, typeof(SubnetsInVpcValidator) }, { OptionSettingItemValidatorList.SecurityGroupsInVpc, typeof(SecurityGroupsInVpcValidator) }, { OptionSettingItemValidatorList.Uri, typeof(UriValidator) }, diff --git a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems index 0b50194b9..7ea42a2bc 100644 --- a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems +++ b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems @@ -13,6 +13,7 @@ + diff --git a/src/AWS.Deploy.Constants/EC2.cs b/src/AWS.Deploy.Constants/EC2.cs new file mode 100644 index 000000000..a1e4f7c61 --- /dev/null +++ b/src/AWS.Deploy.Constants/EC2.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Constants +{ + internal static class EC2 + { + public const string FILTER_PLATFORM_WINDOWS = "windows"; + public const string FILTER_PLATFORM_LINUX = "linux"; + + public const string FILTER_ARCHITECTURE_X86_64 = "x86_64"; + public const string FILTER_ARCHITECTURE_ARM64 = "arm64"; + } +} diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs index 47e3e1df8..5436ee454 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -12,6 +12,7 @@ internal static class RecipeIdentifier // Replacement Tokens public const string REPLACE_TOKEN_STACK_NAME = "{StackName}"; public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; + public const string REPLACE_TOKEN_LATEST_DOTNET_WINDOWS_BEANSTALK_PLATFORM_ARN = "{LatestDotnetWindowsBeanstalkPlatformArn}"; public const string REPLACE_TOKEN_ECR_REPOSITORY_NAME = "{DefaultECRRepositoryName}"; public const string REPLACE_TOKEN_ECR_IMAGE_TAG = "{DefaultECRImageTag}"; public const string REPLACE_TOKEN_DOCKERFILE_PATH = "{DockerfilePath}"; diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 5a3f0f77b..644a28685 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -490,10 +490,16 @@ public async Task GetDefaultVpc() .Vpcs.FirstAsync()); } - public async Task> GetElasticBeanstalkPlatformArns() + public async Task> GetElasticBeanstalkPlatformArns(params BeanstalkPlatformType[]? platformTypes) { + if(platformTypes == null || platformTypes.Length == 0) + { + platformTypes = new BeanstalkPlatformType[] { BeanstalkPlatformType.Linux, BeanstalkPlatformType.Windows }; + } var beanstalkClient = _awsClientFactory.GetAWSClient(); + Func>> fetchPlatforms = async (platformName) => + { var request = new ListPlatformVersionsRequest { Filters = new List @@ -503,13 +509,38 @@ public async Task> GetElasticBeanstalkPlatformArns() Operator = "=", Type = "PlatformStatus", Values = { "Ready" } + }, + new PlatformFilter + { + Operator = "contains", + Type = "PlatformName", + Values = { platformName } } } }; - var response = await HandleException(async () => await beanstalkClient.ListPlatformVersionsAsync(request)); + + var platforms = await HandleException(async () => (await beanstalkClient.Paginators.ListPlatformVersions(request).PlatformSummaryList.ToListAsync())); + + // Filter out old test platforms that only internal accounts would be able to see. + platforms = platforms.Where(x => !string.IsNullOrEmpty(x.PlatformBranchName)).ToList(); + + return platforms; + }; + + var allPlatformSummaries = new List(); + if (platformTypes.Contains(BeanstalkPlatformType.Linux)) + { + allPlatformSummaries.AddRange(await fetchPlatforms(".NET Core")); + } + else if (platformTypes.Contains(BeanstalkPlatformType.Windows)) + { + var windowsPlatforms = await fetchPlatforms("Windows Server"); + SortElasticBeanstalkWindowsPlatforms(windowsPlatforms); + allPlatformSummaries.AddRange(windowsPlatforms); + } var platformVersions = new List(); - foreach (var version in response.PlatformSummaryList) + foreach (var version in allPlatformSummaries) { if (string.IsNullOrEmpty(version.PlatformCategory) || string.IsNullOrEmpty(version.PlatformBranchLifecycleState)) continue; @@ -517,18 +548,15 @@ public async Task> GetElasticBeanstalkPlatformArns() if (!version.PlatformBranchLifecycleState.Equals("Supported")) continue; - if (!version.PlatformCategory.Equals(".NET Core")) - continue; - platformVersions.Add(version); } return platformVersions; } - public async Task GetLatestElasticBeanstalkPlatformArn() + public async Task GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType platformType) { - var platforms = await GetElasticBeanstalkPlatformArns(); + var platforms = await GetElasticBeanstalkPlatformArns(platformType); if (!platforms.Any()) { @@ -538,6 +566,89 @@ public async Task GetLatestElasticBeanstalkPlatformArn() return platforms.First(); } + + + /// + /// For Windows beanstalk platforms the describe calls return a collection of Windows Server Code and Windows Server based platforms. + /// The order return will be sorted by platform versions but not OS. So for example we could get a result like the following + /// + /// IIS 10.0 running on 64bit Windows Server 2016 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server 2016 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server Core 2016 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server Core 2016 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server 2019 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server 2019 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server Core 2019 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server Core 2019 (1.0.0) + /// + /// We want the user to use the latest version of each OS first as well as the latest version of Windows first. Also Windows Server should come before Windows Server Core. + /// This matches the behavior of the existing VS toolkit picker. The above example will be sorted into the following. + /// + /// IIS 10.0 running on 64bit Windows Server 2019 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server Core 2019 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server 2016 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server Core 2016 (1.1.0) + /// IIS 10.0 running on 64bit Windows Server 2019 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server Core 2019 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server 2016 (1.0.0) + /// IIS 10.0 running on 64bit Windows Server Core 2016 (1.0.0) + /// + /// + public static void SortElasticBeanstalkWindowsPlatforms(List windowsPlatforms) + { + var parseYear = (string name) => + { + var tokens = name.Split(' '); + int year; + if (int.TryParse(tokens[tokens.Length - 1], out year)) + return year; + if (int.TryParse(tokens[tokens.Length - 2], out year)) + return year; + + return 0; + }; + var parseOSLevel = (string name) => + { + if (name.Contains("Windows Server Core")) + return 1; + if (name.Contains("Windows Server")) + return 2; + + return 10; + }; + + windowsPlatforms.Sort((x, y) => + { + if (!Version.TryParse(x.PlatformVersion, out var xVersion)) + xVersion = Version.Parse("0.0.0"); + + if (!Version.TryParse(y.PlatformVersion, out var yVersion)) + yVersion = Version.Parse("0.0.0"); + + if (yVersion != xVersion) + { + return yVersion.CompareTo(xVersion); + } + + var xYear = parseYear(x.PlatformBranchName); + var yYear = parseYear(y.PlatformBranchName); + var xOSLevel = parseOSLevel(x.PlatformBranchName); + var yOSLevel = parseOSLevel(y.PlatformBranchName); + + if (yYear == xYear) + { + if (yOSLevel == xOSLevel) + { + return 0; + } + + return yOSLevel < xOSLevel ? -1 : 1; + } + + return yYear < xYear ? -1 : 1; + }); + } + public async Task> GetECRAuthorizationToken() { var ecrClient = _awsClientFactory.GetAWSClient(); diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 7bcd60755..8710d2911 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -173,9 +173,17 @@ public async Task ApplyAllReplacementTokens(Recommendation recommendation, strin if (_awsResourceQueryer == null) throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); - var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(); + var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType.Linux); recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN, latestPlatform.PlatformArn); } + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_LATEST_DOTNET_WINDOWS_BEANSTALK_PLATFORM_ARN)) + { + if (_awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); + + var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType.Windows); + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_LATEST_DOTNET_WINDOWS_BEANSTALK_PLATFORM_ARN, latestPlatform.PlatformArn); + } if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_STACK_NAME)) { // Apply the user entered stack name to the recommendation so that any default settings based on stack name are applied. diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs index cbc9c24e0..5630527c2 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs @@ -127,8 +127,6 @@ public Configuration( string cnamePrefix, Dictionary elasticBeanstalkEnvironmentVariables, VPCConfiguration vpc, - SortedSet subnets, - SortedSet securityGroups, string environmentType = Recipe.ENVIRONMENTTYPE_SINGLEINSTANCE, string loadBalancerType = Recipe.LOADBALANCERTYPE_APPLICATION, string reverseProxy = Recipe.REVERSEPROXY_NGINX, diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/.template.config/template.json new file mode 100644 index 000000000..2c6f8696e --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "author": "AWS", + "classifications": [ + "AWS", + ".NET Deployment Tool" + ], + "name": "ASP.NET Template CDK Project for Elastic Beanstalk deployment on Windows", + "identity": "AWS.Deploy.Recipes.AspNetAppElasticBeanstalkWindows", + "shortName": "netdeploy.AspNetAppElasticBeanstalkWindows", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "AspNetAppElasticBeanstalkWindows", + "preferNameDirectory": true, + "symbols": { + "AWSDeployRecipesCDKCommonVersion": { + "type": "parameter", + "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", + "replaces": "AWSDeployRecipesCDKCommonVersion", + "datatype": "string" + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AppStack.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AppStack.cs new file mode 100644 index 000000000..410b58451 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AppStack.cs @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using Amazon.CDK; +using Amazon.CDK.AWS.ElasticBeanstalk; +using AWS.Deploy.Recipes.CDK.Common; + +using AspNetAppElasticBeanstalkWindows.Configurations; +using Constructs; + +namespace AspNetAppElasticBeanstalkWindows +{ + public class AppStack : Stack + { + private readonly Configuration _configuration; + + internal AppStack(Construct scope, IDeployToolStackProps props) + : base(scope, props.StackName, props) + { + _configuration = props.RecipeProps.Settings; + + // Setup callback for generated construct to provide access to customize CDK properties before creating constructs. + CDKRecipeCustomizer.CustomizeCDKProps += CustomizeCDKProps; + + // Create custom CDK constructs here that might need to be referenced in the CustomizeCDKProps. For example if + // creating a DynamoDB table construct and then later using the CDK construct reference in CustomizeCDKProps to + // pass the table name as an environment variable to the container image. + + // Create the recipe defined CDK construct with all of its sub constructs. + var generatedRecipe = new Recipe(this, props.RecipeProps); + + // Create additional CDK constructs here. The recipe's constructs can be accessed as properties on + // the generatedRecipe variable. + } + + /// + /// This method can be used to customize the properties for CDK constructs before creating the constructs. + /// + /// The pattern used in this method is to check to evnt.ResourceLogicalName to see if the CDK construct about to be created is one + /// you want to customize. If so cast the evnt.Props object to the CDK properties object and make the appropriate settings. + /// + /// + private void CustomizeCDKProps(CustomizePropsEventArgs evnt) + { + // Example of how to customize the Beanstalk Environment. + // + //if (string.Equals(evnt.ResourceLogicalName, nameof(evnt.Construct.BeanstalkEnvironment))) + //{ + // if (evnt.Props is CfnEnvironmentProps props) + // { + // Console.WriteLine("Customizing Beanstalk Environment"); + // } + //} + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj new file mode 100644 index 000000000..c1dccd5ce --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj @@ -0,0 +1,47 @@ + + + + Exe + netcoreapp3.1 + Major + true + enable + DeploymentProject + + AWSDeployRecipesCDKCommonVersion + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Configurations/Configuration.cs new file mode 100644 index 000000000..f4a39a6ed --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Configurations/Configuration.cs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + /// + /// The configuration settings that are passed in from the deploy tool to the CDK project. If + /// custom settings are defined in the recipe definition then corresponding properties should be added here. + /// + /// This is a partial class with all of the settings defined by default in the recipe declared in the + /// Generated directory's version of this file. + /// + public partial class Configuration + { + + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/BeanstalkApplicationConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/BeanstalkApplicationConfiguration.cs new file mode 100644 index 000000000..9032ff4fa --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/BeanstalkApplicationConfiguration.cs @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class BeanstalkApplicationConfiguration + { + public bool CreateNew { get; set; } + public string? ApplicationName { get; set; } + public string? ExistingApplicationName { get; set; } + + /// A parameterless constructor is needed for + /// or the classes will fail to initialize. + /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. +#nullable disable warnings + public BeanstalkApplicationConfiguration() + { + + } +#nullable restore warnings + + public BeanstalkApplicationConfiguration( + bool createNew) + { + CreateNew = createNew; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/Configuration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/Configuration.cs new file mode 100644 index 000000000..9ecd07c93 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/Configuration.cs @@ -0,0 +1,159 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +using System.Collections.Generic; + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class Configuration + { + /// + /// The Identity and Access Management Role that provides AWS credentials to the application to access AWS services + /// + public IAMRoleConfiguration ApplicationIAMRole { get; set; } + + /// + /// A service role is the IAM role that Elastic Beanstalk assumes when calling other services on your behalf + /// + public IAMRoleConfiguration ServiceIAMRole { get; set; } + + /// + /// The type of environment for the Elastic Beanstalk application. + /// + public string EnvironmentType { get; set; } = Recipe.ENVIRONMENTTYPE_SINGLEINSTANCE; + + /// + /// The EC2 instance type used for the EC2 instances created for the environment. + /// + public string InstanceType { get; set; } + + /// + /// The Elastic Beanstalk environment. + /// + public string EnvironmentName { get; set; } + + /// + /// The Elastic Beanstalk application. + /// + public BeanstalkApplicationConfiguration BeanstalkApplication { get; set; } + + /// + /// The name of an Elastic Beanstalk solution stack (platform version) to use with the environment. + /// + public string ElasticBeanstalkPlatformArn { get; set; } + + /// + /// The type of load balancer for your environment. + /// + public string LoadBalancerType { get; set; } = Recipe.LOADBALANCERTYPE_APPLICATION; + + /// + /// The EC2 Key Pair used for the Beanstalk Application. + /// + public string EC2KeyPair { get; set; } + + /// + /// Specifies whether to enable or disable Managed Platform Updates. + /// + public ElasticBeanstalkManagedPlatformUpdatesConfiguration ElasticBeanstalkManagedPlatformUpdates { get; set; } + + /// + /// Specifies whether to enable or disable AWS X-Ray tracing support. + /// + public bool XRayTracingSupportEnabled { get; set; } = false; + + /// + /// Specifies the IIS WebSite. + /// + public string IISWebSite { get; set; } = "Default Web Site"; + + /// + /// Specifies the IIS application path. + /// + public string IISAppPath { get; set; } = "/"; + + /// + /// Specifies whether to enable or disable enhanced health reporting. + /// + public string EnhancedHealthReporting { get; set; } = Recipe.ENHANCED_HEALTH_REPORTING; + + /// + /// The health check URL to use. + /// + public string HealthCheckURL { get; set; } + + /// + /// Specifies whether to enable or disable Rolling Updates. + /// + public ElasticBeanstalkRollingUpdatesConfiguration ElasticBeanstalkRollingUpdates { get; set; } + + /// + /// The CName Prefix used for the Beanstalk Environment. + /// + public string CNamePrefix { get; set; } + + /// + /// The environment variables that are set for the beanstalk environment. + /// + public Dictionary ElasticBeanstalkEnvironmentVariables { get; set; } = new Dictionary { }; + + /// + /// Virtual Private Cloud to launch container instance into a virtual network. + /// + public VPCConfiguration VPC { get; set; } + + /// A parameterless constructor is needed for + /// or the classes will fail to initialize. + /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. +#nullable disable warnings + public Configuration() + { + + } +#nullable restore warnings + + public Configuration( + IAMRoleConfiguration applicationIAMRole, + IAMRoleConfiguration serviceIAMRole, + string instanceType, + string environmentName, + BeanstalkApplicationConfiguration beanstalkApplication, + string elasticBeanstalkPlatformArn, + string ec2KeyPair, + ElasticBeanstalkManagedPlatformUpdatesConfiguration elasticBeanstalkManagedPlatformUpdates, + string healthCheckURL, + ElasticBeanstalkRollingUpdatesConfiguration elasticBeanstalkRollingUpdates, + string cnamePrefix, + Dictionary elasticBeanstalkEnvironmentVariables, + VPCConfiguration vpc, + string environmentType = Recipe.ENVIRONMENTTYPE_SINGLEINSTANCE, + string loadBalancerType = Recipe.LOADBALANCERTYPE_APPLICATION, + bool xrayTracingSupportEnabled = false, + string enhancedHealthReporting = Recipe.ENHANCED_HEALTH_REPORTING) + { + ApplicationIAMRole = applicationIAMRole; + ServiceIAMRole = serviceIAMRole; + InstanceType = instanceType; + EnvironmentName = environmentName; + BeanstalkApplication = beanstalkApplication; + ElasticBeanstalkPlatformArn = elasticBeanstalkPlatformArn; + EC2KeyPair = ec2KeyPair; + ElasticBeanstalkManagedPlatformUpdates = elasticBeanstalkManagedPlatformUpdates; + ElasticBeanstalkRollingUpdates = elasticBeanstalkRollingUpdates; + ElasticBeanstalkEnvironmentVariables = elasticBeanstalkEnvironmentVariables; + VPC = vpc; + EnvironmentType = environmentType; + LoadBalancerType = loadBalancerType; + XRayTracingSupportEnabled = xrayTracingSupportEnabled; + EnhancedHealthReporting = enhancedHealthReporting; + HealthCheckURL = healthCheckURL; + CNamePrefix = cnamePrefix; + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkManagedPlatformUpdatesConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkManagedPlatformUpdatesConfiguration.cs new file mode 100644 index 000000000..a13989703 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkManagedPlatformUpdatesConfiguration.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class ElasticBeanstalkManagedPlatformUpdatesConfiguration + { + public bool ManagedActionsEnabled { get; set; } = true; + public string PreferredStartTime { get; set; } = "Sun:00:00"; + public string UpdateLevel { get; set; } = "minor"; + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkRollingUpdatesConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkRollingUpdatesConfiguration.cs new file mode 100644 index 000000000..7c23bcec9 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/ElasticBeanstalkRollingUpdatesConfiguration.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class ElasticBeanstalkRollingUpdatesConfiguration + { + public bool RollingUpdatesEnabled { get; set; } = false; + public string RollingUpdateType { get; set; } = "Time"; + public int? MaxBatchSize { get; set; } + public int? MinInstancesInService { get; set; } + public string? PauseTime { get; set; } + public string Timeout { get; set; } = "PT30M"; + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/IAMRoleConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/IAMRoleConfiguration.cs new file mode 100644 index 000000000..ef6be1aa4 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/IAMRoleConfiguration.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +// This is a generated file from the original deployment recipe. It contains properties for +// all of the settings defined in the recipe file. It is recommended to not modify this file in order +// to allow easy updates to the file when the original recipe that this project was created from has updates. +// This class is marked as a partial class. If you add new settings to the recipe file, those settings should be +// added to partial versions of this class outside of the Generated folder for example in the Configuration folder. + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class IAMRoleConfiguration + { + /// + /// If set, create a new anonymously named IAM role. + /// + public bool CreateNew { get; set; } + + /// + /// If is false, + /// then use an existing IAM role by referencing through + /// + public string? RoleArn { get; set; } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs new file mode 100644 index 000000000..3ab003091 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Configurations/VPCConfiguration.cs @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AspNetAppElasticBeanstalkWindows.Configurations +{ + public partial class VPCConfiguration + { + /// + /// If set, the deployment will use a VPC to connect to the Elastic Beanstalk service. + /// + public bool UseVPC { get; set; } + + /// + /// The VPC ID to use for the Elastic Beanstalk service. + /// + public string? VpcId { get; set; } + + /// + /// 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. + /// + public SortedSet Subnets { get; set; } = new SortedSet(); + + /// + /// Lists the Amazon EC2 security groups to assign to the EC2 instances in the Auto Scaling group to define firewall rules for the instances. + /// + public SortedSet SecurityGroups { get; set; } = new SortedSet(); + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs new file mode 100644 index 000000000..e6c06b93e --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Generated/Recipe.cs @@ -0,0 +1,460 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.IO; +using System.Linq; +using System.Text.Json; +using Amazon.CDK; +using Amazon.CDK.AWS.ElasticBeanstalk; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.S3.Assets; +using AWS.Deploy.Recipes.CDK.Common; + +using AspNetAppElasticBeanstalkWindows.Configurations; +using Constructs; + +// 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. +// To customize the CDK constructs created in this file you should use the AppStack.CustomizeCDKProps() method. + +namespace AspNetAppElasticBeanstalkWindows +{ + using static AWS.Deploy.Recipes.CDK.Common.CDKRecipeCustomizer; + + public class Recipe : Construct + { + public const string ENVIRONMENTTYPE_SINGLEINSTANCE = "SingleInstance"; + public const string ENVIRONMENTTYPE_LOADBALANCED = "LoadBalanced"; + + public const string LOADBALANCERTYPE_APPLICATION = "application"; + + public const string REVERSEPROXY_NGINX = "nginx"; + + public const string ENHANCED_HEALTH_REPORTING = "enhanced"; + + public IRole? AppIAMRole { get; private set; } + + public IRole? BeanstalkServiceRole { get; private set; } + + public Asset? ApplicationAsset { get; private set; } + + public CfnInstanceProfile? Ec2InstanceProfile { get; private set; } + + public CfnApplicationVersion? ApplicationVersion { get; private set; } + + public CfnApplication? BeanstalkApplication { get; private set; } + + public CfnEnvironment? BeanstalkEnvironment { 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 + // change the expected values for the "DisplayedResources" in the corresponding recipe file. + : base(scope, "Recipe") + { + var settings = props.Settings; + + if (string.IsNullOrEmpty(props.DotnetPublishZipPath)) + throw new InvalidOrMissingConfigurationException("The provided path containing the dotnet publish zip file is null or empty."); + + // Write the Beanstalk manifest file to the .NET Zip bundle. + SetupAWSDeploymentManifest(settings, props.DotnetPublishZipPath); + + ApplicationAsset = new Asset(this, "Asset", new AssetProps + { + Path = props.DotnetPublishZipPath + }); + + ConfigureIAM(settings); + var beanstalkApplicationName = ConfigureApplication(settings); + ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName); + } + + private void ConfigureIAM(Configuration settings) + { + if (settings.ApplicationIAMRole.CreateNew) + { + AppIAMRole = new Role(this, nameof(AppIAMRole), InvokeCustomizeCDKPropsEvent(nameof(AppIAMRole), this, new RoleProps + { + AssumedBy = new ServicePrincipal("ec2.amazonaws.com"), + + // https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/iam-instanceprofile.html + ManagedPolicies = new[] + { + ManagedPolicy.FromAwsManagedPolicyName("AWSElasticBeanstalkWebTier"), + ManagedPolicy.FromAwsManagedPolicyName("AWSElasticBeanstalkWorkerTier") + } + })); + } + else + { + if (string.IsNullOrEmpty(settings.ApplicationIAMRole.RoleArn)) + throw new InvalidOrMissingConfigurationException("The provided Application IAM Role ARN is null or empty."); + + AppIAMRole = Role.FromRoleArn(this, nameof(AppIAMRole), settings.ApplicationIAMRole.RoleArn); + } + + Ec2InstanceProfile = new CfnInstanceProfile(this, nameof(Ec2InstanceProfile), InvokeCustomizeCDKPropsEvent(nameof(Ec2InstanceProfile), this, new CfnInstanceProfileProps + { + Roles = new[] + { + AppIAMRole.RoleName + } + })); + + if (settings.ServiceIAMRole.CreateNew) + { + BeanstalkServiceRole = new Role(this, nameof(BeanstalkServiceRole), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkServiceRole), this, new RoleProps + { + AssumedBy = new ServicePrincipal("elasticbeanstalk.amazonaws.com"), + + ManagedPolicies = new[] + { + ManagedPolicy.FromAwsManagedPolicyName("AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy"), + ManagedPolicy.FromAwsManagedPolicyName("service-role/AWSElasticBeanstalkEnhancedHealth") + } + })); + } + else + { + if (string.IsNullOrEmpty(settings.ServiceIAMRole.RoleArn)) + throw new InvalidOrMissingConfigurationException("The provided Service IAM Role ARN is null or empty."); + + BeanstalkServiceRole = Role.FromRoleArn(this, nameof(BeanstalkServiceRole), settings.ServiceIAMRole.RoleArn); + } + } + + private string ConfigureApplication(Configuration settings) + { + if (ApplicationAsset == null) + throw new InvalidOperationException($"{nameof(ApplicationAsset)} has not been set."); + + string beanstalkApplicationName; + if(settings.BeanstalkApplication.CreateNew) + { + if (settings.BeanstalkApplication.ApplicationName == null) + throw new InvalidOperationException($"{nameof(settings.BeanstalkApplication.ApplicationName)} has not been set."); + + beanstalkApplicationName = settings.BeanstalkApplication.ApplicationName; + } + else + { + // This check is here for deployments that were initially done with an older version of the project. + // In those deployments the existing application name was persisted in the ApplicationName property. + if (settings.BeanstalkApplication.ExistingApplicationName == null && settings.BeanstalkApplication.ApplicationName != null) + { + beanstalkApplicationName = settings.BeanstalkApplication.ApplicationName; + } + else + { + if (settings.BeanstalkApplication.ExistingApplicationName == null) + throw new InvalidOperationException($"{nameof(settings.BeanstalkApplication.ExistingApplicationName)} has not been set."); + + beanstalkApplicationName = settings.BeanstalkApplication.ExistingApplicationName; + } + } + + // Create an app version from the S3 asset defined above + // The S3 "putObject" will occur first before CF generates the template + ApplicationVersion = new CfnApplicationVersion(this, nameof(ApplicationVersion), InvokeCustomizeCDKPropsEvent(nameof(ApplicationVersion), this, new CfnApplicationVersionProps + { + ApplicationName = beanstalkApplicationName, + SourceBundle = new CfnApplicationVersion.SourceBundleProperty + { + S3Bucket = ApplicationAsset.S3BucketName, + S3Key = ApplicationAsset.S3ObjectKey + } + })); + + if (settings.BeanstalkApplication.CreateNew) + { + BeanstalkApplication = new CfnApplication(this, nameof(BeanstalkApplication), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkApplication), this, new CfnApplicationProps + { + ApplicationName = beanstalkApplicationName + })); + + ApplicationVersion.AddDependsOn(BeanstalkApplication); + } + + return beanstalkApplicationName; + } + + private void ConfigureBeanstalkEnvironment(Configuration settings, string beanstalkApplicationName) + { + if (Ec2InstanceProfile == null) + throw new InvalidOperationException($"{nameof(Ec2InstanceProfile)} has not been set. The {nameof(ConfigureIAM)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}"); + if (ApplicationVersion == null) + throw new InvalidOperationException($"{nameof(ApplicationVersion)} has not been set. The {nameof(ConfigureApplication)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}"); + + var optionSettingProperties = new List { + new CfnEnvironment.OptionSettingProperty { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "IamInstanceProfile", + Value = Ec2InstanceProfile.AttrArn + }, + new CfnEnvironment.OptionSettingProperty { + Namespace = "aws:elasticbeanstalk:environment", + OptionName = "EnvironmentType", + Value = settings.EnvironmentType + }, + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:managedactions", + OptionName = "ManagedActionsEnabled", + Value = settings.ElasticBeanstalkManagedPlatformUpdates.ManagedActionsEnabled.ToString().ToLower() + }, + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:xray", + OptionName = "XRayEnabled", + Value = settings.XRayTracingSupportEnabled.ToString().ToLower() + }, + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:healthreporting:system", + OptionName = "SystemType", + Value = settings.EnhancedHealthReporting + } + }; + + if (!string.IsNullOrEmpty(settings.InstanceType)) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "InstanceType", + Value = settings.InstanceType + }); + } + + if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_LOADBALANCED)) + { + optionSettingProperties.Add( + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:environment", + OptionName = "LoadBalancerType", + Value = settings.LoadBalancerType + } + ); + + if (!string.IsNullOrEmpty(settings.HealthCheckURL)) + { + optionSettingProperties.Add( + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:application", + OptionName = "Application Healthcheck URL", + Value = settings.HealthCheckURL + } + ); + + optionSettingProperties.Add( + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:environment:process:default", + OptionName = "HealthCheckPath", + Value = settings.HealthCheckURL + } + ); + } + } + + if (!string.IsNullOrEmpty(settings.EC2KeyPair)) + { + optionSettingProperties.Add( + new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:launchconfiguration", + OptionName = "EC2KeyName", + Value = settings.EC2KeyPair + } + ); + } + + if (settings.ElasticBeanstalkManagedPlatformUpdates.ManagedActionsEnabled) + { + if (BeanstalkServiceRole == null) + throw new InvalidOrMissingConfigurationException("The Elastic Beanstalk service role cannot be null"); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:environment", + OptionName = "ServiceRole", + Value = BeanstalkServiceRole.RoleArn + }); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:managedactions", + OptionName = "PreferredStartTime", + Value = settings.ElasticBeanstalkManagedPlatformUpdates.PreferredStartTime + }); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:managedactions:platformupdate", + OptionName = "UpdateLevel", + Value = settings.ElasticBeanstalkManagedPlatformUpdates.UpdateLevel + }); + } + + if (settings.ElasticBeanstalkRollingUpdates.RollingUpdatesEnabled) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "RollingUpdateEnabled", + Value = settings.ElasticBeanstalkRollingUpdates.RollingUpdatesEnabled.ToString().ToLower() + }); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "RollingUpdateType", + Value = settings.ElasticBeanstalkRollingUpdates.RollingUpdateType + }); + + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "Timeout", + Value = settings.ElasticBeanstalkRollingUpdates.Timeout + }); + + if (settings.ElasticBeanstalkRollingUpdates.MaxBatchSize != null) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "MaxBatchSize", + Value = settings.ElasticBeanstalkRollingUpdates.MaxBatchSize.ToString() + }); + } + + if (settings.ElasticBeanstalkRollingUpdates.MinInstancesInService != null) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "MinInstancesInService", + Value = settings.ElasticBeanstalkRollingUpdates.MinInstancesInService.ToString() + }); + } + + if (settings.ElasticBeanstalkRollingUpdates.PauseTime != null) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:autoscaling:updatepolicy:rollingupdate", + OptionName = "PauseTime", + Value = settings.ElasticBeanstalkRollingUpdates.PauseTime + }); + } + } + + if (settings.ElasticBeanstalkEnvironmentVariables != null) + { + foreach (var (key, value) in settings.ElasticBeanstalkEnvironmentVariables) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:elasticbeanstalk:application:environment", + OptionName = key, + Value = value + }); + } + } + + if (settings.VPC.UseVPC) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + Namespace = "aws:ec2:vpc", + OptionName = "VPCId", + Value = settings.VPC.VpcId + }); + + if (settings.VPC.Subnets.Any()) + { + optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty + { + 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) + }); + } + } + } + + BeanstalkEnvironment = new CfnEnvironment(this, nameof(BeanstalkEnvironment), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkEnvironment), this, new CfnEnvironmentProps + { + EnvironmentName = settings.EnvironmentName, + ApplicationName = beanstalkApplicationName, + PlatformArn = settings.ElasticBeanstalkPlatformArn, + OptionSettings = optionSettingProperties.ToArray(), + CnamePrefix = !string.IsNullOrEmpty(settings.CNamePrefix) ? settings.CNamePrefix : null, + // This line is critical - reference the label created in this same stack + VersionLabel = ApplicationVersion.Ref, + })); + } + + public void SetupAWSDeploymentManifest(Configuration settings, string dotnetZipFilePath) + { + const string MANIFEST_FILENAME = "aws-windows-deployment-manifest.json"; + + var iisWebSite = !string.IsNullOrEmpty(settings.IISWebSite) ? settings.IISWebSite : "Default Web Site"; + var iisAppPath = !string.IsNullOrEmpty(settings.IISAppPath) ? settings.IISAppPath : "/"; + + var jsonStream = new MemoryStream(); + using (var jsonWriter = new Utf8JsonWriter(jsonStream)) + { + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("manifestVersion"); + jsonWriter.WriteNumberValue(1); + + jsonWriter.WriteStartObject("deployments"); + jsonWriter.WriteStartArray("aspNetCoreWeb"); + + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("name"); + jsonWriter.WriteStringValue("MainApp"); + + jsonWriter.WriteStartObject("parameters"); + jsonWriter.WritePropertyName("appBundle"); + jsonWriter.WriteStringValue("."); + jsonWriter.WritePropertyName("iisWebSite"); + jsonWriter.WriteStringValue(iisWebSite); + jsonWriter.WritePropertyName("iisPath"); + jsonWriter.WriteStringValue(iisAppPath); + jsonWriter.WriteEndObject(); + + jsonWriter.WriteEndObject(); + + jsonWriter.WriteEndArray(); + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + } + + using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update)) + { + var zipEntry = zipArchive.CreateEntry(MANIFEST_FILENAME); + using var zipEntryStream = zipEntry.Open(); + jsonStream.Position = 0; + jsonStream.CopyTo(zipEntryStream); + + } + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/GlobalSuppressions.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/GlobalSuppressions.cs new file mode 100644 index 000000000..26233fcb5 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/GlobalSuppressions.cs @@ -0,0 +1 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "Constructs add themselves to the scope in which they are created")] diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Program.cs new file mode 100644 index 000000000..f83e6de24 --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/Program.cs @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.CDK; +using AWS.Deploy.Recipes.CDK.Common; +using AspNetAppElasticBeanstalkWindows.Configurations; +using Microsoft.Extensions.Configuration; + +namespace AspNetAppElasticBeanstalkWindows +{ + sealed class Program + { + public static void Main(string[] args) + { + var app = new App(); + + var builder = new ConfigurationBuilder().AddAWSDeployToolConfiguration(app); + var recipeProps = builder.Build().Get>(); + var appStackProps = new DeployToolStackProps(recipeProps) + { + Env = new Environment + { + Account = recipeProps.AWSAccountId, + Region = recipeProps.AWSRegion + } + }; + + // The RegisterStack method is used to set identifying information on the stack + // for the recipe used to deploy the application and preserve the settings used in the recipe + // to allow redeployment. The information is stored as CloudFormation tags and metadata inside + // the generated CloudFormation template. + CDKRecipeSetup.RegisterStack(new AppStack(app, appStackProps), appStackProps.RecipeProps); + + app.Synth(); + } + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/README.md b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/README.md new file mode 100644 index 000000000..609b87def --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/README.md @@ -0,0 +1,55 @@ +# AWS deploy tool deployment project + +This .NET project is a deployment project used by the AWS deploy tool to deploy .NET applications to AWS. The project is made +up of 2 parts. + +First is a *.recipe file which defines all of the settings for deployment project. The recipe file is what +the AWS.Deploy.Tools and the AWS Toolkit for Visual Studio use to drive the user experience to deploy a .NET application +with this deployment project. + +The second part of the deployment project is a .NET AWS CDK project which defines the AWS infrastructure that the +.NET application will be deployed to. + +## What is CDK? + +The AWS Cloud Development Kit (CDK) is an open source software development framework to define your cloud application +resources using familiar programming languages like C#. In CDK projects, constructs are instantiated for each of the +AWS resources required. CDK projects are used to generate an AWS CloudFormation template to be used by the +AWS CloudFormation service to create a Stack of all of the resources defined in a template. + +Visit the following link for more information on the AWS CDK: +https://aws.amazon.com/cdk/ + +## Should I use the CDK CLI? + +In a regular CDK project the CDK CLI, acquired from NPM, would be used to execute the CDK project. Because AWS deploy +tool deployment projects are made of both a recipe and a CDK project you should not use the CDK CLI directly on +the deployment project. + +The AWS deploy tool from either AWS.Deploy.Tools package or AWS Toolkit for Visual Studio +should be used to drive the experience. The AWS deploy tool will take care of acquiring the CDK CLI and executing the +CDK CLI passing in all of the settings gathered in the AWS deploy tool. + +## Can I modify the deployment project? + +When a deployment projects is saved the project can be customized by adding more CDK constructs or customizing the existing +CDK constructs. + +The default folder structure puts the CDK constructs originally defined by the deployment recipe into a folder called +"Generated". It is recommended to not directly modify these files and instead customize the settings via the +AppStack.CustomizeCDKProps() method. This allows the AWS deploy tool to easily updated the generated code +as new features are added to the original recipe the deployment project was created from. Checkout the AppStack.cs +file for information on how to customize the CDK project. + +## Can I add more settings to the recipe? + +As you customize the deployment project you might want to present the user's of the deployment project more +settings that will be displayed in the AWS.Deploy.Tools package or AWS Toolkit for Visual Studio. The recipe +file in the deployment project can be modified to add new settings. Below is the link to the JSON schema for the +recipe. + +https://github.com/aws/aws-dotnet-deploy/blob/main/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json + +For any new settings added to the recipe you will need to add corresponding fields in the Configuration class using the +setting ids as the property names. + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/cdk.json b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/cdk.json new file mode 100644 index 000000000..744e4fbff --- /dev/null +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "dotnet run", + "context": { + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true" + } +} diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/CdkTemplates.sln b/src/AWS.Deploy.Recipes/CdkTemplates/CdkTemplates.sln index a5e05f8a9..c567c445d 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/CdkTemplates.sln +++ b/src/AWS.Deploy.Recipes/CdkTemplates/CdkTemplates.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30413.136 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32505.173 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetAppElasticBeanstalkLinux", "AspNetAppElasticBeanstalkLinux\AspNetAppElasticBeanstalkLinux.csproj", "{030E87A0-6413-40DC-AAFF-AB05A2D02A88}" EndProject @@ -16,10 +16,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWasm", "BlazorWasm\Bl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetAppAppRunner", "AspNetAppAppRunner\AspNetAppAppRunner.csproj", "{6625E9EB-607F-4390-9A90-8B75EC024FF2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetAppElasticBeanstalkWindows", "AspNetAppElasticBeanstalkWindows\AspNetAppElasticBeanstalkWindows.csproj", "{1157F2CE-813C-4D95-81B9-30C94AEB5AB7}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{a4b77e71-3f69-492b-82ab-71632a20a424}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -53,6 +52,10 @@ Global {6625E9EB-607F-4390-9A90-8B75EC024FF2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6625E9EB-607F-4390-9A90-8B75EC024FF2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6625E9EB-607F-4390-9A90-8B75EC024FF2}.Release|Any CPU.Build.0 = Release|Any CPU + {1157F2CE-813C-4D95-81B9-30C94AEB5AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1157F2CE-813C-4D95-81B9-30C94AEB5AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1157F2CE-813C-4D95-81B9-30C94AEB5AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1157F2CE-813C-4D95-81B9-30C94AEB5AB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,4 +63,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {539AEAE5-6E50-46B7-8058-B883E4881661} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{a4b77e71-3f69-492b-82ab-71632a20a424}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe similarity index 100% rename from src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe rename to src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe new file mode 100644 index 000000000..9eb8352cc --- /dev/null +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe @@ -0,0 +1,849 @@ +{ + "$schema": "./aws-deploy-recipe-schema.json", + "Id": "AspNetAppElasticBeanstalkWindows", + "Version": "0.1.0", + "Name": "ASP.NET Core App to AWS Elastic Beanstalk on Windows", + "DeploymentType": "CdkProject", + "DeploymentBundle": "DotnetPublishZipFile", + "CdkProjectTemplate": "../CdkTemplates/AspNetAppElasticBeanstalkWindows", + "CdkProjectTemplateId": "netdeploy.AspNetAppElasticBeanstalkWindows", + "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Windows. Recommended if you do not want to deploy your application as a container image.", + "ShortDescription": "ASP.NET Core application deployed to AWS Elastic Beanstalk on Windows.", + "TargetService": "AWS Elastic Beanstalk", + + "DisplayedResources": [ + { + "LogicalId": "RecipeBeanstalkEnvironment83CC12DE", + "Description": "Application Endpoint" + } + ], + + "RecipePriority": 80, + "RecommendationRules": [ + { + "Tests": [ + { + "Type": "MSProjectSdkAttribute", + "Condition": { + "Value": "Microsoft.NET.Sdk.Web" + } + }, + { + "Type": "MSProperty", + "Condition": { + "PropertyName": "TargetFramework", + "AllowedValues": [ "netcoreapp3.1", "net5.0", "net6.0" ] + } + } + ], + "Effect": { + "Pass": { "Include": true } + } + }, + + { + "Tests": [ + { + "Type": "FileExists", + "Condition": { + "FileName": "Dockerfile" + } + } + ], + "Effect": { + "Fail": { + "PriorityAdjustment": 100, + "Include": true + } + } + }, + + { + "Tests": [ + { + "Type": "MSPropertyExists", + "Condition": { + "PropertyName": "AWSProjectType" + } + } + ], + "Effect": { + "Pass": { "Include": false }, + "Fail": { "Include": true } + } + } + ], + "Categories": [ + { + "Id": "General", + "DisplayName": "General", + "Order": 10 + }, + { + "Id": "Hosting", + "DisplayName": "Hosting", + "Order": 20 + }, + { + "Id": "Platform", + "DisplayName": "Platform", + "Order": 30 + }, + { + "Id": "RollingUpdates", + "DisplayName": "Rolling updates & deployments", + "Order": 40 + }, + { + "Id": "Health", + "DisplayName": "Health & Monitoring", + "Order": 50 + }, + { + "Id": "Compute", + "DisplayName": "Compute", + "Order": 60 + }, + { + "Id": "LoadBalancer", + "DisplayName": "LoadBalancer", + "Order": 70 + }, + { + "Id": "Permissions", + "DisplayName": "Permissions", + "Order": 80 + }, + { + "Id": "VPC", + "DisplayName": "VPC", + "Order": 90 + }, + { + "Id": "EnvVariables", + "DisplayName": "Environment Variables", + "Order": 100 + } + ], + + "OptionSettings": [ + { + "Id": "BeanstalkApplication", + "Name": "Elastic Beanstalk Application", + "Category": "General", + "Description": "The Elastic Beanstalk application.", + "Type": "Object", + "TypeHint": "BeanstalkApplication", + "AdvancedSetting": false, + "Updatable": false, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create new Elastic Beanstalk application", + "Description": "Do you want to create new application?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "ApplicationName", + "Name": "Application Name", + "Description": "The Elastic Beanstalk application name.", + "Type": "String", + "DefaultValue": "{StackName}", + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "BeanstalkApplication.CreateNew", + "Value": true + } + ], + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^[^/]{1,100}$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid Application Name. The Application name can contain up to 100 Unicode characters, not including forward slash (/)." + } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ElasticBeanstalk::Application" + } + } + ] + }, + { + "Id": "ExistingApplicationName", + "Name": "Application Name", + "Description": "The Elastic Beanstalk application name.", + "Type": "String", + "TypeHint": "ExistingBeanstalkApplication", + "DefaultValue": "{StackName}", + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "BeanstalkApplication.CreateNew", + "Value": false + } + ], + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^[^/]{1,100}$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid Application Name. The Application name can contain up to 100 Unicode characters, not including forward slash (/)." + } + } + ] + } + ] + }, + { + "Id": "EnvironmentName", + "ParentSettingId": "BeanstalkApplication.ApplicationName", + "Name": "Environment Name", + "Category": "General", + "Description": "The Elastic Beanstalk environment name.", + "Type": "String", + "DefaultValue": "{StackName}-dev", + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^[a-zA-Z0-9][a-zA-Z0-9-]{2,38}[a-zA-Z0-9]$", + "ValidationFailedMessage": "Invalid Environment Name. The Environment Name Must be from 4 to 40 characters in length. The name can contain only letters, numbers, and hyphens. It can't start or end with a hyphen." + } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ElasticBeanstalk::Environment" + } + } + ] + }, + { + "Id": "InstanceType", + "Name": "EC2 Instance Type", + "Category": "Compute", + "Description": "The EC2 instance type of the EC2 instances created for the environment.", + "Type": "String", + "TypeHint": "WindowsInstanceType", + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "WindowsInstanceType" + } + ] + }, + { + "Id": "EnvironmentType", + "Name": "Environment Type", + "Category": "General", + "Description": "The type of environment to create; for example, a single instance for development work or load balanced for production.", + "Type": "String", + "DefaultValue": "SingleInstance", + "AllowedValues": [ + "SingleInstance", + "LoadBalanced" + ], + "ValueMapping": { + "SingleInstance": "Single Instance", + "LoadBalanced": "Load Balanced" + }, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "LoadBalancerType", + "Name": "Load Balancer Type", + "Category": "LoadBalancer", + "Description": "The type of load balancer for your environment.", + "Type": "String", + "DefaultValue": "application", + "AllowedValues": [ + "application", + "classic", + "network" + ], + "ValueMapping": { + "application": "Application", + "classic": "Classic", + "network": "Network" + }, + "DependsOn": [ + { + "Id": "EnvironmentType", + "Value": "LoadBalanced" + } + ], + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "ApplicationIAMRole", + "Name": "Application IAM Role", + "Category": "Permissions", + "Description": "The Identity and Access Management (IAM) role that provides AWS credentials to the application to access AWS services.", + "Type": "Object", + "TypeHint": "IAMRole", + "TypeHintData": { + "ServicePrincipal": "ec2.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": false, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New Role", + "Description": "Do you want to create a new role?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "RoleArn", + "Name": "Existing Role ARN", + "Description": "The ARN of the existing role to use.", + "Type": "String", + "TypeHint": "ExistingIAMRole", + "TypeHintData": { + "ServicePrincipal": "ec2.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "ApplicationIAMRole.CreateNew", + "Value": false + } + ], + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "arn:.+:iam::[0-9]{12}:.+", + "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns" + } + } + ] + } + ] + }, + { + "Id": "ServiceIAMRole", + "Name": "Service IAM Role", + "Category": "Permissions", + "Description": "A service role is the IAM role that Elastic Beanstalk assumes when calling other services on your behalf.", + "Type": "Object", + "TypeHint": "IAMRole", + "TypeHintData": { + "ServicePrincipal": "elasticbeanstalk.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": false, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New Role", + "Description": "Do you want to create a new role?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "RoleArn", + "Name": "Existing Role ARN", + "Description": "The ARN of the existing role to use.", + "Type": "String", + "TypeHint": "ExistingIAMRole", + "TypeHintData": { + "ServicePrincipal": "elasticbeanstalk.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "ServiceIAMRole.CreateNew", + "Value": false + } + ], + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "arn:.+:iam::[0-9]{12}:.+", + "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns" + } + } + ] + } + ] + }, + { + "Id": "EC2KeyPair", + "Name": "Key Pair", + "Category": "Permissions", + "Description": "The EC2 key pair used to SSH into EC2 instances for the Elastic Beanstalk environment.", + "Type": "String", + "TypeHint": "EC2KeyPair", + "DefaultValue": "", + "AdvancedSetting": true, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^(?! ).+(? { new ConfigurationOptionSetting("aws:autoscaling:launchconfiguration", "IamInstanceProfile", "aws-elasticbeanstalk-ec2-role"), diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index a6c6b7aca..9c2d89cca 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -74,12 +74,13 @@ public async Task GenerateRecommendationsWithoutCustomRecipes() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - recommendations.Count.ShouldEqual(5); + recommendations.Count.ShouldEqual(6); recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe recommendations[1].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe - recommendations[3].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[4].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe + recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Windows"); // default recipe + recommendations[4].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe + recommendations[5].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe } [Fact] @@ -111,14 +112,15 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(7); + recommendations.Count.ShouldEqual(8); recommendations[0].Name.ShouldEqual(customEcsRecipeName); // custom recipe recommendations[1].Name.ShouldEqual(customEbsRecipeName); // custom recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe - recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[6].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe + recommendations[5].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Windows"); // default recipe + recommendations[6].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe + recommendations[7].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -161,13 +163,14 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(6); + recommendations.Count.ShouldEqual(7); recommendations[0].Name.ShouldEqual(customEbsRecipeName); recommendations[1].Name.ShouldEqual(customEcsRecipeName); recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); - recommendations[3].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); - recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); - recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); + recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Windows"); + recommendations[4].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); + recommendations[5].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); + recommendations[6].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index 1d8b0f1db..fab0e523c 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -25,7 +25,7 @@ namespace AWS.Deploy.CLI.IntegrationTests.Utilities { public class TestToolAWSResourceQueryer : IAWSResourceQueryer { - public Task GetLatestElasticBeanstalkPlatformArn() + public Task GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType platformType) { return System.Threading.Tasks.Task.FromResult(new PlatformSummary() { PlatformArn = string.Empty }); } @@ -41,7 +41,7 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task GetCloudFormationStack(string stackName) => throw new NotImplementedException(); public Task> GetECRAuthorizationToken() => throw new NotImplementedException(); public Task> GetECRRepositories(List repositoryNames) => throw new NotImplementedException(); - public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); + public Task> GetElasticBeanstalkPlatformArns(params BeanstalkPlatformType[] platformTypes) => throw new NotImplementedException(); public Task> GetListOfVpcs() => throw new NotImplementedException(); public Task GetS3BucketLocation(string bucketName) => throw new NotImplementedException(); public Task GetS3BucketWebSiteConfiguration(string bucketName) => throw new NotImplementedException(); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index 92f868a8d..f13ae0b53 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -95,9 +95,9 @@ public async Task DefaultConfigurations() Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); // Example: Endpoint: http://52.36.216.238/ - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")) - .Split(":")[1] - .Trim(); + var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); + var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":") + 1).Trim(); + Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); @@ -126,6 +126,55 @@ public async Task DefaultConfigurations() Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } + [Fact] + public async Task WindowsEBDefaultConfigurations() + { + _stackName = $"WinTest-{Guid.NewGuid().ToString().Split('-').Last()}"; + + // Deploy + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", "ElasticBeanStalkConfigFile-Windows.json" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + // Example: Endpoint: http://52.36.216.238/ + var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); + var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":") + 1).Trim(); + Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); + + // "extra-path" is the IISAppPath set in the config file. + applicationUrl = new Uri(new Uri(applicationUrl), "extra-path").AbsoluteUri; + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs)); ; + + // Verify stack exists in list of deployments + var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + + // Arrange input for delete + // Use --silent flag to delete without user prompts + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" }; + + // Delete + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs)); ; + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + public void Dispose() { Dispose(true); diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index 40b4ad4e9..4dd883c87 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -85,7 +85,7 @@ public async Task ApplyApplicationIAMRolePreviousSettings(bool createNew, string var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var roleArnValue = roleArn == null ? "null" : $"\"{roleArn}\""; @@ -178,7 +178,7 @@ public async Task ApplyBeanstalkApplicationNamePreviousSettings() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var serializedSettings = @$" {{ @@ -206,7 +206,7 @@ public async Task ApplyBeanstalkEnvironmentNamePreviousSettings() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var serializedSettings = @$" {{ diff --git a/test/AWS.Deploy.CLI.UnitTests/Constants.cs b/test/AWS.Deploy.CLI.UnitTests/Constants.cs index c03a5ac4b..f98f3e7d2 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Constants.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Constants.cs @@ -6,7 +6,8 @@ namespace AWS.Deploy.CLI.UnitTests internal static class Constants { public const string ASPNET_CORE_ASPNET_CORE_FARGATE_RECIPE_ID = "AspNetAppEcsFargate"; - public const string ASPNET_CORE_BEANSTALK_RECIPE_ID = "AspNetAppElasticBeanstalkLinux"; + public const string ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID = "AspNetAppElasticBeanstalkLinux"; + public const string ASPNET_CORE_BEANSTALK_WINDOWS_RECIPE_ID = "AspNetAppElasticBeanstalkWindows"; public const string ASPNET_CORE_APPRUNNER_ID = "AspNetAppAppRunner"; public const string CONSOLE_APP_FARGATE_SERVICE_RECIPE_ID = "ConsoleAppEcsFargateService"; diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index d908c0c34..bc70097b1 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -80,7 +80,7 @@ public async Task GetOptionSettingTests_OptionSettingExists(string jsonPath, str var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, jsonPath); @@ -96,7 +96,7 @@ public async Task GetOptionSettingTests_OptionSettingDoesNotExist(string jsonPat var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); Assert.Throws(() => _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, jsonPath)); } @@ -110,7 +110,7 @@ public async Task GetOptionSettingTests_GetDisplayableChildren(string optionSett var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var managedActionsEnabled = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, $"{optionSetting}.{childSetting}"); await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, managedActionsEnabled, childValue); diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index 80efb03db..d5852d840 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -90,8 +90,8 @@ public async Task WebAppNoDockerFileTest() var recommendations = await engine.ComputeRecommendations(); recommendations - .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID) - .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID) + .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); recommendations .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_ASPNET_CORE_FARGATE_RECIPE_ID) @@ -106,8 +106,8 @@ public async Task WebApiNET6() var recommendations = await engine.ComputeRecommendations(); recommendations - .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID) - .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID) + .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); recommendations .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_ASPNET_CORE_FARGATE_RECIPE_ID) @@ -131,8 +131,8 @@ public async Task WebAppWithDockerFileTest() .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_ASPNET_CORE_FARGATE_RECIPE_ID); recommendations - .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID) - .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + .Any(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID) + .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); } [Fact] @@ -177,7 +177,7 @@ public async Task WorkerServiceTest() Assert.Single(recommendations); recommendations .Any(r => r.Recipe.Id == Constants.CONSOLE_APP_FARGATE_SERVICE_RECIPE_ID) - .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + .ShouldBeTrue("Failed to receive Recommendation: " + Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); } @@ -188,7 +188,7 @@ public async Task ValueMappingWithDefaultValue() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var environmentTypeOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("EnvironmentType")); Assert.Equal("SingleInstance", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting)); @@ -258,7 +258,7 @@ public async Task ObjectMappingWithDefaultValue() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); var iamRoleTypeHintResponse = _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting); @@ -274,7 +274,7 @@ public async Task ObjectMappingWithoutDefaultValue() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); Assert.Null(_optionSettingHandler.GetOptionSettingDefaultValue(beanstalkRecommendation, applicationIAMRoleOptionSetting)); @@ -287,7 +287,7 @@ public async Task ValueMappingSetWithValue() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var environmentTypeOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("EnvironmentType")); await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); @@ -301,7 +301,7 @@ public async Task ObjectMappingSetWithValue() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting, new IAMRoleTypeHintResponse {CreateNew = false, @@ -320,7 +320,7 @@ public async Task ApplyProjectNameToSettings() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var beanstalEnvNameSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "BeanstalkEnvironment.EnvironmentName"); @@ -338,7 +338,7 @@ public async Task GetKeyValueOptionSettingServerMode() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var envVarsSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "ElasticBeanstalkEnvironmentVariables"); @@ -352,7 +352,7 @@ public async Task GetKeyValueOptionSettingConfigFile() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.FirstOrDefault(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var envVarsSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "ElasticBeanstalkEnvironmentVariables.Key"); @@ -408,7 +408,7 @@ public async Task IsDisplayable_OneDependency() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var environmentTypeOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("EnvironmentType")); var loadBalancerTypeOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("LoadBalancerType")); @@ -460,7 +460,7 @@ public async Task IsDisplayable_NotEmptyOperation() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var useVpcOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.UseVPC"); var vpcIdOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.VpcId"); var subnetsSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.Subnets"); diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index 74e73e731..716d43b74 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -75,7 +75,7 @@ public async Task SetOptionSettingTests_DisallowedValues() { var beanstalkApplication = new List { new Amazon.ElasticBeanstalk.Model.ApplicationDescription { ApplicationName = "WebApp1"} }; _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkApplications(It.IsAny())).ReturnsAsync(beanstalkApplication); - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = _optionSettingHandler.GetOptionSetting(recommendation, "BeanstalkApplication.ApplicationName"); await Assert.ThrowsAsync(() => _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, "WebApp1")); @@ -88,7 +88,7 @@ public async Task SetOptionSettingTests_DisallowedValues() [Fact] public async Task SetOptionSettingTests_AllowedValues() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.AllowedValues.First()); @@ -105,7 +105,7 @@ public async Task SetOptionSettingTests_AllowedValues() [Fact] public async Task SetOptionSettingTests_MappedValues() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.ValueMapping.Values.First())); @@ -114,7 +114,7 @@ public async Task SetOptionSettingTests_MappedValues() [Fact] public async Task SetOptionSettingTests_KeyValueType() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); var values = new Dictionary() { { "key", "value" } }; @@ -126,7 +126,7 @@ public async Task SetOptionSettingTests_KeyValueType() [Fact] public async Task SetOptionSettingTests_KeyValueType_String() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); var dictionary = new Dictionary() { { "key", "value" } }; @@ -139,7 +139,7 @@ public async Task SetOptionSettingTests_KeyValueType_String() [Fact] public async Task SetOptionSettingTests_KeyValueType_Error() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, "string")); @@ -170,7 +170,7 @@ public async Task DeploymentBundleWriteThrough_Docker() [Fact] public async Task DeploymentBundleWriteThrough_Dotnet() { - var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var dotnetBuildConfiguration = "Debug"; var dotnetPublishArgs = "--force --nologo"; diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs index 4e2b7038b..65b923bef 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs @@ -52,7 +52,7 @@ public async Task GetResources() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var vpcOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.VpcId"); @@ -91,7 +91,7 @@ public async Task Execute() var recommendations = await engine.ComputeRecommendations(); - var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); var vpcOptionSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "VPC.VpcId"); diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs new file mode 100644 index 000000000..efe074c3e --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs @@ -0,0 +1,547 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Amazon.EC2.Model; +using AWS.Deploy.CLI.Commands.TypeHints; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Constants; +using AWS.Deploy.Orchestration; +using Moq; +using Xunit; + +namespace AWS.Deploy.CLI.UnitTests.TypeHintCommands +{ + public class InstanceTypeCommandTest + { + private readonly Mock _mockAWSResourceQueryer; + private readonly IDirectoryManager _directoryManager; + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; + + public InstanceTypeCommandTest() + { + _mockAWSResourceQueryer = new Mock(); + _directoryManager = new TestDirectoryManager(); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + } + + [Fact] + public async Task WindowsGetResources() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_WINDOWS_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + var interactiveServices = new TestToolInteractiveServiceImpl(new List()); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new WindowsInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + _mockAWSResourceQueryer + .Setup(x => x.ListOfAvailableInstanceTypes()) + .ReturnsAsync(new List() + { + new InstanceTypeInfo() + { + InstanceType = "t1.any", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64, EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.arm64", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + }); + + var resources = await command.GetResources(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains(resources, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(resources, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task LinuxGetResources() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + var interactiveServices = new TestToolInteractiveServiceImpl(new List()); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new LinuxInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + _mockAWSResourceQueryer + .Setup(x => x.ListOfAvailableInstanceTypes()) + .ReturnsAsync(new List() + { + new InstanceTypeInfo() + { + InstanceType = "t1.any", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64, EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.arm64", + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + }); + + var resources = await command.GetResources(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains(resources, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task WindowsExecute() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_WINDOWS_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + + _mockAWSResourceQueryer + .Setup(x => x.ListOfAvailableInstanceTypes()) + .ReturnsAsync(new List() + { + new InstanceTypeInfo() + { + InstanceType = "t1.any", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 1, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 1000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64, EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 2, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 2000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64v2", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 2, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 3000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.arm64", + FreeTierEligible = false, + VCpuInfo = new VCpuInfo + { + DefaultCores = 3, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 2000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + }); + + // Default options + { + var interactiveServices = new TestToolInteractiveServiceImpl(new List + { + "y", // Free tier + "1", // CPU + "1", // Memory + "1" // Instance type + }); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new WindowsInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + var typeHintResponse = await command.Execute(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains("t1.any", typeHintResponse.ToString()); + } + + // Select instance type with 2 cores and 3000 of memory + { + var interactiveServices = new TestToolInteractiveServiceImpl(new List + { + "y", // Free tier + "2", // CPU + "2", // Memory + "1" // Instance type + }); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new WindowsInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + var typeHintResponse = await command.Execute(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains("t1.x86_64v2", typeHintResponse.ToString()); + } + } + + [Fact] + public async Task LinuxExecute() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + + _mockAWSResourceQueryer + .Setup(x => x.ListOfAvailableInstanceTypes()) + .ReturnsAsync(new List() + { + new InstanceTypeInfo() + { + InstanceType = "t1.any", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 1, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 1000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64, EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 2, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 2000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.x86_64v2", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 2, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 3000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_X86_64 } + } + }, + new InstanceTypeInfo() + { + InstanceType = "t1.arm64", + FreeTierEligible = true, + VCpuInfo = new VCpuInfo + { + DefaultCores = 3, + }, + MemoryInfo = new MemoryInfo + { + SizeInMiB = 2000 + }, + ProcessorInfo = new ProcessorInfo() + { + SupportedArchitectures = new List{ EC2.FILTER_ARCHITECTURE_ARM64 } + } + }, + }); + + // Default options + { + var interactiveServices = new TestToolInteractiveServiceImpl(new List + { + "y", // Free tier + "1", // Architecture x64_86 + "1", // CPU + "1", // Memory + "1" // Instance type + }); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new LinuxInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + var typeHintResponse = await command.Execute(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains("t1.any", typeHintResponse.ToString()); + } + + // Select instance type with ARM CPU 3 cores and 2000 of memory + { + var interactiveServices = new TestToolInteractiveServiceImpl(new List + { + "y", // Free tier + "2", // Architecture arm64 + "2", // CPU + "1", // Memory + "1" // Instance type + }); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager, _optionSettingHandler); + var command = new LinuxInstanceTypeCommand(_mockAWSResourceQueryer.Object, consoleUtilities, _optionSettingHandler); + + var typeHintResponse = await command.Execute(beanstalkRecommendation, instanceTypeSetting); + + Assert.Contains("t1.arm64", typeHintResponse.ToString()); + } + } + + [Fact] + public async Task WindowsValidate() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_WINDOWS_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + _mockAWSResourceQueryer + .Setup(x => x.DescribeInstanceType(It.IsAny())) + .ReturnsAsync((string type) => + { + if (type == "t1.x64_86") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { "x64_86" } + } + }; + } + if (type == "t1.arm64") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { EC2.FILTER_ARCHITECTURE_ARM64 } + } + }; + } + if (type == "t1.both") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { "x64_86", EC2.FILTER_ARCHITECTURE_ARM64 } + } + }; + } + + return null; + }); + + var validator = new WindowsInstanceTypeValidator(_mockAWSResourceQueryer.Object); + + Assert.True(validator.Validate("t1.x64_86", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.False(validator.Validate("t1.arm64", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.True(validator.Validate("t1.both", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.False(validator.Validate("t1.fake", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + } + + [Fact] + public async Task LinuxValidate() + { + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + new FileManager(), + new DirectoryManager(), + "us-west-2", + "123456789012", + "default" + ); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_LINUX_RECIPE_ID); + + var instanceTypeSetting = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, "InstanceType"); + + _mockAWSResourceQueryer + .Setup(x => x.DescribeInstanceType(It.IsAny())) + .ReturnsAsync((string type) => + { + if (type == "t1.x64_86") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { "x64_86" } + } + }; + } + if (type == "t1.arm64") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { EC2.FILTER_ARCHITECTURE_ARM64 } + } + }; + } + if (type == "t1.both") + { + return new InstanceTypeInfo + { + ProcessorInfo = new ProcessorInfo + { + SupportedArchitectures = new List { "x64_86", EC2.FILTER_ARCHITECTURE_ARM64 } + } + }; + } + + return null; + }); + + var validator = new LinuxInstanceTypeValidator(_mockAWSResourceQueryer.Object); + + Assert.True(validator.Validate("t1.x64_86", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.True(validator.Validate("t1.arm64", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.True(validator.Validate("t1.both", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + Assert.False(validator.Validate("t1.fake", beanstalkRecommendation, instanceTypeSetting).Result.IsValid); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index 6f5cb09a7..32d1e2573 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -56,12 +56,12 @@ public Task> GetECRRepositories(List repositoryNames) return Task.FromResult>(new List() { repository }); } - public Task GetLatestElasticBeanstalkPlatformArn() + public Task GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType platformType) { return Task.FromResult(new PlatformSummary() { PlatformArn = string.Empty }); } - public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); + public Task> GetElasticBeanstalkPlatformArns(params BeanstalkPlatformType[] platformTypes) => throw new NotImplementedException(); public Task> GetListOfVpcs() => throw new NotImplementedException(); public Task> ListOfEC2KeyPairs() => throw new NotImplementedException(); public Task> ListOfECSClusters(string ecsClusterName) => throw new NotImplementedException(); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs index a50107c13..3705b11ac 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/AWSResourceQueryerTests.cs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Amazon.ElasticBeanstalk.Model; using Amazon.SecurityToken; using Amazon.SecurityToken.Model; using AWS.Deploy.Common; @@ -69,5 +71,76 @@ public async Task GetCallerIdentity_BadConnection() var exceptionThrown = await Assert.ThrowsAsync(() => awsResourceQueryer.GetCallerIdentity("ap-southeast-3")); Assert.Equal(DeployToolErrorCode.UnableToAccessAWSRegion, exceptionThrown.ErrorCode); } + + [Fact] + public void SortElasticBeanstalkWindowsPlatforms() + { + // Use PlatformOwner as a placeholder to store where the summary should be sorted to. + var platforms = new List() + { + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server 2016", + PlatformVersion = "2.0.0", + PlatformOwner = "2" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server 2019", + PlatformVersion = "2.0.0", + PlatformOwner = "0" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server Core 2016", + PlatformVersion = "2.0.0", + PlatformOwner = "3" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server Core 2019", + PlatformVersion = "2.0.0", + PlatformOwner = "1" + }, + new PlatformSummary + { + PlatformBranchName = "Test Environment", + PlatformVersion = "0.5.0", + PlatformOwner = "8" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server 2016", + PlatformVersion = "1.0.0", + PlatformOwner = "6" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server 2019", + PlatformVersion = "1.0.0", + PlatformOwner = "4" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server Core 2016", + PlatformVersion = "1.0.0", + PlatformOwner = "7" + }, + new PlatformSummary + { + PlatformBranchName = "IIS 10.0 running on 64bit Windows Server Core 2019", + PlatformVersion = "1.0.0", + PlatformOwner = "5" + } + }; + + + AWSResourceQueryer.SortElasticBeanstalkWindowsPlatforms(platforms); + + for(var i = 0; i < platforms.Count; i++) + { + Assert.Equal(i.ToString(), platforms[i].PlatformOwner); + } + } } } diff --git a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows.json new file mode 100644 index 000000000..b5614e328 --- /dev/null +++ b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows.json @@ -0,0 +1,6 @@ +{ + "RecipeId": "AspNetAppElasticBeanstalkWindows", + "OptionSettingsConfig": { + "IISAppPath": "/extra-path" + } +} From 37e9fb9e8bfa40797e84c6316441c9fd2a2c6f5a Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 21 Jun 2022 22:50:18 -0400 Subject: [PATCH 16/18] chore: run CDK bootstrap in deploy tool workspace instead of generated CDK project --- src/AWS.Deploy.Orchestration/CdkProjectHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index f559cc7db..50e8622b9 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -113,8 +113,8 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication if (await DetermineIfCDKBootstrapShouldRun()) { // Ensure region is bootstrapped - var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\" --template \"{_workspaceMetadata.CDKBootstrapTemplatePath}\"", - workingDirectory: cdkProjectPath, + var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} --template \"{_workspaceMetadata.CDKBootstrapTemplatePath}\"", + workingDirectory: _workspaceMetadata.DeployToolWorkspaceDirectoryRoot, needAwsCredentials: true, redirectIO: true, streamOutputToInteractiveService: true); From b8e73abc90cfd47b2524d1d2742f154641a80385 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Wed, 22 Jun 2022 09:53:20 -0700 Subject: [PATCH 17/18] fix: Instance type and windows platform arn type hints to accommodate recent API changes. --- ...otnetWindowsBeanstalkPlatformArnCommand.cs | 37 ++++++++++++++----- .../InstanceTypeCommandTest.cs | 12 +++--- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs index a9f08a6fa..71a672b6e 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetWindowsBeanstalkPlatformArnCommand.cs @@ -33,28 +33,47 @@ private async Task> GetData() return await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(BeanstalkPlatformType.Windows); } - public async Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) + public async Task GetResources(Recommendation recommendation, OptionSettingItem optionSetting) { var platformArns = await GetData(); - return platformArns.Select(x => new TypeHintResource(x.PlatformArn, $"{x.PlatformBranchName} v{x.PlatformVersion}")).ToList(); + + var resourceTable = new TypeHintResourceTable + { + Columns = new List() + { + new TypeHintResourceColumn("Platform Branch"), + new TypeHintResourceColumn("Platform Version") + } + }; + + foreach (var platformArn in platformArns) + { + var row = new TypeHintResource(platformArn.PlatformArn, $"{platformArn.PlatformBranchName} v{platformArn.PlatformVersion}"); + row.ColumnValues.Add(platformArn.PlatformBranchName); + row.ColumnValues.Add(platformArn.PlatformVersion); + + resourceTable.Rows.Add(row); + } + + return resourceTable; } public async Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) { var currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); - var platformArns = await GetData(); + var resourceTable = await GetResources(recommendation, optionSetting); - var userInputConfiguration = new UserInputConfiguration( - idSelector: platform => platform.PlatformArn, - displaySelector: platform => $"{platform.PlatformBranchName} v{platform.PlatformVersion}", - defaultSelector: platform => platform.PlatformArn.Equals(currentValue)) + var userInputConfiguration = new UserInputConfiguration( + idSelector: platform => platform.SystemName, + displaySelector: platform => platform.DisplayName, + defaultSelector: platform => platform.SystemName.Equals(currentValue)) { CreateNew = false }; - var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(platformArns, "Select the Platform to use:", userInputConfiguration); + var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(resourceTable.Rows, "Select the Platform to use:", userInputConfiguration); - return userResponse.SelectedOption?.PlatformArn!; + return userResponse.SelectedOption?.SystemName!; } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs index efe074c3e..cfda75c43 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/InstanceTypeCommandTest.cs @@ -94,9 +94,9 @@ public async Task WindowsGetResources() var resources = await command.GetResources(beanstalkRecommendation, instanceTypeSetting); - Assert.Contains(resources, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); - Assert.Contains(resources, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); - Assert.DoesNotContain(resources, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources.Rows, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources.Rows, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(resources.Rows, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -153,9 +153,9 @@ public async Task LinuxGetResources() var resources = await command.GetResources(beanstalkRecommendation, instanceTypeSetting); - Assert.Contains(resources, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); - Assert.Contains(resources, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); - Assert.Contains(resources, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources.Rows, x => string.Equals("t1.any", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources.Rows, x => string.Equals("t1.x86_64", x.SystemName, StringComparison.OrdinalIgnoreCase)); + Assert.Contains(resources.Rows, x => string.Equals("t1.arm64", x.SystemName, StringComparison.OrdinalIgnoreCase)); } [Fact] From 5e69f8f6a8b30e7dee0a22e8f2709ae907b068cf Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Wed, 22 Jun 2022 23:48:21 -0400 Subject: [PATCH 18/18] chore: update Newtonsoft.Json version --- .../Commands/TypeHints/BeanstalkApplicationCommand.cs | 2 +- .../Commands/TypeHints/BeanstalkEnvironmentCommand.cs | 2 +- .../Commands/TypeHints/DockerBuildArgsCommand.cs | 2 +- .../TypeHints/DockerExecutionDirectoryCommand.cs | 2 +- .../Commands/TypeHints/DotnetPublishArgsCommand.cs | 2 +- .../TypeHints/DotnetPublishBuildConfigurationCommand.cs | 2 +- .../Commands/TypeHints/ECRRepositoryCommand.cs | 2 +- .../Commands/TypeHints/ECSClusterCommand.cs | 2 +- .../Commands/TypeHints/ExistingVpcConnectorCommand.cs | 2 +- src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs | 2 +- src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs | 2 +- src/AWS.Deploy.Common/AWS.Deploy.Common.csproj | 2 +- .../DeploymentManifest/DeploymentManifestEngine.cs | 3 ++- src/AWS.Deploy.Common/Exceptions.cs | 6 +++++- src/AWS.Deploy.Common/Extensions/GenericExtensions.cs | 3 ++- src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs | 2 +- .../Recipes/OptionSettingItem.ValueOverride.cs | 4 ++-- src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs | 2 +- .../FargateTaskCpuMemorySizeValidator.cs | 9 +++++++-- src/AWS.Deploy.Common/UserDeploymentSettings.cs | 7 ++++--- src/AWS.Deploy.Orchestration/Exceptions.cs | 8 ++++++++ src/AWS.Deploy.Orchestration/OptionSettingHandler.cs | 2 +- src/AWS.Deploy.Orchestration/Orchestrator.cs | 2 ++ 23 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs index 07f626d2e..f5998ccf4 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs @@ -53,7 +53,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: app => app.ApplicationName, displaySelector: app => app.ApplicationName, defaultSelector: app => app.ApplicationName.Equals(currentTypeHintResponse?.ApplicationName), - defaultNewName: currentTypeHintResponse.ApplicationName ?? String.Empty) + defaultNewName: currentTypeHintResponse?.ApplicationName ?? string.Empty) { AskNewName = true, }; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs index 5bace059e..dde9f8701 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs @@ -56,7 +56,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: env => env.EnvironmentName, displaySelector: env => env.EnvironmentName, defaultSelector: app => app.EnvironmentName.Equals(currentTypeHintResponse?.EnvironmentName), - defaultNewName: currentTypeHintResponse.EnvironmentName) + defaultNewName: currentTypeHintResponse?.EnvironmentName ?? string.Empty) { AskNewName = true, }; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs index ac2159593..ce30e7860 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs @@ -28,7 +28,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var settingValue = _consoleUtilities .AskUserForValue( string.Empty, - _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting) ?? string.Empty, allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", validators: async buildArgs => await ValidateBuildArgs(buildArgs, recommendation, optionSetting)) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index ae5c64347..8ac97090a 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -32,7 +32,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var settingValue = _consoleUtilities .AskUserForValue( string.Empty, - _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting) ?? string.Empty, allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", validators: async executionDirectory => await ValidateExecutionDirectory(executionDirectory, recommendation, optionSetting)); diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index d2a70e040..de39c32dc 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -28,7 +28,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var settingValue = _consoleUtilities .AskUserForValue( string.Empty, - _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting) ?? string.Empty, allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", validators: async publishArgs => await ValidateDotnetPublishArgs(publishArgs, recommendation, optionSetting)) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs index a89b246e9..c70cc496f 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs @@ -26,7 +26,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var settingValue = _consoleUtilities.AskUserForValue( string.Empty, - _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting) ?? string.Empty, allowEmpty: false, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? ""); recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = settingValue; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs index 3b21f2507..d51fefe85 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs @@ -37,7 +37,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: rep => rep.RepositoryName, displaySelector: rep => rep.RepositoryName, defaultSelector: rep => rep.RepositoryName.Equals(currentRepositoryName), - defaultNewName: currentRepositoryName) + defaultNewName: currentRepositoryName ?? string.Empty) { AskNewName = true, }; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs index 86b22c4d9..f420187c4 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs @@ -55,7 +55,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: cluster => cluster.ClusterArn, displaySelector: cluster => cluster.ClusterName, defaultSelector: cluster => cluster.ClusterArn.Equals(currentTypeHintResponse?.ClusterArn), - defaultNewName: currentTypeHintResponse.NewClusterName) + defaultNewName: currentTypeHintResponse?.NewClusterName ?? string.Empty) { AskNewName = true }; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs index c5eeae520..87f272c55 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs @@ -52,7 +52,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt idSelector: vpcConnector => vpcConnector.VpcConnectorArn, displaySelector: vpcConnector => vpcConnector.VpcConnectorName, defaultSelector: vpcConnector => vpcConnector.VpcConnectorArn.Equals(currentVpcConnector), - defaultNewName: currentVpcConnector) + defaultNewName: currentVpcConnector ?? string.Empty) { CanBeEmpty = true, CreateNew = false diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs index f3db51123..c5d75e92a 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs @@ -44,7 +44,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var userFilePath = _consoleUtilities .AskUserForValue( string.Empty, - _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting) ?? string.Empty, allowEmpty: typeHintData?.AllowEmpty ?? true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "") ; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs index 7dce64662..4a79be4c1 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs @@ -56,7 +56,7 @@ public async Task Execute(Recommendation recommendation, OptionSettingIt var userInputConfiguration = new UserInputConfiguration( idSelector: role => role.Arn, displaySelector: role => role.RoleName, - defaultSelector: role => currentTypeHintResponse.RoleArn?.Equals(role.Arn) ?? false); + defaultSelector: role => currentTypeHintResponse?.RoleArn?.Equals(role.Arn) ?? false); var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(existingRoles ,"Select an IAM role", userInputConfiguration); diff --git a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj index c9c4ce6b8..98148018f 100644 --- a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj +++ b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs index 313be6b4f..d97c12939 100644 --- a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs @@ -123,7 +123,8 @@ public async Task> GetRecipeDefinitionPaths(string targetApplicatio private async Task ReadManifestFile(string filePath) { var manifestFilejsonString = await _fileManager.ReadAllTextAsync(filePath); - return JsonConvert.DeserializeObject(manifestFilejsonString); + return JsonConvert.DeserializeObject(manifestFilejsonString) ?? + throw new FailedToDeserializeException(DeployToolErrorCode.InvalidDeploymentManifestModel, "The deployment manifest file is invalid."); } /// diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index cc67972b2..fe28f9d4a 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -118,7 +118,11 @@ public enum DeployToolErrorCode SelectedValueIsNotAllowed = 10009600, MissingValidatorConfiguration = 10009700, InvalidFilePath = 10009800, - InvalidDeployToolWorkspace = 10009900 + InvalidDeployToolWorkspace = 10009900, + InvalidDeploymentManifestModel = 10010000, + FailedToCreateDeepCopy = 10010100, + FailedToGetOptionSettingValue = 10010200, + ECRRepositoryNameIsNull = 10010300 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs b/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs index d48863218..9bdb87ed5 100644 --- a/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs +++ b/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs @@ -11,7 +11,8 @@ public static class GenericExtensions public static T DeepCopy(this T obj) { var serializedObject = JsonConvert.SerializeObject(obj); - return JsonConvert.DeserializeObject(serializedObject); + return JsonConvert.DeserializeObject(serializedObject) ?? + throw new FailedToDeserializeException(DeployToolErrorCode.FailedToCreateDeepCopy, "Failed to create a deep copy."); } public static bool TryDeserialize(this object obj, out T? inputList) diff --git a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs index 57cfaeea3..88af3dd84 100644 --- a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs +++ b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs @@ -40,7 +40,7 @@ public interface IOptionSettingHandler /// Retrieve the value for a specific /// This method retrieves the value in a specified type. /// - T GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting); + T? GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting); /// /// Retrieve the value for a specific diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index ece24ebb8..92ec8bac9 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -13,7 +13,7 @@ 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); @@ -159,7 +159,7 @@ public async Task SetValue(IOptionSettingHandler optionSettingHandler, object va var deserialized = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(valueOverride)); foreach (var childOptionSetting in ChildOptionSettings) { - if (deserialized.TryGetValue(childOptionSetting.Id, out var childValueOverride)) + if (deserialized?.TryGetValue(childOptionSetting.Id, out var childValueOverride) ?? false) { await optionSettingHandler.SetOptionSettingValue(recommendation, childOptionSetting, childValueOverride, skipValidation: skipValidation); } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index 9e4cd2b82..b2823bc1f 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -19,7 +19,7 @@ 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. diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs index 59f75028d..5b0a21bf6 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs @@ -62,8 +62,8 @@ private static IEnumerable BuildMemoryArray(int start, int end, int incr /// public Task Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { - string cpu; - string memory; + string? cpu; + string? memory; try { @@ -76,6 +76,11 @@ public Task Validate(Recommendation recommendation, IDeployToo "as part of of the ECS Fargate deployment configuration. Please provide a valid value and try again.")); } + if (cpu == null) + return ValidationResult.FailedAsync("Task CPU is null."); + if (memory == null) + return ValidationResult.FailedAsync("Task Memory is null."); + if (!_cpuMemoryMap.ContainsKey(cpu)) { // this could happen, but shouldn't. diff --git a/src/AWS.Deploy.Common/UserDeploymentSettings.cs b/src/AWS.Deploy.Common/UserDeploymentSettings.cs index 3a9a22016..d2f24da5d 100644 --- a/src/AWS.Deploy.Common/UserDeploymentSettings.cs +++ b/src/AWS.Deploy.Common/UserDeploymentSettings.cs @@ -36,7 +36,8 @@ public class UserDeploymentSettings { try { - var userDeploymentSettings = JsonConvert.DeserializeObject(File.ReadAllText(filePath)); + var userDeploymentSettings = JsonConvert.DeserializeObject(File.ReadAllText(filePath)) ?? + throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeUserDeploymentFile, "Failed to read user deployment settings file."); if (userDeploymentSettings.OptionSettingsConfig != null) userDeploymentSettings.TraverseRootToLeaf(userDeploymentSettings.OptionSettingsConfig.Root); return userDeploymentSettings; @@ -56,7 +57,7 @@ private void TraverseRootToLeaf(JToken node) { if (!string.IsNullOrEmpty(node.Path) && node.Type.ToString().Equals("Array")) { - var list = node.Values().Select(x => x.ToString()).ToList(); + var list = node.Values().Select(x => x?.ToString()).ToList(); LeafOptionSettingItems.Add(node.Path, JsonConvert.SerializeObject(list)); return; } @@ -70,7 +71,7 @@ private void TraverseRootToLeaf(JToken node) var path = node.Path; if (path.Contains("['")) path = path.Substring(2, node.Path.Length - 4); - LeafOptionSettingItems.Add(path, node.Value()); + LeafOptionSettingItems.Add(path, node.Value() ?? string.Empty); return; } diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 83c82374c..bfe48171f 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -69,6 +69,14 @@ public class DockerTagFailedException : DeployToolException public DockerTagFailedException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } + /// + /// Exception is thrown if ECR repository name is invalid. + /// + public class InvalidECRRepositoryNameException : DeployToolException + { + public InvalidECRRepositoryNameException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } + /// /// Exception is thrown if docker push attempt failed /// diff --git a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs index 28764f7db..7b427d06f 100644 --- a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs +++ b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs @@ -207,7 +207,7 @@ public OptionSettingItem GetOptionSetting(RecipeDefinition recipe, string? jsonP /// /// Retrieves the value of the Option Setting Item in a given recommendation. /// - public T GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting) + public T? GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting) { var displayableOptionSettings = new Dictionary(); if (optionSetting.Type == OptionSettingValueType.Object) diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 8710d2911..f97e3c020 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -285,6 +285,8 @@ private async Task CreateContainerDeploymentBundle(CloudApplication cloudApplica // Read this from the OptionSetting instead of recommendation.DeploymentBundle. // When its value comes from a replacement token, it wouldn't have been set back to the DeploymentBundle var respositoryName = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.Docker.ECRRepositoryNameOptionId)); + if (respositoryName == null) + throw new InvalidECRRepositoryNameException(DeployToolErrorCode.ECRRepositoryNameIsNull, "The ECR Repository Name is null."); string imageTag; try