From 3bb9c5da2936e22f232272c5e6707c605ed7de88 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Fri, 8 Apr 2022 14:16:36 +0000 Subject: [PATCH 01/17] build: version bump to 0.40 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 0e2524388..581eb6ecd 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.39", + "version": "0.40", "publicReleaseRefSpec": [ ".*" ], From 502ad3fcd50ccd6576dea08708a4a7ab984e9481 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 8 Apr 2022 10:20:08 -0400 Subject: [PATCH 02/17] fix: deployment doesn't fail even if cdk bootstrap fails --- src/AWS.Deploy.Common/Exceptions.cs | 3 ++- src/AWS.Deploy.Orchestration/CdkProjectHandler.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 4b551d8c3..71b90a465 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -106,7 +106,8 @@ public enum DeployToolErrorCode ECRRepositoryDoesNotExist = 10008400, FailedToDeserializeRecipe = 10008500, FailedToDeserializeDeploymentBundle = 10008600, - FailedToDeserializeDeploymentProjectRecipe = 10008700 + FailedToDeserializeDeploymentProjectRecipe = 10008700, + FailedToRunCDKBootstrap = 10008800 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index f4c1a26b8..ef66c5346 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -82,9 +82,15 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); // Ensure region is bootstrapped - await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", + var cdkBootstrap = await _commandLineWrapper.TryRunWithResult($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion} -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", workingDirectory: cdkProjectPath, - needAwsCredentials: true); + needAwsCredentials: true, + redirectIO: true, + streamOutputToInteractiveService: true); + + if (cdkBootstrap.ExitCode != 0) + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToRunCDKBootstrap, "The AWS CDK Bootstrap, which is the process of provisioning initial resources for the deployment environment, has failed. Please review the output above for additional details [and check out our troubleshooting guide for the most common failure reasons]. You can learn more about CDK bootstrapping at https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html."); + var deploymentStartDate = DateTime.Now; // Handover to CDK command line tool From 44c212fc5560021cdbd339207360f23572322d81 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Thu, 14 Apr 2022 09:38:56 -0400 Subject: [PATCH 03/17] fix: users can create a new stack with existing stack name --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 47cc01e74..ed1444dca 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -603,10 +603,12 @@ private string AskForNewCloudApplicationName(DeploymentTypes deploymentType, Lis allowEmpty: false, defaultAskValuePrompt: inputPrompt); - if (!string.IsNullOrEmpty(cloudApplicationName) && _cloudApplicationNameGenerator.IsValidName(cloudApplicationName)) + if (string.IsNullOrEmpty(cloudApplicationName) || !_cloudApplicationNameGenerator.IsValidName(cloudApplicationName)) + PrintInvalidApplicationNameMessage(); + else if (deployedApplications.Any(x => x.Name.Equals(cloudApplicationName))) + PrintApplicationNameAlreadyExistsMessage(); + else return cloudApplicationName; - - PrintInvalidApplicationNameMessage(); } } @@ -652,6 +654,14 @@ private void PrintInvalidApplicationNameMessage() "It must start with an alphabetic character and can't be longer than 128 characters"); } + private void PrintApplicationNameAlreadyExistsMessage() + { + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteErrorLine( + "Invalid application name. There already exists a CloudFormation stack with the name you provided. " + + "Please choose another application name."); + } + private bool ConfirmDeployment(Recommendation recommendation) { var message = recommendation.Recipe.DeploymentConfirmation?.DefaultMessage; From 2ec88e7bf8386ab2b3e272ef00e00c1d63449f11 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 12 Apr 2022 15:53:43 -0400 Subject: [PATCH 04/17] fix: remove --project-path argument from list-deployments command --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 1 - .../Commands/CommandHandlerInput/ListCommandHandlerInput.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index a13815eab..c35531537 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -353,7 +353,6 @@ private Command BuildListCommand() { listCommand.Add(_optionProfile); listCommand.Add(_optionRegion); - listCommand.Add(_optionProjectPath); listCommand.Add(_optionDiagnosticLogging); } diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs index f4a854f43..43f0689d4 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs @@ -12,7 +12,6 @@ public class ListCommandHandlerInput { public string? Profile { get; set; } public string? Region { get; set; } - public string? ProjectPath { get; set; } public bool Diagnostics { get; set; } } } From c9b52360f636258aa927d9904d0206f7fab637f9 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 12 Apr 2022 11:20:21 -0400 Subject: [PATCH 05/17] fix: throw an error when using non-existent profile names with commands --- src/AWS.Deploy.CLI/AWSUtilities.cs | 31 +++++++++++++---------------- src/AWS.Deploy.CLI/Exceptions.cs | 8 ++++++++ src/AWS.Deploy.Common/Exceptions.cs | 5 ++--- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/AWS.Deploy.CLI/AWSUtilities.cs b/src/AWS.Deploy.CLI/AWSUtilities.cs index d0c6a47b8..d06d552fc 100644 --- a/src/AWS.Deploy.CLI/AWSUtilities.cs +++ b/src/AWS.Deploy.CLI/AWSUtilities.cs @@ -7,9 +7,6 @@ using System.Threading.Tasks; using Amazon.Runtime; using Amazon.Runtime.CredentialManagement; -using Amazon.EC2.Model; -using System.IO; -using AWS.Deploy.CLI.Commands.CommandHandlerInput; using AWS.Deploy.CLI.Utilities; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; @@ -18,7 +15,7 @@ namespace AWS.Deploy.CLI { public interface IAWSUtilities { - Task ResolveAWSCredentials(string? profileName, string? lastUsedProfileName = null); + Task ResolveAWSCredentials(string? profileName); string ResolveAWSRegion(string? region, string? lastRegionUsed = null); } @@ -38,26 +35,26 @@ public AWSUtilities( _directoryManager = directoryManager; } - public async Task ResolveAWSCredentials(string? profileName, string? lastUsedProfileName = null) + public async Task ResolveAWSCredentials(string? profileName) { async Task Resolve() { var chain = new CredentialProfileStoreChain(); - if (!string.IsNullOrEmpty(profileName) && chain.TryGetAWSCredentials(profileName, out var profileCredentials) && + if (!string.IsNullOrEmpty(profileName)) + { + if (chain.TryGetAWSCredentials(profileName, out var profileCredentials) && // Skip checking CanLoadCredentials for AssumeRoleAWSCredentials because it might require an MFA token and the callback hasn't been setup yet. (profileCredentials is AssumeRoleAWSCredentials || await CanLoadCredentials(profileCredentials))) - { - _toolInteractiveService.WriteLine($"Configuring AWS Credentials from Profile {profileName}."); - return profileCredentials; - } - - if (!string.IsNullOrEmpty(lastUsedProfileName) && - chain.TryGetAWSCredentials(lastUsedProfileName, out var lastUsedCredentials) && - await CanLoadCredentials(lastUsedCredentials)) - { - _toolInteractiveService.WriteLine($"Configuring AWS Credentials with previous configured profile value {lastUsedProfileName}."); - return lastUsedCredentials; + { + _toolInteractiveService.WriteLine($"Configuring AWS Credentials from Profile {profileName}."); + return profileCredentials; + } + else + { + var message = $"Failed to get credentials for profile \"{profileName}\". Please provide a valid profile name and try again."; + throw new FailedToGetCredentialsForProfile(DeployToolErrorCode.FailedToGetCredentialsForProfile, message); + } } try diff --git a/src/AWS.Deploy.CLI/Exceptions.cs b/src/AWS.Deploy.CLI/Exceptions.cs index d33519562..f70fafc75 100644 --- a/src/AWS.Deploy.CLI/Exceptions.cs +++ b/src/AWS.Deploy.CLI/Exceptions.cs @@ -76,4 +76,12 @@ public class FailedToFindDeploymentProjectRecipeIdException : DeployToolExceptio { public FailedToFindDeploymentProjectRecipeIdException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if failed to retrieve credentials from the specified profile name. + /// + public class FailedToGetCredentialsForProfile : DeployToolException + { + public FailedToGetCredentialsForProfile(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 71b90a465..c1784311a 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -2,9 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Reflection; using AWS.Deploy.Common.Recipes; -using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.Common { @@ -107,7 +105,8 @@ public enum DeployToolErrorCode FailedToDeserializeRecipe = 10008500, FailedToDeserializeDeploymentBundle = 10008600, FailedToDeserializeDeploymentProjectRecipe = 10008700, - FailedToRunCDKBootstrap = 10008800 + FailedToRunCDKBootstrap = 10008800, + FailedToGetCredentialsForProfile = 10008900 } public class ProjectFileNotFoundException : DeployToolException From 84f5626f78019589df7809013f2f7a37d86758e0 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Thu, 14 Apr 2022 15:04:40 -0400 Subject: [PATCH 06/17] chore: enable semgrep --- .github/workflows/semgrep-analysis.yml | 36 ++++++++++++++++++++++++++ .semgrepignore | 1 + 2 files changed, 37 insertions(+) create mode 100644 .github/workflows/semgrep-analysis.yml create mode 100644 .semgrepignore diff --git a/.github/workflows/semgrep-analysis.yml b/.github/workflows/semgrep-analysis.yml new file mode 100644 index 000000000..783c5acbe --- /dev/null +++ b/.github/workflows/semgrep-analysis.yml @@ -0,0 +1,36 @@ +name: Semgrep + +on: + # Scan changed files in PRs, block on new issues only (existing issues ignored) + pull_request: + + push: + branches: ["dev", "main"] + + schedule: + - cron: '23 20 * * 1' + +jobs: + semgrep: + name: Scan + runs-on: ubuntu-latest + container: + image: returntocorp/semgrep + # Skip any PR created by dependabot to avoid permission issues + if: (github.actor != 'dependabot[bot]') + steps: + # Fetch project source + - uses: actions/checkout@v3 + + - run: semgrep scan --sarif --output=semgrep.sarif + env: + SEMGREP_RULES: >- # more at semgrep.dev/explore + p/security-audit + p/secrets + p/owasp-top-ten + + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: semgrep.sarif + if: always() \ No newline at end of file diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 000000000..ac7191dd2 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1 @@ +testapps/ \ No newline at end of file From 6edd23d642d445c4dbab4fe377a45b95ed44817e Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Mon, 18 Apr 2022 19:35:25 -0400 Subject: [PATCH 07/17] feat: Use CDK Diff to perform server mode integ tests --- .../Controllers/DeploymentController.cs | 56 ++++++++- src/AWS.Deploy.CLI/ServerMode/Exceptions.cs | 8 ++ .../GenerateCloudFormationTemplateOutput.cs | 23 ++++ .../Tasks/DeployRecommendationTask.cs | 26 ++++- src/AWS.Deploy.Common/Exceptions.cs | 4 +- .../CdkProjectHandler.cs | 29 ++++- .../CdkDeploymentCommand.cs | 2 +- src/AWS.Deploy.Orchestration/Exceptions.cs | 16 +++ src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 107 ++++++++++++++++++ .../ServerMode/GetApplyOptionSettings.cs | 105 +++++++---------- 10 files changed, 304 insertions(+), 72 deletions(-) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/GenerateCloudFormationTemplateOutput.cs diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 4598eb07e..804639a3f 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -439,6 +439,43 @@ public async Task GetCompatibility(string sessionId) return Ok(output); } + /// + /// Creates the CloudFormation template that will be used by CDK for the deployment. + /// This operation returns the CloudFormation template that is created for this deployment. + /// + [HttpGet("session//cftemplate")] + [SwaggerOperation(OperationId = "GenerateCloudFormationTemplate")] + [SwaggerResponse(200, type: typeof(GenerateCloudFormationTemplateOutput))] + [Authorize] + public async Task GenerateCloudFormationTemplate(string sessionId) + { + var state = _stateServer.Get(sessionId); + if (state == null) + { + return NotFound($"Session ID {sessionId} not found."); + } + + var serviceProvider = CreateSessionServiceProvider(state); + + var orchestratorSession = CreateOrchestratorSession(state); + + var orchestrator = CreateOrchestrator(state, serviceProvider); + + var cdkProjectHandler = CreateCdkProjectHandler(state, serviceProvider); + + if (state.SelectedRecommendation == null) + throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); + + if (!state.SelectedRecommendation.Recipe.DeploymentType.Equals(Common.Recipes.DeploymentTypes.CdkProject)) + throw new SelectedRecommendationIsIncompatibleException($"We cannot generate a CloudFormation template for the selected recommendation as it is not of type '{nameof(Models.DeploymentTypes.CloudFormationStack)}'."); + + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); + var cloudFormationTemplate = await task.GenerateCloudFormationTemplate(cdkProjectHandler); + var output = new GenerateCloudFormationTemplateOutput(cloudFormationTemplate); + + return Ok(output); + } + /// /// Begin execution of the deployment. /// @@ -455,6 +492,8 @@ public async Task StartDeployment(string sessionId) var serviceProvider = CreateSessionServiceProvider(state); + var orchestratorSession = CreateOrchestratorSession(state); + var orchestrator = CreateOrchestrator(state, serviceProvider); if (state.SelectedRecommendation == null) @@ -473,7 +512,7 @@ public async Task StartDeployment(string sessionId) if (capabilities.Any()) return Problem($"Unable to start deployment due to missing system capabilities.{Environment.NewLine}{missingCapabilitiesMessage}"); - var task = new DeployRecommendationTask(orchestrator, state.ApplicationDetails, state.SelectedRecommendation); + var task = new DeployRecommendationTask(orchestratorSession, orchestrator, state.ApplicationDetails, state.SelectedRecommendation); state.DeploymentTask = task.Execute(); return Ok(); @@ -603,6 +642,21 @@ private OrchestratorSession CreateOrchestratorSession(SessionState state, AWSCre state.AWSAccountId); } + private CdkProjectHandler CreateCdkProjectHandler(SessionState state, IServiceProvider? serviceProvider = null) + { + if (serviceProvider == null) + { + serviceProvider = CreateSessionServiceProvider(state); + } + + return new CdkProjectHandler( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService() + ); + } + private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? serviceProvider = null, AWSCredentials? awsCredentials = null) { if(serviceProvider == null) diff --git a/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs b/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs index c82b8b5a3..ba314af34 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Exceptions.cs @@ -14,6 +14,14 @@ public class SelectedRecommendationIsNullException : Exception public SelectedRecommendationIsNullException(string message, Exception? innerException = null) : base(message, innerException) { } } + /// + /// Throw if the selected recommendation is incompatible with the operation performed. + /// + public class SelectedRecommendationIsIncompatibleException : Exception + { + public SelectedRecommendationIsIncompatibleException(string message, Exception? innerException = null) : base(message, innerException) { } + } + /// /// Throw if the tool was not able to retrieve the AWS Credentials. /// diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/GenerateCloudFormationTemplateOutput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/GenerateCloudFormationTemplateOutput.cs new file mode 100644 index 000000000..3f93d5ef2 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/GenerateCloudFormationTemplateOutput.cs @@ -0,0 +1,23 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using AWS.Deploy.CLI.ServerMode.Controllers; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + /// + /// The response that will be returned by the operation. + /// + public class GenerateCloudFormationTemplateOutput + { + /// + /// The CloudFormation template of the generated CDK deployment project. + /// + public string CloudFormationTemplate { get; set; } + + public GenerateCloudFormationTemplateOutput(string cloudFormationTemplate) + { + CloudFormationTemplate = cloudFormationTemplate; + } + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs index 695be7844..c4b1078bd 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Tasks/DeployRecommendationTask.cs @@ -15,10 +15,12 @@ public class DeployRecommendationTask { private readonly CloudApplication _cloudApplication; private readonly Orchestrator _orchestrator; + private readonly OrchestratorSession _orchestratorSession; private readonly Recommendation _selectedRecommendation; - public DeployRecommendationTask(Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation) + public DeployRecommendationTask(OrchestratorSession orchestratorSession, Orchestrator orchestrator, CloudApplication cloudApplication, Recommendation selectedRecommendation) { + _orchestratorSession = orchestratorSession; _orchestrator = orchestrator; _cloudApplication = cloudApplication; _selectedRecommendation = selectedRecommendation; @@ -30,6 +32,28 @@ public async Task Execute() await _orchestrator.DeployRecommendation(_cloudApplication, _selectedRecommendation); } + /// + /// Generates the CloudFormation template that will be used by CDK for the deployment. + /// This involves creating a deployment bundle, generating the CDK project and running 'cdk diff' to get the CF template. + /// This operation returns the CloudFormation template that is created for this deployment. + /// + public async Task GenerateCloudFormationTemplate(CdkProjectHandler cdkProjectHandler) + { + if (cdkProjectHandler == null) + throw new FailedToCreateCDKProjectException(DeployToolErrorCode.FailedToCreateCDKProject, $"We could not create a CDK deployment project due to a missing dependency '{nameof(cdkProjectHandler)}'."); + + await CreateDeploymentBundle(); + var cdkProject = await cdkProjectHandler.ConfigureCdkProject(_orchestratorSession, _cloudApplication, _selectedRecommendation); + try + { + return await cdkProjectHandler.PerformCdkDiff(cdkProject, _cloudApplication); + } + finally + { + cdkProjectHandler.DeleteTemporaryCdkProject(cdkProject); + } + } + private async Task CreateDeploymentBundle() { if (_selectedRecommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index c1784311a..fa2c3079d 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -106,7 +106,9 @@ public enum DeployToolErrorCode FailedToDeserializeDeploymentBundle = 10008600, FailedToDeserializeDeploymentProjectRecipe = 10008700, FailedToRunCDKBootstrap = 10008800, - FailedToGetCredentialsForProfile = 10008900 + FailedToGetCredentialsForProfile = 10008900, + FailedToRunCDKDiff = 10009000, + FailedToCreateCDKProject = 10009100 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index ef66c5346..0cb479fdc 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -19,7 +19,8 @@ public interface ICdkProjectHandler Task ConfigureCdkProject(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); string CreateCdkProject(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); Task DeployCdkProject(OrchestratorSession session, CloudApplication cloudApplication, string cdkProjectPath, Recommendation recommendation); - void DeleteTemporaryCdkProject(OrchestratorSession session, string cdkProjectPath); + void DeleteTemporaryCdkProject(string cdkProjectPath); + Task PerformCdkDiff(string cdkProjectPath, CloudApplication cloudApplication); } public class CdkProjectHandler : ICdkProjectHandler @@ -29,17 +30,20 @@ public class CdkProjectHandler : ICdkProjectHandler private readonly CdkAppSettingsSerializer _appSettingsBuilder; private readonly IDirectoryManager _directoryManager; private readonly IAWSResourceQueryer _awsResourceQueryer; + private readonly IFileManager _fileManager; public CdkProjectHandler( IOrchestratorInteractiveService interactiveService, ICommandLineWrapper commandLineWrapper, - IAWSResourceQueryer awsResourceQueryer) + IAWSResourceQueryer awsResourceQueryer, + IFileManager fileManager) { _interactiveService = interactiveService; _commandLineWrapper = commandLineWrapper; _awsResourceQueryer = awsResourceQueryer; _appSettingsBuilder = new CdkAppSettingsSerializer(); _directoryManager = new DirectoryManager(); + _fileManager = fileManager; } public async Task ConfigureCdkProject(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation) @@ -69,6 +73,25 @@ public async Task ConfigureCdkProject(OrchestratorSession session, Cloud return cdkProjectPath; } + /// + /// Run 'cdk diff' on the deployment project to get the CF template that will be used by CDK to deploy the application. + /// + /// The CloudFormation template that is created for this deployment. + public async Task PerformCdkDiff(string cdkProjectPath, CloudApplication cloudApplication) + { + var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); + + var cdkDiff = await _commandLineWrapper.TryRunWithResult($"npx cdk diff -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", + workingDirectory: cdkProjectPath, + needAwsCredentials: true); + + if (cdkDiff.ExitCode != 0) + throw new FailedToRunCDKDiffException(DeployToolErrorCode.FailedToRunCDKDiff, "The CDK Diff command encountered an error and failed."); + + var templateFilePath = Path.Combine(cdkProjectPath, "cdk.out", $"{cloudApplication.Name}.template.json"); + return await _fileManager.ReadAllTextAsync(templateFilePath); + } + public async Task DeployCdkProject(OrchestratorSession session, CloudApplication cloudApplication, string cdkProjectPath, Recommendation recommendation) { var recipeInfo = $"{recommendation.Recipe.Id}_{recommendation.Recipe.Version}"; @@ -161,7 +184,7 @@ public string CreateCdkProject(Recommendation recommendation, OrchestratorSessio return saveCdkDirectoryPath; } - public void DeleteTemporaryCdkProject(OrchestratorSession session, string cdkProjectPath) + public void DeleteTemporaryCdkProject(string cdkProjectPath) { var parentPath = Path.GetFullPath(Constants.CDK.ProjectsDirectory); cdkProjectPath = Path.GetFullPath(cdkProjectPath); diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs index 41291aba1..7d8f003b9 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/CdkDeploymentCommand.cs @@ -48,7 +48,7 @@ public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloud } finally { - orchestrator._cdkProjectHandler.DeleteTemporaryCdkProject(orchestrator._session, cdkProject); + orchestrator._cdkProjectHandler.DeleteTemporaryCdkProject(cdkProject); } await orchestrator._localUserSettingsEngine.UpdateLastDeployedStack(cloudApplication.Name, orchestrator._session.ProjectDefinition.ProjectName, orchestrator._session.AWSAccountId, orchestrator._session.AWSRegion); diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 8f65b2dba..41271992e 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -141,6 +141,14 @@ public class FailedToDeployCDKAppException : DeployToolException public FailedToDeployCDKAppException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + /// + /// Exception thrown if the 'cdk diff' command failed. + /// + public class FailedToRunCDKDiffException : DeployToolException + { + public FailedToRunCDKDiffException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } + /// /// Exception thrown if an AWS Resource is not found or does not exist. /// @@ -220,4 +228,12 @@ public class FailedToFindCloudApplicationResourceType : DeployToolException { public FailedToFindCloudApplicationResourceType(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if we are unable to generate a CDK Project + /// + public class FailedToCreateCDKProjectException : DeployToolException + { + public FailedToCreateCDKProjectException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index b588f3e91..fac37244c 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -119,6 +119,19 @@ public partial interface IRestAPIClient /// A server side error occurred. System.Threading.Tasks.Task GetCompatibilityAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + /// Creates the CloudFormation template that will be used by CDK for the deployment. + /// This operation returns the CloudFormation template that is created for this deployment. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GenerateCloudFormationTemplateAsync(string sessionId); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Creates the CloudFormation template that will be used by CDK for the deployment. + /// This operation returns the CloudFormation template that is created for this deployment. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GenerateCloudFormationTemplateAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + /// Begin execution of the deployment. /// Success /// A server side error occurred. @@ -954,6 +967,89 @@ public async System.Threading.Tasks.Task GetCompatibilit } } + /// Creates the CloudFormation template that will be used by CDK for the deployment. + /// This operation returns the CloudFormation template that is created for this deployment. + /// Success + /// A server side error occurred. + public System.Threading.Tasks.Task GenerateCloudFormationTemplateAsync(string sessionId) + { + return GenerateCloudFormationTemplateAsync(sessionId, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Creates the CloudFormation template that will be used by CDK for the deployment. + /// This operation returns the CloudFormation template that is created for this deployment. + /// Success + /// A server side error occurred. + public async System.Threading.Tasks.Task GenerateCloudFormationTemplateAsync(string sessionId, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v1/Deployment/session//cftemplate?"); + if (sessionId != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("sessionId") + "=").Append(System.Uri.EscapeDataString(ConvertToString(sessionId, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// Begin execution of the deployment. /// Success /// A server side error occurred. @@ -1649,6 +1745,17 @@ public partial class ExistingDeploymentSummary public string ExistingDeploymentId { get; set; } + } + + /// The response that will be returned by the M:AWS.Deploy.CLI.ServerMode.Controllers.DeploymentController.GenerateCloudFormationTemplate(System.String) operation. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] + public partial class GenerateCloudFormationTemplateOutput + { + /// The CloudFormation template of the generated CDK deployment project. + [Newtonsoft.Json.JsonProperty("cloudFormationTemplate", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string CloudFormationTemplate { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index 59a63004b..c2e7c3f67 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -9,15 +9,19 @@ using System.Threading; using System.Threading.Tasks; using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; using Amazon.Runtime; using AWS.Deploy.CLI.Commands; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; -using AWS.Deploy.CLI.IntegrationTests.Services; +using AWS.Deploy.CLI.TypeHintResponses; +using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; using Microsoft.Extensions.DependencyInjection; +using Moq; using Newtonsoft.Json; using Xunit; @@ -28,18 +32,17 @@ public class GetApplyOptionSettings : IDisposable private bool _isDisposed; private string _stackName; private readonly IServiceProvider _serviceProvider; - private readonly CloudFormationHelper _cloudFormationHelper; private readonly string _awsRegion; private readonly TestAppManager _testAppManager; - private readonly InMemoryInteractiveService _interactiveService; + + private readonly Mock _mockAWSClientFactory; + private readonly Mock _mockCFClient; public GetApplyOptionSettings() { - _interactiveService = new InMemoryInteractiveService(); - - var cloudFormationClient = new AmazonCloudFormationClient(Amazon.RegionEndpoint.USWest2); - _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); + _mockAWSClientFactory = new Mock(); + _mockCFClient = new Mock(); var serviceCollection = new ServiceCollection(); @@ -53,24 +56,20 @@ public GetApplyOptionSettings() _testAppManager = new TestAppManager(); } - public Task ResolveCredentials() + public TemplateMetadataReader GetTemplateMetadataReader(string templateBody) { - var testCredentials = FallbackCredentialsFactory.GetCredentials(); - return Task.FromResult(testCredentials); + var templateMetadataReader = new TemplateMetadataReader(_mockAWSClientFactory.Object); + var cfResponse = new GetTemplateResponse(); + cfResponse.TemplateBody = templateBody; + _mockAWSClientFactory.Setup(x => x.GetAWSClient(It.IsAny())).Returns(_mockCFClient.Object); + _mockCFClient.Setup(x => x.GetTemplateAsync(It.IsAny(), It.IsAny())).ReturnsAsync(cfResponse); + return templateMetadataReader; } - private async Task WaitForDeployment(RestAPIClient restApiClient, string sessionId) + public Task ResolveCredentials() { - // 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 () => - { - DeploymentStatus status = (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; ; - return status != DeploymentStatus.Executing; - }, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(15)); - - return (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; + var testCredentials = FallbackCredentialsFactory.GetCredentials(); + return Task.FromResult(testCredentials); } [Fact] @@ -130,12 +129,13 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() Assert.Empty(subnetsResourcesEmpty.Resources); Assert.Empty(securityGroupsResourcesEmpty.Resources); + var vpcId = vpcResources.Resources.First().SystemName; await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() { UpdatedSettings = new Dictionary() { {"VPCConnector.CreateNew", "true"}, - {"VPCConnector.VpcId", vpcResources.Resources.First().SystemName} + {"VPCConnector.VpcId", vpcId} } }); @@ -144,48 +144,31 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() Assert.NotEmpty(subnetsResources.Resources); Assert.NotEmpty(securityGroupsResources.Resources); + var subnet = subnetsResources.Resources.Last().SystemName; + var securityGroup = securityGroupsResources.Resources.First().SystemName; var setConfigResult = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() { UpdatedSettings = new Dictionary() { - {"VPCConnector.Subnets", JsonConvert.SerializeObject(new List{subnetsResources.Resources.Last().SystemName})}, - {"VPCConnector.SecurityGroups", JsonConvert.SerializeObject(new List{securityGroupsResources.Resources.Last().SystemName})} + {"VPCConnector.Subnets", JsonConvert.SerializeObject(new List{subnet})}, + {"VPCConnector.SecurityGroups", JsonConvert.SerializeObject(new List{securityGroup})} } }); - await restClient.StartDeploymentAsync(sessionId); - - await WaitForDeployment(restClient, sessionId); - - var stackStatus = await _cloudFormationHelper.GetStackStatus(_stackName); - Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); - - Assert.True(logOutput.Length > 0); - Assert.Contains("Initiating deployment", logOutput.ToString()); - - var redeploymentSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput - { - AwsRegion = _awsRegion, - ProjectPath = projectPath - }); - - var redeploymentSessionId = redeploymentSessionOutput.SessionId; + var generateCloudFormationTemplateResponse = await restClient.GenerateCloudFormationTemplateAsync(sessionId); - var existingDeployments = await restClient.GetExistingDeploymentsAsync(redeploymentSessionId); - var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(_stackName, x.Name)); + var metadata = await GetAppSettingsFromCFTemplate(generateCloudFormationTemplateResponse.CloudFormationTemplate, _stackName); - Assert.Equal(_stackName, existingDeployment.Name); - Assert.Equal(appRunnerRecommendation.RecipeId, existingDeployment.RecipeId); - Assert.Equal(appRunnerRecommendation.Name, existingDeployment.RecipeName); - Assert.Equal(appRunnerRecommendation.ShortDescription, existingDeployment.ShortDescription); - Assert.Equal(appRunnerRecommendation.Description, existingDeployment.Description); - Assert.Equal(appRunnerRecommendation.TargetService, existingDeployment.TargetService); - Assert.Equal(DeploymentTypes.CloudFormationStack, existingDeployment.DeploymentType); + Assert.True(metadata.Settings.ContainsKey("VPCConnector")); + var vpcConnector = JsonConvert.DeserializeObject(metadata.Settings["VPCConnector"].ToString()); + Assert.True(vpcConnector.CreateNew); + Assert.Equal(vpcId, vpcConnector.VpcId); + Assert.Contains(subnet, vpcConnector.Subnets); + Assert.Contains(securityGroup, vpcConnector.SecurityGroups); } finally { cancelSource.Cancel(); - await _cloudFormationHelper.DeleteStack(_stackName); _stackName = null; } } @@ -208,6 +191,12 @@ await WaitUntilHelper.WaitUntil(async () => }, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10)); } + private async Task GetAppSettingsFromCFTemplate(string cloudFormationTemplate, string stackName) + { + var templateMetadataReader = GetTemplateMetadataReader(cloudFormationTemplate); + return await templateMetadataReader.LoadCloudApplicationMetadata(stackName); + } + public void Dispose() { Dispose(true); @@ -218,20 +207,6 @@ protected virtual void Dispose(bool disposing) { if (_isDisposed) return; - if (disposing) - { - if(!string.IsNullOrEmpty(_stackName)) - { - var isStackDeleted = _cloudFormationHelper.IsStackDeleted(_stackName).GetAwaiter().GetResult(); - if (!isStackDeleted) - { - _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); - } - - _interactiveService.ReadStdOutStartToEnd(); - } - } - _isDisposed = true; } From abe744a57f925a9dbc72c7658b42b4cb448f40a3 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Mon, 18 Apr 2022 21:44:55 -0400 Subject: [PATCH 08/17] feat: Add TypeHintData to GetConfigSettings response in Server Mode --- .../Controllers/DeploymentController.cs | 1 + .../Models/OptionSettingItemSummary.cs | 2 + src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 3 + .../ServerMode/GetApplyOptionSettings.cs | 110 +++++++++++++----- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 804639a3f..b0a33e4f6 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -193,6 +193,7 @@ private List ListOptionSettingSummary(Recommendation r var settingSummary = new OptionSettingItemSummary(setting.Id, setting.Name, setting.Description, setting.Type.ToString()) { TypeHint = setting.TypeHint?.ToString(), + TypeHintData = setting.TypeHintData, Value = recommendation.GetOptionSettingValue(setting), Advanced = setting.AdvancedSetting, ReadOnly = recommendation.IsExistingCloudApplication && !setting.Updatable, diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs index b9be7d986..d20264033 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs @@ -20,6 +20,8 @@ public class OptionSettingItemSummary public string? TypeHint { get; set; } + public Dictionary TypeHintData { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public bool Advanced { get; set; } public bool ReadOnly { get; set; } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index fac37244c..907e9ba22 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1861,6 +1861,9 @@ public partial class OptionSettingItemSummary [Newtonsoft.Json.JsonProperty("typeHint", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string TypeHint { get; set; } + [Newtonsoft.Json.JsonProperty("typeHintData", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.IDictionary TypeHintData { get; set; } + [Newtonsoft.Json.JsonProperty("advanced", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool Advanced { get; set; } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index c2e7c3f67..58a4bf686 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -20,6 +20,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; +using AWS.Deploy.Common.TypeHintData; using Microsoft.Extensions.DependencyInjection; using Moq; using Newtonsoft.Json; @@ -75,7 +76,7 @@ public Task ResolveCredentials() [Fact] public async Task GetAndApplyAppRunnerSettings_VPCConnector() { - _stackName = $"ServerModeWebFargate{Guid.NewGuid().ToString().Split('-').Last()}"; + _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4001; @@ -92,35 +93,12 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() await WaitTillServerModeReady(restClient); - var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput - { - AwsRegion = _awsRegion, - ProjectPath = projectPath - }); - - var sessionId = startSessionOutput.SessionId; - Assert.NotNull(sessionId); - - var signalRClient = new DeploymentCommunicationClient(baseUrl); - await signalRClient.JoinSession(sessionId); + var sessionId = await StartDeploymentSession(restClient, projectPath); var logOutput = new StringBuilder(); - signalRClient.ReceiveLogAllLogAction = (line) => - { - logOutput.AppendLine(line); - }; - - var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); - Assert.NotEmpty(getRecommendationOutput.Recommendations); + await SetupSignalRConnection(baseUrl, sessionId, logOutput); - var appRunnerRecommendation = getRecommendationOutput.Recommendations.FirstOrDefault(x => string.Equals(x.RecipeId, "AspNetAppAppRunner")); - Assert.NotNull(appRunnerRecommendation); - - await restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput - { - NewDeploymentName = _stackName, - NewDeploymentRecipeId = appRunnerRecommendation.RecipeId - }); + var appRunnerRecommendation = await GetRecommendationsAndSelectAppRunner(restClient, sessionId); var vpcResources = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.VpcId"); var subnetsResourcesEmpty = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.Subnets"); @@ -173,6 +151,45 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() } } + [Fact] + public async Task GetAppRunnerConfigSettings_TypeHintData() + { + _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; + + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var portNumber = 4002; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(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 WaitTillServerModeReady(restClient); + + var sessionId = await StartDeploymentSession(restClient, projectPath); + + var logOutput = new StringBuilder(); + await SetupSignalRConnection(baseUrl, sessionId, logOutput); + + await GetRecommendationsAndSelectAppRunner(restClient, sessionId); + + var configSettings = restClient.GetConfigSettingsAsync(sessionId); + Assert.NotEmpty(configSettings.Result.OptionSettings); + var iamRoleSetting = Assert.Single(configSettings.Result.OptionSettings, o => o.Id == "ApplicationIAMRole"); + Assert.NotEmpty(iamRoleSetting.TypeHintData); + Assert.Equal("tasks.apprunner.amazonaws.com", iamRoleSetting.TypeHintData[nameof(IAMRoleTypeHintData.ServicePrincipal)]); + } + finally + { + cancelSource.Cancel(); + _stackName = null; + } + } private async Task WaitTillServerModeReady(RestAPIClient restApiClient) { @@ -197,6 +214,45 @@ private async Task GetAppSettingsFromCFTemplate(string return await templateMetadataReader.LoadCloudApplicationMetadata(stackName); } + private async Task StartDeploymentSession(RestAPIClient restClient, string projectPath) + { + var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput + { + AwsRegion = _awsRegion, + ProjectPath = projectPath + }); + + var sessionId = startSessionOutput.SessionId; + Assert.NotNull(sessionId); + return sessionId; + } + + private static async Task SetupSignalRConnection(string baseUrl, string sessionId, StringBuilder logOutput) + { + var signalRClient = new DeploymentCommunicationClient(baseUrl); + await signalRClient.JoinSession(sessionId); + + signalRClient.ReceiveLogAllLogAction = (line) => { logOutput.AppendLine(line); }; + } + + private async Task GetRecommendationsAndSelectAppRunner(RestAPIClient restClient, string sessionId) + { + var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); + Assert.NotEmpty(getRecommendationOutput.Recommendations); + + var appRunnerRecommendation = + getRecommendationOutput.Recommendations.FirstOrDefault(x => string.Equals(x.RecipeId, "AspNetAppAppRunner")); + Assert.NotNull(appRunnerRecommendation); + + await restClient.SetDeploymentTargetAsync(sessionId, + new SetDeploymentTargetInput + { + NewDeploymentName = _stackName, + NewDeploymentRecipeId = appRunnerRecommendation.RecipeId + }); + return appRunnerRecommendation; + } + public void Dispose() { Dispose(true); From dbebe5e7880fa294f7b03f921e8f88b323a183a5 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Mon, 18 Apr 2022 10:49:17 -0400 Subject: [PATCH 09/17] fix: Change Elastic Beanstalk ApplicationIAMRole service principal to ec2.amazonaws.com --- .../RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index 04b9e25c5..c9f6b0aed 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -213,7 +213,7 @@ "Type": "Object", "TypeHint": "IAMRole", "TypeHintData": { - "ServicePrincipal": "elasticbeanstalk.amazonaws.com" + "ServicePrincipal": "ec2.amazonaws.com" }, "AdvancedSetting": false, "Updatable": false, @@ -234,7 +234,7 @@ "Type": "String", "TypeHint": "ExistingIAMRole", "TypeHintData": { - "ServicePrincipal": "elasticbeanstalk.amazonaws.com" + "ServicePrincipal": "ec2.amazonaws.com" }, "AdvancedSetting": false, "Updatable": false, From ae2ec145a16911cb124040de53846de82298a412 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Mon, 18 Apr 2022 12:50:09 -0400 Subject: [PATCH 10/17] feat: Add service IAM role to Elastic Beanstalk deployment configuration --- .../AspNetAppAppRunner/Generated/Recipe.cs | 1 - .../Generated/Configurations/Configuration.cs | 7 +++ .../Generated/Recipe.cs | 31 ++++++++--- .../ASP.NETAppElasticBeanstalk.recipe | 51 +++++++++++++++++++ 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs index e49a204fa..ddff76571 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/Generated/Recipe.cs @@ -14,7 +14,6 @@ using CfnService = Amazon.CDK.AWS.AppRunner.CfnService; using CfnServiceProps = Amazon.CDK.AWS.AppRunner.CfnServiceProps; using Constructs; -using System.Linq; using System.Collections.Generic; // This is a generated file from the original deployment recipe. It is recommended to not modify this file in order 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 f6db3921d..438a23c79 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Configurations/Configuration.cs @@ -18,6 +18,11 @@ public partial class Configuration /// 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. /// @@ -105,6 +110,7 @@ public Configuration() public Configuration( IAMRoleConfiguration applicationIAMRole, + IAMRoleConfiguration serviceIAMRole, string instanceType, BeanstalkEnvironmentConfiguration beanstalkEnvironment, BeanstalkApplicationConfiguration beanstalkApplication, @@ -122,6 +128,7 @@ public Configuration( string enhancedHealthReporting = Recipe.ENHANCED_HEALTH_REPORTING) { ApplicationIAMRole = applicationIAMRole; + ServiceIAMRole = serviceIAMRole; InstanceType = instanceType; BeanstalkEnvironment = beanstalkEnvironment; BeanstalkApplication = beanstalkApplication; diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs index fe1d97897..1b76cfc0e 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Generated/Recipe.cs @@ -96,6 +96,27 @@ private void ConfigureIAM(Configuration settings) 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 void ConfigureApplication(Configuration settings) @@ -221,14 +242,8 @@ private void ConfigureBeanstalkEnvironment(Configuration settings) if (settings.ElasticBeanstalkManagedPlatformUpdates.ManagedActionsEnabled) { - BeanstalkServiceRole = new Role(this, nameof(BeanstalkServiceRole), InvokeCustomizeCDKPropsEvent(nameof(BeanstalkServiceRole), this, new RoleProps - { - AssumedBy = new ServicePrincipal("elasticbeanstalk.amazonaws.com"), - ManagedPolicies = new[] - { - ManagedPolicy.FromAwsManagedPolicyName("AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy") - } - })); + if (BeanstalkServiceRole == null) + throw new InvalidOrMissingConfigurationException("The Elastic Beanstalk service role cannot be null"); optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index c9f6b0aed..f92bc4132 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -257,6 +257,57 @@ } ] }, + { + "Id": "ServiceIAMRole", + "Name": "Service IAM Role", + "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}:.+", + "AllowEmptyString": true, + "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", From e9a1a38abe0ffcd20ba9d2bb03a8b874f06a4108 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 15 Apr 2022 14:48:22 -0400 Subject: [PATCH 11/17] fix: name generation reverses suffixed digits and fails over 100 --- .../CloudApplicationNameGenerator.cs | 9 +++--- .../CloudApplicationNameGeneratorTests.cs | 31 +++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs index 0097cd088..dd7cc1aa8 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs @@ -67,18 +67,19 @@ public string GenerateValidName(ProjectDefinition target, List // make sure the recommendation doesn't exist already in existingApplications var recommendation = recommendedPrefix; - var suffix = 0; + var suffixString = ""; var recommendationCharArray = recommendation.ToCharArray(); for (var i = recommendationCharArray.Length - 1; i >= 0; i--) { if (char.IsDigit(recommendationCharArray[i])) - suffix = suffix * 10 + (recommendationCharArray[i] - '0'); + suffixString = $"{recommendationCharArray[i]}{suffixString}"; else break; } - var prefix = suffix != 0 ? recommendation[..^suffix.ToString().Length] : recommendedPrefix; - while (suffix < 100) + var prefix = !string.IsNullOrEmpty(suffixString) ? recommendation[..^suffixString.Length] : recommendedPrefix; + var suffix = !string.IsNullOrEmpty(suffixString) ? int.Parse(suffixString): 0; + while (suffix < int.MaxValue) { if (existingApplications.All(x => x.Name != recommendation) && IsValidName(recommendation)) return recommendation; diff --git a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs index 4fe2c339e..d5f00e5e8 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs @@ -106,13 +106,12 @@ public async Task SuggestsValidNameAndRespectsExistingApplications() recommendation.ShouldEqual(expectedRecommendation); } - [Fact] - public async Task SuggestsValidNameAndRespectsExistingApplications_ProjectWithNumber() + [Theory] + [InlineData("SuperTest", "SuperTest1")] + [InlineData("SuperTest1", "SuperTest2")] + [InlineData("SuperTest2022", "SuperTest2023")] + public async Task SuggestsValidNameAndRespectsExistingApplications_ProjectWithNumber(string projectFile, string expectedRecommendation) { - // ARRANGE - var projectFile = "SuperTest1"; - var expectedRecommendation = $"SuperTest2"; - var projectPath = _fakeFileManager.AddEmptyProjectFile($"c:\\{projectFile}.csproj"); var projectDefinition = await _projectDefinitionParser.Parse(projectPath); @@ -129,6 +128,26 @@ public async Task SuggestsValidNameAndRespectsExistingApplications_ProjectWithNu recommendation.ShouldEqual(expectedRecommendation); } + [Fact] + public async Task SuggestsValidNameAndRespectsExistingApplications_NoExistingCloudApplication() + { + // ARRANGE + var projectFile = "SuperTest"; + var expectedRecommendation = $"{projectFile}"; + + var projectPath = _fakeFileManager.AddEmptyProjectFile($"c:\\{projectFile}.csproj"); + + var projectDefinition = await _projectDefinitionParser.Parse(projectPath); + + var existingApplication = new List (); + + // ACT + var recommendation = _cloudApplicationNameGenerator.GenerateValidName(projectDefinition, existingApplication); + + // ASSERT + recommendation.ShouldEqual(expectedRecommendation); + } + [Fact] public async Task SuggestsValidNameAndRespectsExistingApplications_MultipleProjectWithNumber() { From 5734190f2774b489c66da39584789732d9c312a6 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 8 Apr 2022 14:08:41 -0400 Subject: [PATCH 12/17] fix: CDK not cleaning up resources after bootstrap failure --- src/AWS.Deploy.Constants/CDK.cs | 5 + .../AWS.Deploy.Orchestration.csproj | 1 + .../CDK/CDKBootstrapTemplate.yaml | 522 ++++++++++++++++++ .../CDK/CDKManager.cs | 10 + .../CdkProjectHandler.cs | 3 +- 5 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml diff --git a/src/AWS.Deploy.Constants/CDK.cs b/src/AWS.Deploy.Constants/CDK.cs index 2dc62315f..12376c710 100644 --- a/src/AWS.Deploy.Constants/CDK.cs +++ b/src/AWS.Deploy.Constants/CDK.cs @@ -22,5 +22,10 @@ internal static class CDK /// 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"); } } diff --git a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj index a8856f7f4..d335de701 100644 --- a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj +++ b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj @@ -38,6 +38,7 @@ + diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml b/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml new file mode 100644 index 000000000..82a94b97d --- /dev/null +++ b/src/AWS.Deploy.Orchestration/CDK/CDKBootstrapTemplate.yaml @@ -0,0 +1,522 @@ +Description: This stack includes resources needed to deploy AWS CDK apps into this environment +Parameters: + TrustedAccounts: + Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment + Default: "" + Type: CommaDelimitedList + TrustedAccountsForLookup: + Description: List of AWS accounts that are trusted to look up values in this environment + Default: "" + Type: CommaDelimitedList + CloudFormationExecutionPolicies: + Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role + Default: "" + Type: CommaDelimitedList + FileAssetsBucketName: + Description: The name of the S3 bucket used for file assets + Default: "" + Type: String + FileAssetsBucketKmsKeyId: + Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed S3 key, or the ID/ARN of an existing key. + Default: "" + Type: String + ContainerAssetsRepositoryName: + Description: A user-provided custom name to use for the container assets ECR repository + Default: "" + Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String + AllowedPattern: "[A-Za-z0-9_-]{1,10}" + ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters + PublicAccessBlockConfiguration: + Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration + Default: "true" + Type: String + AllowedValues: + - "true" + - "false" +Conditions: + HasTrustedAccounts: + Fn::Not: + - Fn::Equals: + - "" + - Fn::Join: + - "" + - Ref: TrustedAccounts + HasTrustedAccountsForLookup: + Fn::Not: + - Fn::Equals: + - "" + - Fn::Join: + - "" + - Ref: TrustedAccountsForLookup + HasCloudFormationExecutionPolicies: + Fn::Not: + - Fn::Equals: + - "" + - Fn::Join: + - "" + - Ref: CloudFormationExecutionPolicies + HasCustomFileAssetsBucketName: + Fn::Not: + - Fn::Equals: + - "" + - Ref: FileAssetsBucketName + CreateNewKey: + Fn::Equals: + - "" + - Ref: FileAssetsBucketKmsKeyId + UseAwsManagedKey: + Fn::Equals: + - AWS_MANAGED_KEY + - Ref: FileAssetsBucketKmsKeyId + HasCustomContainerAssetsRepositoryName: + Fn::Not: + - Fn::Equals: + - "" + - Ref: ContainerAssetsRepositoryName + UsePublicAccessBlockConfiguration: + Fn::Equals: + - "true" + - Ref: PublicAccessBlockConfiguration +Resources: + FileAssetsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Resource: "*" + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: "*" + Resource: "*" + Condition: + StringEquals: + kms:CallerAccount: + Ref: AWS::AccountId + kms:ViaService: + - Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + Fn::Sub: ${FilePublishingRole.Arn} + Resource: "*" + Condition: CreateNewKey + FileAssetsBucketEncryptionKeyAlias: + Condition: CreateNewKey + Type: AWS::KMS::Alias + Properties: + AliasName: + Fn::Sub: alias/cdk-${Qualifier}-assets-key + TargetKeyId: + Ref: FileAssetsBucketEncryptionKey + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::If: + - HasCustomFileAssetsBucketName + - Fn::Sub: ${FileAssetsBucketName} + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: aws:kms + KMSMasterKeyID: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::If: + - UseAwsManagedKey + - Ref: AWS::NoValue + - Fn::Sub: ${FileAssetsBucketKmsKeyId} + PublicAccessBlockConfiguration: + Fn::If: + - UsePublicAccessBlockConfiguration + - BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + - Ref: AWS::NoValue + VersioningConfiguration: + Status: Enabled + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + StagingBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: + Ref: StagingBucket + PolicyDocument: + Id: AccessControl + Version: "2012-10-17" + Statement: + - Sid: AllowSSLRequestsOnly + Action: s3:* + Effect: Deny + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + Condition: + Bool: + aws:SecureTransport: "false" + Principal: "*" + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + ImageScanningConfiguration: + ScanOnPush: true + RepositoryName: + Fn::If: + - HasCustomContainerAssetsRepositoryName + - Fn::Sub: ${ContainerAssetsRepositoryName} + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - Sid: LambdaECRImageRetrievalPolicy + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Condition: + StringLike: + aws:sourceArn: + Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:* + FilePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: file-publishing + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: image-publishing + LookupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccountsForLookup + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess + Policies: + - PolicyDocument: + Statement: + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt + Resource: "*" + Version: "2012-10-17" + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup + FilePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:GetEncryptionConfiguration + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + Effect: Allow + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: "2012-10-17" + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:BatchCheckLayerAvailability + - ecr:DescribeRepositories + - ecr:DescribeImages + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Resource: + Fn::Sub: ${ContainerAssetsRepository.Arn} + Effect: Allow + - Action: + - ecr:GetAuthorizationToken + Resource: "*" + Effect: Allow + Version: "2012-10-17" + Roles: + - Ref: ImagePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + DeploymentActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + - cloudformation:ExecuteChangeSet + - cloudformation:CreateStack + - cloudformation:UpdateStack + Resource: "*" + - Sid: PipelineCrossAccountArtifactsBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:Abort* + - s3:DeleteObject* + - s3:PutObject* + Resource: "*" + Condition: + StringNotEquals: + s3:ResourceAccount: + Ref: AWS::AccountId + - Sid: PipelineCrossAccountArtifactsKey + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Resource: "*" + Condition: + StringEquals: + kms:ViaService: + Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: iam:PassRole + Resource: + Fn::Sub: ${CloudFormationExecutionRole.Arn} + Effect: Allow + - Sid: CliPermissions + Action: + - cloudformation:DescribeStackEvents + - cloudformation:GetTemplate + - cloudformation:DeleteStack + - cloudformation:UpdateTerminationProtection + - sts:GetCallerIdentity + - cloudformation:GetTemplateSummary + Resource: "*" + Effect: Allow + - Sid: CliStagingBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + - Sid: ReadVersion + Effect: Allow + Action: + - ssm:GetParameter + Resource: + - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion} + Version: "2012-10-17" + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: deploy + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + Fn::If: + - HasCloudFormationExecutionPolicies + - Ref: CloudFormationExecutionPolicies + - Fn::If: + - HasTrustedAccounts + - Ref: AWS::NoValue + - - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: /cdk-bootstrap/${Qualifier}/version + Value: "12" +Outputs: + BucketName: + Description: The name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: ${StagingBucket} + BucketDomainName: + Description: The domain name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: ${StagingBucket.RegionalDomainName} + FileAssetKeyArn: + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) + Value: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::Sub: ${FileAssetsBucketKmsKeyId} + Export: + Name: + Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: ${ContainerAssetsRepository} + BootstrapVersion: + Description: The version of the bootstrap resources that are currently mastered in this stack + Value: + Fn::GetAtt: + - CdkBootstrapVersion + - Value + diff --git a/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs b/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs index 1ea4131ea..b2e0f5370 100644 --- a/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs +++ b/src/AWS.Deploy.Orchestration/CDK/CDKManager.cs @@ -3,8 +3,10 @@ using System; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; +using AWS.Deploy.Common.Extensions; using AWS.Deploy.Orchestration.Utilities; namespace AWS.Deploy.Orchestration.CDK @@ -34,6 +36,8 @@ public class CDKManager : ICDKManager private readonly INPMPackageInitializer _npmPackageInitializer; private readonly IOrchestratorInteractiveService _interactiveService; + private const string TemplateIdentifier = "AWS.Deploy.Orchestration.CDK.CDKBootstrapTemplate.yaml"; + public CDKManager(ICDKInstaller cdkInstaller, INPMPackageInitializer npmPackageInitializer, IOrchestratorInteractiveService interactiveService) { _cdkInstaller = cdkInstaller; @@ -47,6 +51,12 @@ public async Task EnsureCompatibleCDKExists(string workingDirectory, Version cdk try { + // 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 cdkBootstrapTemplateFile.WriteAsync(cdkBootstrapTemplate); + var installedCdkVersion = await _cdkInstaller.GetVersion(workingDirectory); if (installedCdkVersion.Success && installedCdkVersion.Result?.CompareTo(cdkVersion) >= 0) { diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 0cb479fdc..bddc875f8 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.CloudFormation; using AWS.Deploy.Common; +using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; @@ -105,7 +106,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); // 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}\"", + 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}\"", workingDirectory: cdkProjectPath, needAwsCredentials: true, redirectIO: true, From 0b3a5786f37174e52eb648542746176e1d1df58b Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 19 Apr 2022 13:25:25 -0400 Subject: [PATCH 13/17] chore: set up documentation CI/CD process --- .github/workflows/doc-builder.yml | 20 ++++++++ THIRD_PARTY_LICENSES | 4 +- mkdocs.yml | 47 +++++++++++++++++++ site/content/contributing.md | 21 +++++++++ site/content/docs/aws-computes/app-runner.md | 1 + site/content/docs/aws-computes/beanstalk.md | 1 + site/content/docs/aws-computes/ecs.md | 1 + site/content/docs/aws-computes/s3.md | 1 + .../docs/features/autogen-dockerfile.md | 1 + .../docs/features/deployment-project.md | 1 + site/content/docs/features/recipe.md | 1 + .../docs/features/recommendation-engine.md | 1 + .../docs/getting-started/installation.md | 1 + .../docs/getting-started/pre-requisites.md | 1 + .../docs/tutorials/automate-deployments.md | 1 + .../docs/tutorials/delete-deployment.md | 1 + .../docs/tutorials/deploy-blazorapp.md | 1 + .../docs/tutorials/deploy-console-service.md | 1 + .../docs/tutorials/deploy-console-task.md | 1 + site/content/docs/tutorials/deploy-webapp.md | 1 + .../docs/tutorials/list-deployments.md | 1 + site/content/docs/tutorials/push-image-ecr.md | 1 + site/content/index.md | 1 + site/content/troubleshooting-guide.md | 1 + 24 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/doc-builder.yml create mode 100644 mkdocs.yml create mode 100644 site/content/contributing.md create mode 100644 site/content/docs/aws-computes/app-runner.md create mode 100644 site/content/docs/aws-computes/beanstalk.md create mode 100644 site/content/docs/aws-computes/ecs.md create mode 100644 site/content/docs/aws-computes/s3.md create mode 100644 site/content/docs/features/autogen-dockerfile.md create mode 100644 site/content/docs/features/deployment-project.md create mode 100644 site/content/docs/features/recipe.md create mode 100644 site/content/docs/features/recommendation-engine.md create mode 100644 site/content/docs/getting-started/installation.md create mode 100644 site/content/docs/getting-started/pre-requisites.md create mode 100644 site/content/docs/tutorials/automate-deployments.md create mode 100644 site/content/docs/tutorials/delete-deployment.md create mode 100644 site/content/docs/tutorials/deploy-blazorapp.md create mode 100644 site/content/docs/tutorials/deploy-console-service.md create mode 100644 site/content/docs/tutorials/deploy-console-task.md create mode 100644 site/content/docs/tutorials/deploy-webapp.md create mode 100644 site/content/docs/tutorials/list-deployments.md create mode 100644 site/content/docs/tutorials/push-image-ecr.md create mode 100644 site/content/index.md create mode 100644 site/content/troubleshooting-guide.md diff --git a/.github/workflows/doc-builder.yml b/.github/workflows/doc-builder.yml new file mode 100644 index 000000000..c2239ccfb --- /dev/null +++ b/.github/workflows/doc-builder.yml @@ -0,0 +1,20 @@ +name: Publish docs via GitHub Pages +on: + push: + branches: + - main + - dev + + # Allow the workflow to be triggered also manually. + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - run: pip install mkdocs-material==8.2.9 + - run: mkdocs gh-deploy --force \ No newline at end of file diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index 4f51df975..f43e085e0 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -350,7 +350,9 @@ Copyright (c) 2016 Richard Morris Copyright (c) 2016 Richard Morris ** Swashbuckle.AspNetCore.SwaggerGen ; version 6.1.2 -- https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerGen/ Copyright (c) 2016 Richard Morris - +** mkdocs-material; version 8.2.9 -- https://pypi.org/project/mkdocs-material/ +Copyright (c) 2016-2022 Martin Donath + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..cfba5cf96 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,47 @@ +site_name: AWS .NET deployment tool +site_description: 'Deploy .NET applications on AWS' +site_url: 'https://aws.github.io/aws-dotnet-deploy/' +repo_name: 'aws/aws-dotnet-deploy' +repo_url: 'https://github.com/aws/aws-dotnet-deploy' +edit_uri: 'edit/main/site/content' +copyright: '© 2022, Amazon Web Services, Inc. or its affiliates. All rights reserved.' +docs_dir: 'site/content' +site_dir: 'docs' + +nav: + - Overview: index.md + - Documentation: + - Getting Started: + - Pre-requisites: docs/getting-started/pre-requisites.md + - Installing the tool: docs/getting-started/installation.md + - Supported AWS computes: + - Amazon ECS: docs/aws-computes/ecs.md + - AWS Elastic Beanstalk: docs/aws-computes/beanstalk.md + - AWS App Runner: docs/aws-computes/app-runner.md + - Amazon S3: docs/aws-computes/s3.md + - Features: + - Recipe: docs/features/recipe.md + - Recommendation Engine: docs/features/recommendation-engine.md + - Deployment Project: docs/features/deployment-project.md + - Auto-generated Dockerfile: docs/features/autogen-dockerfile.md + - Tutorials: + - Deploying ASP.NET Core Application: docs/tutorials/deploy-webapp.md + - Deploying Blazor WebAssembly Application: docs/tutorials/deploy-blazorapp.md + - Deploying Console Service: docs/tutorials/deploy-console-service.md + - Deploying Console Task: docs/tutorials/deploy-console-task.md + - Pushing Image to ECS: docs/tutorials/push-image-ecr.md + - Automating Deployments: docs/tutorials/automate-deployments.md + - Listing Deployments: docs/tutorials/list-deployments.md + - Deleting Deployment: docs/tutorials/delete-deployment.md + - Troubleshooting Guide: troubleshooting-guide.md + - Contributing to the project: contributing.md + +theme: + name: material + palette: + primary: white + font: false + language: en + features: + - tabs + - instant \ No newline at end of file diff --git a/site/content/contributing.md b/site/content/contributing.md new file mode 100644 index 000000000..77ec2025a --- /dev/null +++ b/site/content/contributing.md @@ -0,0 +1,21 @@ +## Build and Test Documentation + +### Install Material for MkDocs +Material for MkDocs is a theme for MkDocs, a static site generator geared towards (technical) project documentation. If you're familiar with Python, you can install Material for MkDocs with pip, the Python package manager. + +``` +pip install mkdocs-material +``` +For, other installation options [see here](https://squidfunk.github.io/mkdocs-material/getting-started/) + +### Deploying to a Local Server +MkDocs comes with a built-in dev-server that lets you preview your documentation as you work on it. + +From the root of the project repository, run the following command: +``` +mkdocs serve +``` + +Paste the link to the local server on a web browser to look at the documentation. + +The dev-server also supports auto-reloading, and will rebuild your documentation whenever anything in the configuration file, documentation directory, or theme directory changes. \ No newline at end of file diff --git a/site/content/docs/aws-computes/app-runner.md b/site/content/docs/aws-computes/app-runner.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/aws-computes/app-runner.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/aws-computes/beanstalk.md b/site/content/docs/aws-computes/beanstalk.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/aws-computes/beanstalk.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/aws-computes/ecs.md b/site/content/docs/aws-computes/ecs.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/aws-computes/ecs.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/aws-computes/s3.md b/site/content/docs/aws-computes/s3.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/aws-computes/s3.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/features/autogen-dockerfile.md b/site/content/docs/features/autogen-dockerfile.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/features/autogen-dockerfile.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/features/deployment-project.md b/site/content/docs/features/deployment-project.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/features/deployment-project.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/features/recipe.md b/site/content/docs/features/recipe.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/features/recipe.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/features/recommendation-engine.md b/site/content/docs/features/recommendation-engine.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/features/recommendation-engine.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/getting-started/installation.md b/site/content/docs/getting-started/installation.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/getting-started/installation.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/getting-started/pre-requisites.md b/site/content/docs/getting-started/pre-requisites.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/getting-started/pre-requisites.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/automate-deployments.md b/site/content/docs/tutorials/automate-deployments.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/automate-deployments.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/delete-deployment.md b/site/content/docs/tutorials/delete-deployment.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/delete-deployment.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/deploy-blazorapp.md b/site/content/docs/tutorials/deploy-blazorapp.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/deploy-blazorapp.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/deploy-console-service.md b/site/content/docs/tutorials/deploy-console-service.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/deploy-console-service.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/deploy-console-task.md b/site/content/docs/tutorials/deploy-console-task.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/deploy-console-task.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/deploy-webapp.md b/site/content/docs/tutorials/deploy-webapp.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/deploy-webapp.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/list-deployments.md b/site/content/docs/tutorials/list-deployments.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/list-deployments.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/docs/tutorials/push-image-ecr.md b/site/content/docs/tutorials/push-image-ecr.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/docs/tutorials/push-image-ecr.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/index.md b/site/content/index.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/index.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file diff --git a/site/content/troubleshooting-guide.md b/site/content/troubleshooting-guide.md new file mode 100644 index 000000000..85a4aa015 --- /dev/null +++ b/site/content/troubleshooting-guide.md @@ -0,0 +1 @@ +### TODO \ No newline at end of file From a5accf846b2fda6ff8f28dd42211097ac2aeaddb Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 22 Apr 2022 09:59:49 -0400 Subject: [PATCH 14/17] fix: add missing Type attribute to option setting in AppRunner recipe --- .../RecipeDefinitions/ASP.NETAppAppRunner.recipe | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 7dfedd094..3f9f03fad 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -402,6 +402,7 @@ "Id": "VpcId", "Name": "VPC ID", "Description": "A list of VPC IDs that App Runner should use when it associates your service with a custom Amazon VPC.", + "Type": "String", "TypeHint": "ExistingVpc", "DefaultValue": null, "AdvancedSetting": false, From d3676b966a7ca91b37bc7d87c8536e9a3328ae69 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Thu, 21 Apr 2022 17:15:20 -0700 Subject: [PATCH 15/17] fix: ask for "Create New" even if options to select is empty ## Motivation `AskUserToChooseOrCreateNew` allows users to select an existing option or create new one. When there are no options to select, **Create New** is not shown and CLI returns to previous screen with default selection **Create New** which is correct in theory because that's the only action user can take. However, this is not a good UX. ## Changes The check to display selection is moved to (Options + ** Empty** + **Create New**) combination, i.e. if any of the options are present, user will be asked to pick one. If none of the exists, the there is modeling error and should be handled at recipe level. ## Result In cases like selecting ECS application role and there doesn't exist any compatible role in AWS account, customer is still asked to picked Create New. --- src/AWS.Deploy.CLI/ConsoleUtilities.cs | 25 +++++++++++---- .../ConsoleUtilitiesTests.cs | 31 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/AWS.Deploy.CLI/ConsoleUtilities.cs b/src/AWS.Deploy.CLI/ConsoleUtilities.cs index 747f42a7f..33049b0cf 100644 --- a/src/AWS.Deploy.CLI/ConsoleUtilities.cs +++ b/src/AWS.Deploy.CLI/ConsoleUtilities.cs @@ -268,14 +268,27 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str defaultValue = userInputConfiguration.CreateNew || !options.Any() ? createNewLabel : userInputConfiguration.DisplaySelector(options.First()); } - if (optionStrings.Any()) + var displayOptionStrings = new List(); + + // add empty option at the top if configured + if (userInputConfiguration.EmptyOption) { - var displayOptionStrings = new List(optionStrings); - if (userInputConfiguration.EmptyOption) - displayOptionStrings.Insert(0, Constants.CLI.EMPTY_LABEL); - if (userInputConfiguration.CreateNew) - displayOptionStrings.Add(createNewLabel); + displayOptionStrings.Add(Constants.CLI.EMPTY_LABEL); + } + // add all the options, this can be empty list if there are no options + // e.g. selecting a role for a service when there are no roles with a service principal + displayOptionStrings.AddRange(optionStrings); + + // add create new option at the bottom if configured + if (userInputConfiguration.CreateNew) + { + displayOptionStrings.Add(createNewLabel); + } + + // if list contains any options, ask user to choose one + if (displayOptionStrings.Any()) + { var selectedString = AskUserToChoose(displayOptionStrings, title, defaultValue, defaultChoosePrompt); if (selectedString == Constants.CLI.EMPTY_LABEL) diff --git a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs index be6ed7ebb..1c2f04b47 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs @@ -156,6 +156,37 @@ public void AskUserToChooseOrCreateNewPickExisting() Assert.Null(userResponse.NewName); } + [Fact] + public void AskUserToChooseOrCreateNewNoOptions() + { + var interactiveServices = new TestToolInteractiveServiceImpl(new List + { + "1" + }); + var consoleUtilities = new ConsoleUtilities(interactiveServices, _directoryManager); + var userInputConfiguration = new UserInputConfiguration( + option => option.DisplayName, + option => option.DisplayName, + option => option.Identifier.Equals("Identifier2"), + "NewIdentifier") + { + AskNewName = false, + CreateNew = true, + EmptyOption = false + }; + var userResponse = consoleUtilities.AskUserToChooseOrCreateNew(Array.Empty(), "Title", userInputConfiguration); + + Assert.Equal("Title", interactiveServices.OutputMessages[0]); + + Assert.True(interactiveServices.OutputContains("Title")); + Assert.True(interactiveServices.OutputContains("1: *** Create new *** (default)")); + + Assert.True(userResponse.CreateNew); + Assert.Null(userResponse.SelectedOption); + Assert.Null(userResponse.NewName); + Assert.False(userResponse.IsEmpty); + } + [Fact] public void AskUserToChooseStringsPickDefault() { From e4d53c0ba6483f136f8d2d7eb3a142b0a0cbe3ec Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Thu, 14 Apr 2022 14:10:59 -0400 Subject: [PATCH 16/17] chore: add banner to notify of NuGet package name change --- src/AWS.Deploy.CLI/App.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/AWS.Deploy.CLI/App.cs b/src/AWS.Deploy.CLI/App.cs index 98bd77da1..1db5715fb 100644 --- a/src/AWS.Deploy.CLI/App.cs +++ b/src/AWS.Deploy.CLI/App.cs @@ -31,6 +31,12 @@ public async Task Run(string[] args) _toolInteractiveService.WriteLine("AWS .NET deployment tool for deploying .NET Core applications to AWS."); _toolInteractiveService.WriteLine("Project Home: https://github.com/aws/aws-dotnet-deploy"); _toolInteractiveService.WriteLine(string.Empty); + _toolInteractiveService.WriteLine("---------------------------------------------------------------------"); + _toolInteractiveService.WriteLine("Deprecation Notice: The name of the AWS .NET deployment tool NuGet package will change from 'AWS.Deploy.CLI' to 'AWS.Deploy.Tools'. " + + "In order to keep receiving updates, make sure to uninstall the current dotnet tool 'AWS.Deploy.CLI' and install 'AWS.Deploy.Tools'. " + + "The NuGet package 'AWS.Deploy.CLI' will no longer receive any updates, so please make sure to install the new package 'AWS.Deploy.Tools'."); + _toolInteractiveService.WriteLine("---------------------------------------------------------------------"); + _toolInteractiveService.WriteLine(string.Empty); // if user didn't specify a command, default to help if (args.Length == 0) From 164741a784a2f0f915a7455a35cf3358e2ed8cfb Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 21 Apr 2022 23:02:02 -0700 Subject: [PATCH 17/17] fix: Prevent non CDK projects from being displayed as an option when creating deployment projects --- src/AWS.Deploy.Orchestration/Orchestrator.cs | 21 ++++++++++++++++++- .../RecommendationTests.cs | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 5ac75fd9c..a0c467b71 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -79,6 +79,11 @@ public Orchestrator(OrchestratorSession session, IList recipeDefinitionP _recipeDefinitionPaths = recipeDefinitionPaths; } + /// + /// Method that generates the list of recommendations to deploy with. + /// + /// + /// public async Task> GenerateDeploymentRecommendations() { if (_recipeDefinitionPaths == null) @@ -95,6 +100,11 @@ public async Task> GenerateDeploymentRecommendations() return await engine.ComputeRecommendations(); } + /// + /// Method to generate the list of recommendations to create deployment projects for. + /// + /// + /// public async Task> GenerateRecommendationsToSaveDeploymentProject() { if (_recipeDefinitionPaths == null) @@ -103,9 +113,18 @@ public async Task> GenerateRecommendationsToSaveDeploymentP throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths, _session); - return await engine.ComputeRecommendations(); + var compatibleRecommendations = await engine.ComputeRecommendations(); + var cdkRecommendations = compatibleRecommendations.Where(x => x.Recipe.DeploymentType == DeploymentTypes.CdkProject).ToList(); + return cdkRecommendations; } + /// + /// Include in the list of recommendations the recipe the deploymentProjectPath implements. + /// + /// + /// + /// + /// public async Task> GenerateRecommendationsFromSavedDeploymentProject(string deploymentProjectPath) { if (_session == null) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index bcf192b8f..baa433a08 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using AWS.Deploy.CLI.Utilities; @@ -39,6 +40,26 @@ public RecommendationTests() _commandLineWrapper = new CommandLineWrapper(_inMemoryInteractiveService); } + [Fact] + public async Task GenerateRecommendationsForDeploymentProject() + { + // ARRANGE + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var orchestrator = await GetOrchestrator(webAppWithDockerFilePath); + await _commandLineWrapper.Run("git init", tempDirectoryPath); + + // ACT + var recommendations = await orchestrator.GenerateRecommendationsToSaveDeploymentProject(); + + // ASSERT + var anyNonCdkRecommendations = recommendations.Where(x => x.Recipe.DeploymentType != DeploymentTypes.CdkProject); + Assert.False(anyNonCdkRecommendations.Any()); + + Assert.NotNull(recommendations.FirstOrDefault(x => x.Recipe.Id == "AspNetAppEcsFargate")); + Assert.NotNull(recommendations.FirstOrDefault(x => x.Recipe.Id == "AspNetAppElasticBeanstalkLinux")); + } + [Fact] public async Task GenerateRecommendationsWithoutCustomRecipes() {