diff --git a/src/AWS.Deploy.CLI/AWSUtilities.cs b/src/AWS.Deploy.CLI/AWSUtilities.cs index 70478a196..5d0621e5a 100644 --- a/src/AWS.Deploy.CLI/AWSUtilities.cs +++ b/src/AWS.Deploy.CLI/AWSUtilities.cs @@ -14,8 +14,8 @@ namespace AWS.Deploy.CLI { public interface IAWSUtilities { - Task ResolveAWSCredentials(string profileName, string? lastUsedProfileName); - string ResolveAWSRegion(string region, string? lastRegionUsed); + Task ResolveAWSCredentials(string? profileName, string? lastUsedProfileName = null); + string ResolveAWSRegion(string? region, string? lastRegionUsed = null); } public class AWSUtilities : IAWSUtilities @@ -29,10 +29,8 @@ public AWSUtilities(IToolInteractiveService toolInteractiveService, IConsoleUtil _consoleUtilities = consoleUtilities; } - public async Task ResolveAWSCredentials(string profileName, string? lastUsedProfileName) + public async Task ResolveAWSCredentials(string? profileName, string? lastUsedProfileName = null) { - - async Task Resolve() { var chain = new CredentialProfileStoreChain(); @@ -113,7 +111,7 @@ private async Task CanLoadCredentials(AWSCredentials credentials) } } - public string ResolveAWSRegion(string region, string? lastRegionUsed) + public string ResolveAWSRegion(string? region, string? lastRegionUsed = null) { if (!string.IsNullOrEmpty(region)) { diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 5a9bff505..8e12d4925 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -14,6 +14,7 @@ using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; +using AWS.Deploy.CLI.Commands.CommandHandlerInput; namespace AWS.Deploy.CLI.Commands { @@ -29,6 +30,8 @@ public class CommandFactory : ICommandFactory private static readonly Option _optionProjectPath = new("--project-path", () => Directory.GetCurrentDirectory(), "Path to the project to deploy."); private static readonly Option _optionStackName = new("--stack-name", "Name the AWS stack to deploy your application to."); private static readonly Option _optionDiagnosticLogging = new(new []{"-d", "--diagnostics"}, "Enable diagnostic output."); + private static readonly Option _optionApply = new("--apply", "Path to the deployment settings file to be applied."); + private static readonly Option _optionDisableInteractive = new(new []{"-s", "--silent" }, "Disable interactivity to deploy without any prompts for user input."); private readonly IToolInteractiveService _toolInteractiveService; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; @@ -110,26 +113,31 @@ private Command BuildDeployCommand() _optionRegion, _optionProjectPath, _optionStackName, + _optionApply, _optionDiagnosticLogging, + _optionDisableInteractive }; - deployCommand.Handler = CommandHandler.Create(async (profile, region, projectPath, stackName, saveCdkProject, diagnostics) => + deployCommand.Handler = CommandHandler.Create(async (DeployCommandHandlerInput input) => { try { - _toolInteractiveService.Diagnostics = diagnostics; + _toolInteractiveService.Diagnostics = input.Diagnostics; + _toolInteractiveService.DisableInteractive = input.Silent; + + var userDeploymentSettings = !string.IsNullOrEmpty(input.Apply) + ? UserDeploymentSettings.ReadSettings(input.Apply) + : null; - var previousSettings = PreviousDeploymentSettings.ReadSettings(projectPath, null); - - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(profile, previousSettings.Profile); - var awsRegion = _awsUtilities.ResolveAWSRegion(region, previousSettings.Region); + var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile ?? userDeploymentSettings?.AWSProfile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? userDeploymentSettings?.AWSRegion); _commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion); _awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion); var systemCapabilities = _systemCapabilityEvaluator.Evaluate(); - var projectDefinition = await _projectParserUtility.Parse(projectPath); + var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? ""); var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); @@ -140,7 +148,7 @@ private Command BuildDeployCommand() callerIdentity.Account) { SystemCapabilities = systemCapabilities, - AWSProfileName = profile + AWSProfileName = input.Profile ?? userDeploymentSettings?.AWSProfile ?? null }; var dockerEngine = new DockerEngine.DockerEngine(projectDefinition); @@ -160,13 +168,13 @@ private Command BuildDeployCommand() _consoleUtilities, session); - await deploy.ExecuteAsync(stackName, saveCdkProject); + await deploy.ExecuteAsync(input.StackName ?? "", input.SaveCdkProject, userDeploymentSettings); return CommandReturnCodes.SUCCESS; } catch (Exception e) when (e.IsAWSDeploymentExpectedException()) { - if (diagnostics) + if (input.Diagnostics) _toolInteractiveService.WriteErrorLine(e.PrettyPrint()); else { @@ -199,16 +207,14 @@ private Command BuildDeleteCommand() _optionDiagnosticLogging, new Argument("deployment-name") }; - deleteCommand.Handler = CommandHandler.Create(async (profile, region, projectPath, deploymentName, diagnostics) => + deleteCommand.Handler = CommandHandler.Create(async (DeleteCommandHandlerInput input) => { try { - _toolInteractiveService.Diagnostics = diagnostics; - - var previousSettings = PreviousDeploymentSettings.ReadSettings(projectPath, null); + _toolInteractiveService.Diagnostics = input.Diagnostics; - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(profile, previousSettings.Profile); - var awsRegion = _awsUtilities.ResolveAWSRegion(region, previousSettings.Region); + var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region); _awsClientFactory.ConfigureAWSOptions(awsOption => { @@ -216,13 +222,20 @@ private Command BuildDeleteCommand() awsOption.Region = RegionEndpoint.GetBySystemName(awsRegion); }); - await new DeleteDeploymentCommand(_awsClientFactory, _toolInteractiveService, _consoleUtilities).ExecuteAsync(deploymentName); + if (string.IsNullOrEmpty(input.DeploymentName)) + { + _toolInteractiveService.WriteErrorLine(string.Empty); + _toolInteractiveService.WriteErrorLine("Deployment name cannot be empty. Please provide a valid deployment name and try again."); + return CommandReturnCodes.USER_ERROR; + } + + await new DeleteDeploymentCommand(_awsClientFactory, _toolInteractiveService, _consoleUtilities).ExecuteAsync(input.DeploymentName); return CommandReturnCodes.SUCCESS; } catch (Exception e) when (e.IsAWSDeploymentExpectedException()) { - if (diagnostics) + if (input.Diagnostics) _toolInteractiveService.WriteErrorLine(e.PrettyPrint()); else { @@ -254,16 +267,14 @@ private Command BuildListCommand() _optionProjectPath, _optionDiagnosticLogging }; - listCommand.Handler = CommandHandler.Create(async (profile, region, projectPath, diagnostics) => + listCommand.Handler = CommandHandler.Create(async (ListCommandHandlerInput input) => { try { - _toolInteractiveService.Diagnostics = diagnostics; - - var previousSettings = PreviousDeploymentSettings.ReadSettings(projectPath, null); + _toolInteractiveService.Diagnostics = input.Diagnostics; - var awsCredentials = await _awsUtilities.ResolveAWSCredentials(profile, previousSettings.Profile); - var awsRegion = _awsUtilities.ResolveAWSRegion(region, previousSettings.Region); + var awsCredentials = await _awsUtilities.ResolveAWSCredentials(input.Profile); + var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region); _awsClientFactory.ConfigureAWSOptions(awsOptions => { @@ -277,7 +288,7 @@ private Command BuildListCommand() } catch (Exception e) when (e.IsAWSDeploymentExpectedException()) { - if (diagnostics) + if (input.Diagnostics) _toolInteractiveService.WriteErrorLine(e.PrettyPrint()); else { @@ -307,12 +318,12 @@ private Command BuildServerModeCommand() new Option(new []{"--encryption-keyinfo-stdin"}, description: "If set the cli reads encryption key info from stdin to use for decryption."), _optionDiagnosticLogging }; - serverModeCommand.Handler = CommandHandler.Create(async (port, parentPid, encryptionKeyInfoStdIn, diagnostics) => + serverModeCommand.Handler = CommandHandler.Create(async (ServerModeCommandHandlerInput input) => { try { - var toolInteractiveService = new ConsoleInteractiveServiceImpl(diagnostics); - var serverMode = new ServerModeCommand(toolInteractiveService, port, parentPid, encryptionKeyInfoStdIn); + var toolInteractiveService = new ConsoleInteractiveServiceImpl(input.Diagnostics); + var serverMode = new ServerModeCommand(toolInteractiveService, input.Port, input.ParentPid, input.EncryptionKeyInfoStdIn); await serverMode.ExecuteAsync(); @@ -320,7 +331,7 @@ private Command BuildServerModeCommand() } catch (Exception e) when (e.IsAWSDeploymentExpectedException()) { - if (diagnostics) + if (input.Diagnostics) _toolInteractiveService.WriteErrorLine(e.PrettyPrint()); else { diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeleteCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeleteCommandHandlerInput.cs new file mode 100644 index 000000000..69e9356cb --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeleteCommandHandlerInput.cs @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AWS.Deploy.CLI.Commands.CommandHandlerInput +{ + public class DeleteCommandHandlerInput + { + public string? Profile { get; set; } + public string? Region { get; set; } + public string? ProjectPath { get; set; } + public string? DeploymentName { get; set; } + public bool Diagnostics { get; set; } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs new file mode 100644 index 000000000..f053112bd --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AWS.Deploy.CLI.Commands.CommandHandlerInput +{ + public class DeployCommandHandlerInput + { + public string? Profile { get; set; } + public string? Region { get; set; } + public string? ProjectPath { get; set; } + public string? StackName { get; set; } + public string? Apply { get; set; } + public bool Diagnostics { get; set; } + public bool Silent { get; set; } + public bool SaveCdkProject { get; set; } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs new file mode 100644 index 000000000..f4a854f43 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ListCommandHandlerInput.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AWS.Deploy.CLI.Commands.CommandHandlerInput +{ + public class ListCommandHandlerInput + { + public string? Profile { get; set; } + public string? Region { get; set; } + public string? ProjectPath { get; set; } + public bool Diagnostics { get; set; } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs new file mode 100644 index 000000000..90203bf98 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/ServerModeCommandHandlerInput.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AWS.Deploy.CLI.Commands.CommandHandlerInput +{ + public class ServerModeCommandHandlerInput + { + public int Port { get; set; } + public int ParentPid { get; set; } + public bool EncryptionKeyInfoStdIn { get; set; } + public bool Diagnostics { get; set; } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index f00606bc5..6da6c4689 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -67,7 +67,7 @@ public DeployCommand( _cdkManager = cdkManager; } - public async Task ExecuteAsync(string stackName, bool saveCdkProject) + public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploymentSettings? userDeploymentSettings = null) { var orchestrator = new Orchestrator( @@ -96,10 +96,9 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject) throw new InvalidCliArgumentException("Found invalid CLI arguments"); } - var cloudApplicationName = - !string.IsNullOrEmpty(stackName) - ? stackName - : AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); + _toolInteractiveService.WriteLine(); + + var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, deployedApplications); var deployedApplication = deployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName)); @@ -115,7 +114,14 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject) selectedRecommendation = recommendations.FirstOrDefault(x => string.Equals(x.Recipe.Id, deployedApplication.RecipeId, StringComparison.InvariantCultureIgnoreCase)); if (selectedRecommendation == null) - throw new FailedToCompatibleRecipeException("A compatible recipe was not found for the deployed application."); + throw new FailedToFindCompatibleRecipeException("A compatible recipe was not found for the deployed application."); + + if (userDeploymentSettings != null && !string.IsNullOrEmpty(userDeploymentSettings.RecipeId)) + { + if (!string.Equals(userDeploymentSettings.RecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase)) + throw new InvalidUserDeploymentSettingsException("The recipe ID specified as part of the deployment configuration file" + + " does not match the original recipe used to deploy the application stack."); + } selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); @@ -147,7 +153,7 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject) } else { - selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations); + selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); } // Apply the user enter project name to the recommendation so that any default settings based on project name are applied. @@ -180,7 +186,15 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject) var configurableOptionSettings = selectedRecommendation.Recipe.OptionSettings.Union(deploymentBundleDefinition.Parameters); - await ConfigureDeployment(selectedRecommendation, configurableOptionSettings, false); + if (userDeploymentSettings != null) + { + ConfigureDeployment(selectedRecommendation, deploymentBundleDefinition, userDeploymentSettings); + } + + if (!_toolInteractiveService.DisableInteractive) + { + await ConfigureDeployment(selectedRecommendation, configurableOptionSettings, false); + } var cloudApplication = new CloudApplication(cloudApplicationName, string.Empty); @@ -194,6 +208,126 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject) await orchestrator.DeployRecommendation(cloudApplication, selectedRecommendation); } + /// + /// This method is used to set the values for Option Setting Items when a deployment is being performed using a user specifed config file. + /// + /// The selected recommendation settings used for deployment + /// The container for the deployment bundle used by an application. + /// The deserialized object from the user provided config file. + private void ConfigureDeployment(Recommendation recommendation, DeploymentBundleDefinition deploymentBundleDefinition, UserDeploymentSettings userDeploymentSettings) + { + foreach (var entry in userDeploymentSettings.LeafOptionSettingItems) + { + var optionSettingJsonPath = entry.Key; + var optionSettingValue = entry.Value; + + var isPartOfDeploymentBundle = true; + var optionSetting = deploymentBundleDefinition.Parameters.FirstOrDefault(x => x.Id.Equals(optionSettingJsonPath)); + if (optionSetting == null) + { + optionSetting = recommendation.GetOptionSetting(optionSettingJsonPath); + isPartOfDeploymentBundle = false; + } + + if (!recommendation.IsExistingCloudApplication || optionSetting.Updatable) + { + object settingValue; + try + { + switch (optionSetting.Type) + { + case OptionSettingValueType.String: + settingValue = optionSettingValue; + break; + case OptionSettingValueType.Int: + settingValue = int.Parse(optionSettingValue); + break; + case OptionSettingValueType.Bool: + settingValue = bool.Parse(optionSettingValue); + break; + default: + throw new InvalidOverrideValueException($"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); + } + } + catch (Exception) + { + throw new InvalidOverrideValueException($"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); + } + + optionSetting.SetValueOverride(settingValue); + + if (isPartOfDeploymentBundle) + SetDeploymentBundleOptionSetting(recommendation, optionSetting.Id, settingValue); + } + } + } + + private void SetDeploymentBundleOptionSetting(Recommendation recommendation, string optionSettingId, object settingValue) + { + switch (optionSettingId) + { + case "DockerExecutionDirectory": + new DockerExecutionDirectoryCommand(_consoleUtilities).OverrideValue(recommendation, settingValue.ToString() ?? ""); + break; + case "DockerBuildArgs": + new DockerBuildArgsCommand(_consoleUtilities).OverrideValue(recommendation, settingValue.ToString() ?? ""); + break; + case "DotnetBuildConfiguration": + new DotnetPublishBuildConfigurationCommand(_consoleUtilities).Overridevalue(recommendation, settingValue.ToString() ?? ""); + break; + case "DotnetPublishArgs": + new DotnetPublishArgsCommand(_consoleUtilities).OverrideValue(recommendation, settingValue.ToString() ?? ""); + break; + case "SelfContainedBuild": + new DotnetPublishSelfContainedBuildCommand(_consoleUtilities).OverrideValue(recommendation, (bool)settingValue); + break; + default: + throw new OptionSettingItemDoesNotExistException($"The Option Setting Item { optionSettingId } does not exist."); + } + } + + private string GetCloudApplicationName(string? stackName, UserDeploymentSettings? userDeploymentSettings, List deployedApplications) + { + if (!string.IsNullOrEmpty(stackName)) + return stackName; + + if (userDeploymentSettings == null || string.IsNullOrEmpty(userDeploymentSettings.StackName)) + { + if (_toolInteractiveService.DisableInteractive) + { + var message = "The \"--silent\" CLI argument can only be used if a CDK stack name is provided either via the CLI argument \"--stack-name\" or through a deployment-settings file. " + + "Please provide a stack name and try again"; + throw new InvalidCliArgumentException(message); + } + return AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); + } + + _toolInteractiveService.WriteLine($"Configuring Stack Name with specified value '{userDeploymentSettings.StackName}'."); + return userDeploymentSettings.StackName; + } + + private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDeploymentSettings, List recommendations) + { + if (userDeploymentSettings == null || string.IsNullOrEmpty(userDeploymentSettings.RecipeId)) + { + if (_toolInteractiveService.DisableInteractive) + { + var message = "The \"--silent\" CLI argument can only be used if a deployment recipe is specified as part of the deployement-settings file. " + + "Please provide a deployment recipe and try again"; + throw new InvalidCliArgumentException(message); + } + return _consoleUtilities.AskToChooseRecommendation(recommendations); + } + + Recommendation? selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(userDeploymentSettings.RecipeId)); + + if (selectedRecommendation == null) + throw new InvalidUserDeploymentSettingsException($"The user deployment settings provided contains an invalid value for the property '{nameof(userDeploymentSettings.RecipeId)}'."); + + _toolInteractiveService.WriteLine($"Configuring Recommendation with specified value '{selectedRecommendation.Name}'."); + return selectedRecommendation; + } + private string AskUserForCloudApplicationName(ProjectDefinition project, List existingApplications) { var defaultName = ""; @@ -270,6 +404,14 @@ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendat { while (!await orchestrator.CreateContainerDeploymentBundle(cloudApplication, selectedRecommendation)) { + if (_toolInteractiveService.DisableInteractive) + { + var errorMessage = "Failed to build Docker Image." + Environment.NewLine; + errorMessage += "Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile." + Environment.NewLine; + errorMessage += "Specify a valid Docker execution directory as part of the deployment settings file and try again."; + throw new DockerBuildFailedException(errorMessage); + } + _toolInteractiveService.WriteLine(string.Empty); var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "true"); if (answer == YesNo.Yes) diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs index 77ca5f5b7..734fdd28a 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs @@ -34,6 +34,20 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt return Task.FromResult(settingValue); } + /// + /// This method will be invoked to set the Docker build arguments in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// Arguments to be passed when performing a Docker build + public void OverrideValue(Recommendation recommendation, string dockerBuildArgs) + { + var resultString = ValidateBuildArgs(dockerBuildArgs); + if (!string.IsNullOrEmpty(resultString)) + throw new InvalidOverrideValueException(resultString); + recommendation.DeploymentBundle.DockerBuildArgs = dockerBuildArgs; + } + private string ValidateBuildArgs(string buildArgs) { var argsList = buildArgs.Split(","); diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index 13b1bdbad..644f2f293 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -32,10 +32,24 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt return Task.FromResult(settingValue); } + /// + /// This method will be invoked to set the Docker execution directory in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// The directory specified for Docker execution. + public void OverrideValue(Recommendation recommendation, string executionDirectory) + { + var resultString = ValidateExecutionDirectory(executionDirectory); + if (!string.IsNullOrEmpty(resultString)) + throw new InvalidOverrideValueException(resultString); + recommendation.DeploymentBundle.DockerExecutionDirectory = executionDirectory; + } + private string ValidateExecutionDirectory(string executionDirectory) { if (!string.IsNullOrEmpty(executionDirectory) && !Directory.Exists(executionDirectory)) - return "The directory you specified does not exist."; + return "The directory specified for Docker execution does not exist."; else return ""; } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index 0e12c331c..664b71c5c 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; @@ -25,18 +26,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt allowEmpty: true, resetValue: recommendation.GetOptionSettingDefaultValue(optionSetting) ?? "", // validators: - publishArgs => - (publishArgs.Contains("-o ") || publishArgs.Contains("--output ")) - ? "You must not include -o/--output as an additional argument as it is used internally." - : "", - publishArgs => - (publishArgs.Contains("-c ") || publishArgs.Contains("--configuration ") - ? "You must not include -c/--configuration as an additional argument. You can set the build configuration in the advanced settings." - : ""), - publishArgs => - (publishArgs.Contains("--self-contained") || publishArgs.Contains("--no-self-contained") - ? "You must not include --self-contained/--no-self-contained as an additional argument. You can set the self-contained property in the advanced settings." - : "")) + publishArgs => ValidateDotnetPublishArgs(publishArgs)) .ToString() .Replace("\"", "\"\""); @@ -44,5 +34,35 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt return Task.FromResult(settingValue); } + + /// + /// This method will be invoked to set any additional Dotnet build arguments in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// The user specified Dotnet build arguments. + public void OverrideValue(Recommendation recommendation, string publishArgs) + { + var resultString = ValidateDotnetPublishArgs(publishArgs); + if (!string.IsNullOrEmpty(resultString)) + throw new InvalidOverrideValueException(resultString); + recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments = publishArgs.Replace("\"", "\"\""); + } + + private string ValidateDotnetPublishArgs(string publishArgs) + { + var resultString = string.Empty; + + if (publishArgs.Contains("-o ") || publishArgs.Contains("--output ")) + resultString += "You must not include -o/--output as an additional argument as it is used internally." + Environment.NewLine; + if (publishArgs.Contains("-c ") || publishArgs.Contains("--configuration ")) + resultString += "You must not include -c/--configuration as an additional argument. You can set the build configuration in the advanced settings." + Environment.NewLine; + if (publishArgs.Contains("--self-contained") || publishArgs.Contains("--no-self-contained")) + resultString += "You must not include --self-contained/--no-self-contained as an additional argument. You can set the self-contained property in the advanced settings." + Environment.NewLine; + + if (!string.IsNullOrEmpty(resultString)) + return "Invalid valid value for Dotnet Publish Arguments." + Environment.NewLine + resultString.Trim(); + return ""; + } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs index 6bfbd0168..314e5317f 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishBuildConfigurationCommand.cs @@ -27,5 +27,16 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = settingValue; return Task.FromResult(settingValue); } + + /// + /// This method will be invoked to set the Dotnet build configuration in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// The user specified Dotnet build configuration. + public void Overridevalue(Recommendation recommendation, string configuration) + { + recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = configuration; + } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs index c54867e0a..5e5fa02a5 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishSelfContainedBuildCommand.cs @@ -23,5 +23,16 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt var result = answer == YesNo.Yes ? "true" : "false"; return Task.FromResult(result); } + + /// + /// This method will be invoked to indiciate if this is a self-contained build in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// The user specified value to indicate if this is a self-contained build. + public void OverrideValue(Recommendation recommendation, bool publishSelfContainedBuild) + { + recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = publishSelfContainedBuild; + } } } diff --git a/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs b/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs index 50d682f7b..858374ed0 100644 --- a/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs +++ b/src/AWS.Deploy.CLI/ConsoleInteractiveServiceImpl.cs @@ -20,6 +20,7 @@ public string ReadLine() } public bool Diagnostics { get; set; } + public bool DisableInteractive { get; set; } public void WriteDebugLine(string? message) { diff --git a/src/AWS.Deploy.CLI/Exceptions.cs b/src/AWS.Deploy.CLI/Exceptions.cs index 5bb71ae91..957bb2f84 100644 --- a/src/AWS.Deploy.CLI/Exceptions.cs +++ b/src/AWS.Deploy.CLI/Exceptions.cs @@ -75,8 +75,8 @@ public TcpPortInUseException(string message, Exception? innerException = null) : /// Throw if unable to find a compatible recipe. /// [AWSDeploymentExpectedException] - public class FailedToCompatibleRecipeException : Exception + public class FailedToFindCompatibleRecipeException : Exception { - public FailedToCompatibleRecipeException(string message, Exception? innerException = null) : base(message, innerException) { } + public FailedToFindCompatibleRecipeException(string message, Exception? innerException = null) : base(message, innerException) { } } } diff --git a/src/AWS.Deploy.CLI/IToolInteractiveService.cs b/src/AWS.Deploy.CLI/IToolInteractiveService.cs index 3639af381..7b5871b5c 100644 --- a/src/AWS.Deploy.CLI/IToolInteractiveService.cs +++ b/src/AWS.Deploy.CLI/IToolInteractiveService.cs @@ -14,6 +14,7 @@ public interface IToolInteractiveService string ReadLine(); bool Diagnostics { get; set; } + bool DisableInteractive { get; set; } ConsoleKeyInfo ReadKey(bool intercept); } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 746cb6e89..7f07d8042 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -27,6 +27,7 @@ using AWS.Deploy.Orchestration.Utilities; using Microsoft.AspNetCore.Authorization; using Amazon.Runtime; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.CLI.ServerMode.Controllers { @@ -127,6 +128,99 @@ public async Task GetRecommendations(string sessionId) return Ok(output); } + /// + /// Gets the list of updatable option setting items for the selected recommendation. + /// + [HttpGet("session//settings")] + [SwaggerOperation(OperationId = "GetConfigSettings")] + [SwaggerResponse(200, type: typeof(GetOptionSettingsOutput))] + [Authorize] + public IActionResult GetConfigSettings(string sessionId) + { + var state = _stateServer.Get(sessionId); + if (state == null) + { + return NotFound($"Session ID {sessionId} not found."); + } + + if (state.SelectedRecommendation == null) + { + return NotFound($"A deployment target is not set for Session ID {sessionId}."); + } + + var orchestrator = CreateOrchestrator(state); + + var deploymentBundleDefinition = orchestrator.GetDeploymentBundleDefinition(state.SelectedRecommendation); + + var configurableOptionSettings = state.SelectedRecommendation.Recipe.OptionSettings.Union(deploymentBundleDefinition.Parameters); + + var output = new GetOptionSettingsOutput(); + output.OptionSettings = ListOptionSettingSummary(state.SelectedRecommendation, configurableOptionSettings); + + return Ok(output); + } + + private List ListOptionSettingSummary(Recommendation recommendation, IEnumerable configurableOptionSettings) + { + var optionSettingItems = new List(); + + foreach (var setting in configurableOptionSettings) + { + var settingSummary = new OptionSettingItemSummary(setting.Id, setting.Name, setting.Description, setting.Type.ToString()) + { + TypeHint = setting.TypeHint?.ToString(), + Value = recommendation.GetOptionSettingValue(setting), + Advanced = setting.AdvancedSetting, + Updatable = (!recommendation.IsExistingCloudApplication || setting.Updatable) && recommendation.IsOptionSettingDisplayable(setting), + ChildOptionSettings = ListOptionSettingSummary(recommendation, setting.ChildOptionSettings) + }; + + optionSettingItems.Add(settingSummary); + } + + return optionSettingItems; + } + + /// + /// Applies a value for a list of option setting items on the selected recommendation. + /// Option setting updates are provided as Key Value pairs with the Key being the JSON path to the leaf node. + /// Only primitive data types are supported for Value updates. The Value is a string value which will be parsed as its corresponding data type. + /// + [HttpPut("session//settings")] + [SwaggerOperation(OperationId = "ApplyConfigSettings")] + [SwaggerResponse(200, type: typeof(ApplyConfigSettingsOutput))] + [Authorize] + public IActionResult ApplyConfigSettings(string sessionId, [FromBody] ApplyConfigSettingsInput input) + { + var state = _stateServer.Get(sessionId); + if (state == null) + { + return NotFound($"Session ID {sessionId} not found."); + } + + if (state.SelectedRecommendation == null) + { + return NotFound($"A deployment target is not set for Session ID {sessionId}."); + } + + var output = new ApplyConfigSettingsOutput(); + + foreach (var updatedSetting in input.UpdatedSettings) + { + try + { + var setting = state.SelectedRecommendation.GetOptionSetting(updatedSetting.Key); + setting.SetValueOverride(updatedSetting.Value); + } + catch (Exception ex) + { + output.FailedConfigUpdates.Add(updatedSetting.Key, ex.Message); + } + } + + return Ok(output); + } + /// /// Gets the list of existing deployments that are compatible with the session's project. /// diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsInput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsInput.cs new file mode 100644 index 000000000..b91668238 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsInput.cs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class ApplyConfigSettingsInput + { + public Dictionary UpdatedSettings { get; set; } = new Dictionary(); + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsOutput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsOutput.cs new file mode 100644 index 000000000..3c9aca47f --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/ApplyConfigSettingsOutput.cs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class ApplyConfigSettingsOutput + { + public IDictionary FailedConfigUpdates { get; set; } = new Dictionary(); + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/GetOptionSettingsOutput.cs b/src/AWS.Deploy.CLI/ServerMode/Models/GetOptionSettingsOutput.cs new file mode 100644 index 000000000..bbdb8b351 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/GetOptionSettingsOutput.cs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class GetOptionSettingsOutput + { + public IList OptionSettings { get; set; } = new List(); + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs new file mode 100644 index 000000000..b3a86ac68 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class OptionSettingItemSummary + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public object? Value { get; set; } + + public string Type { get; set; } + + public string? TypeHint { get; set; } + + public bool Advanced { get; set; } + + public bool Updatable { get; set; } + + public List ChildOptionSettings { get; set; } = new(); + + public OptionSettingItemSummary(string id, string name, string description, string type) + { + Id = id; + Name = name; + Description = description; + Type = type; + } + } +} diff --git a/src/AWS.Deploy.CLI/Utilities/CommandLineWrapper.cs b/src/AWS.Deploy.CLI/Utilities/CommandLineWrapper.cs index 781f1923a..52d9c5ed5 100644 --- a/src/AWS.Deploy.CLI/Utilities/CommandLineWrapper.cs +++ b/src/AWS.Deploy.CLI/Utilities/CommandLineWrapper.cs @@ -43,7 +43,8 @@ public async Task Run( Action? onComplete = null, bool redirectIO = true, IDictionary? environmentVariables = null, - CancellationToken cancelToken = default) + CancellationToken cancelToken = default, + bool needAwsCredentials = false) { StringBuilder strOutput = new StringBuilder(); StringBuilder strError = new StringBuilder(); @@ -74,7 +75,9 @@ public async Task Run( } UpdateEnvironmentVariables(processStartInfo, environmentVariables); - _processStartInfoAction?.Invoke(processStartInfo); + + if (needAwsCredentials) + _processStartInfoAction?.Invoke(processStartInfo); var process = Process.Start(processStartInfo); if (null == process) diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index ae5515250..0c2f951a8 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -136,6 +136,7 @@ public InvalidValidatorTypeException(string? message, Exception? innerException /// /// Thrown if is given an invalid value. /// + [AWSDeploymentExpectedException] public class ValidationFailedException : Exception { public ValidationFailedException(string? message, Exception? innerException = null) : base(message, innerException) { } @@ -149,6 +150,14 @@ public class InvalidProjectPathException : Exception { public InvalidProjectPathException(string message, Exception? innerException = null) : base(message, innerException) { } } + + /// Throw if an invalid is used. + /// + [AWSDeploymentExpectedException] + public class InvalidUserDeploymentSettingsException : Exception + { + public InvalidUserDeploymentSettingsException(string message, Exception? innerException = null) : base(message, innerException) { } + } /// /// Indicates a specific strongly typed Exception can be anticipated. diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 16c4fbf53..5a89305fe 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -107,10 +107,13 @@ public void SetValueOverride(object valueOverride) validationFailedMessage += result.ValidationFailedMessage + Environment.NewLine; } } - if (!isValid) throw new ValidationFailedException(validationFailedMessage.Trim()); + if (AllowedValues != null && AllowedValues.Count > 0 && valueOverride != null && + !AllowedValues.Contains(valueOverride.ToString() ?? "")) + throw new InvalidOverrideValueException($"Invalid value for option setting item: {Id}"); + if (valueOverride is bool || valueOverride is int || valueOverride is long) { _valueOverride = valueOverride; @@ -127,8 +130,6 @@ public void SetValueOverride(object valueOverride) } else { - if (AllowedValues != null && AllowedValues.Count > 0 && !AllowedValues.Contains(valueOverrideString)) - throw new InvalidOverrideValueException("Invalid value for option setting item"); _valueOverride = valueOverrideString; } } diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index b44a6022c..c0b0ae358 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -90,7 +90,8 @@ private void ApplyPreviousSettings(IEnumerable optionSettings public OptionSettingItem GetOptionSetting(string? jsonPath) { if (string.IsNullOrEmpty(jsonPath)) - throw new OptionSettingItemDoesNotExistException("The Option Setting Item you are looking for does not exist."); + throw new OptionSettingItemDoesNotExistException($"The Option Setting Item {jsonPath} does not exist as part of the" + + $" {Recipe.Name} recipe"); var ids = jsonPath.Split('.'); OptionSettingItem? optionSetting = null; @@ -101,7 +102,8 @@ public OptionSettingItem GetOptionSetting(string? jsonPath) optionSetting = optionSettings.FirstOrDefault(os => os.Id.Equals(id)); if (optionSetting == null) { - throw new OptionSettingItemDoesNotExistException("The Option Setting Item you are looking for does not exist."); + throw new OptionSettingItemDoesNotExistException($"The Option Setting Item {jsonPath} does not exist as part of the" + + $" {Recipe.Name} recipe"); } } diff --git a/src/AWS.Deploy.Common/UserDeploymentSettings.cs b/src/AWS.Deploy.Common/UserDeploymentSettings.cs new file mode 100644 index 000000000..8c7d154e4 --- /dev/null +++ b/src/AWS.Deploy.Common/UserDeploymentSettings.cs @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AWS.Deploy.Common +{ + /// + /// A container for User Deployment Settings file that is supplied with the deployment + /// to apply defaults and bypass some user prompts. + /// + public class UserDeploymentSettings + { + public string? AWSProfile { get; set; } + + public string? AWSRegion { get; set; } + + public string? StackName { get; set; } + + public string? RecipeId { get; set; } + + public JObject? OptionSettingsConfig { get; set; } + + public Dictionary LeafOptionSettingItems = new Dictionary(); + + /// + /// Reads the User Deployment Settings file and deserializes it into a object. + /// + /// Thrown if an error occured while reading or deserializing the User Deployment Settings file. + public static UserDeploymentSettings? ReadSettings(string filePath) + { + try + { + var userDeploymentSettings = JsonConvert.DeserializeObject(File.ReadAllText(filePath)); + if (userDeploymentSettings.OptionSettingsConfig != null) + userDeploymentSettings.TraverseRootToLeaf(userDeploymentSettings.OptionSettingsConfig.Root); + return userDeploymentSettings; + } + catch (Exception ex) + { + throw new InvalidUserDeploymentSettingsException("An error occured while trying to deserialize the User Deployment Settings file.", ex); + } + } + + /// + /// This method is responsible for traversing all paths from root node to leaf nodes in a Json blob. + /// These paths and the corresponding leaf node values are stored in a dictionary + /// + /// The current node that is being processed. + private void TraverseRootToLeaf(JToken node) + { + if (!node.HasValues) + { + // The only way to reach a leaf node of type object is if the object is empty. + if (node.Type.ToString() == "Object") + return; + + var path = node.Path; + if (path.Contains("['")) + path = path.Substring(2, node.Path.Length - 4); + LeafOptionSettingItems.Add(path, node.Value()); + return; + } + + foreach (var childNode in node.Children()) + { + TraverseRootToLeaf(childNode); + } + } + } +} diff --git a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs index 9a678ae82..6e495bb99 100644 --- a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs +++ b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs @@ -11,7 +11,7 @@ namespace AWS.Deploy.Orchestration { public class CdkAppSettingsSerializer { - public string Build(CloudApplication cloudApplication, Recommendation recommendation) + public string Build(CloudApplication cloudApplication, Recommendation recommendation, OrchestratorSession session) { var projectPath = new FileInfo(recommendation.ProjectPath).Directory?.FullName; if (string.IsNullOrEmpty(projectPath)) @@ -23,6 +23,8 @@ public string Build(CloudApplication cloudApplication, Recommendation recommenda projectPath, recommendation.Recipe.Id, recommendation.Recipe.Version, + session.AWSAccountId, + session.AWSRegion, new () ) { diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 805e9e9c4..1cf7deb27 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -40,7 +40,7 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat var cdkProjectPath = await CreateCdkProjectForDeployment(recommendation, session); // Write required configuration in appsettings.json - var appSettingsBody = _appSettingsBuilder.Build(cloudApplication, recommendation); + var appSettingsBody = _appSettingsBuilder.Build(cloudApplication, recommendation, session); var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); using (var appSettingsFile = new StreamWriter(appSettingsFilePath)) { @@ -50,13 +50,15 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat _interactiveService.LogMessageLine("Starting deployment of CDK Project"); // Ensure region is bootstrapped - await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion}"); + await _commandLineWrapper.Run($"npx cdk bootstrap aws://{session.AWSAccountId}/{session.AWSRegion}", + needAwsCredentials: true); // Handover to CDK command line tool // Use a CDK Context parameter to specify the settings file that has been serialized. await _commandLineWrapper.Run( $"npx cdk deploy --require-approval never -c {Constants.CloudFormationIdentifier.SETTINGS_PATH_CDK_CONTEXT_PARAMETER}=\"{appSettingsFilePath}\"", workingDirectory: cdkProjectPath, - environmentVariables: environmentVariables); + environmentVariables: environmentVariables, + needAwsCredentials: true); } private async Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session) diff --git a/src/AWS.Deploy.Orchestration/PreviousDeploymentSettings.cs b/src/AWS.Deploy.Orchestration/PreviousDeploymentSettings.cs deleted file mode 100644 index 673c13d58..000000000 --- a/src/AWS.Deploy.Orchestration/PreviousDeploymentSettings.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.IO; -using Newtonsoft.Json; - -namespace AWS.Deploy.Orchestration -{ - public class PreviousDeploymentSettings - { - public const string DEFAULT_FILE_NAME = "aws-netsuite-deployment.json"; - - public string? Profile { get; set; } - public string? Region { get; set; } - - public static PreviousDeploymentSettings ReadSettings(string projectPath, string? configFile) - { - var fullPath = GetFullConfigFilePath(projectPath, configFile); - if (!File.Exists(fullPath)) - return new PreviousDeploymentSettings(); - - return ReadSettings(fullPath); - } - - public static PreviousDeploymentSettings ReadSettings(string filePath) - { - return JsonConvert.DeserializeObject(File.ReadAllText(filePath)); - } - - public void SaveSettings(string projectPath, string configFile) - { - SaveSettings(GetFullConfigFilePath(projectPath, configFile)); - } - - public void SaveSettings(string filePath) - { - var json = JsonConvert.SerializeObject(this, Formatting.Indented); - File.WriteAllText(filePath, json); - } - - public static string GetFullConfigFilePath(string projectPath, string? configFile) - { - var fullPath = string.IsNullOrEmpty(configFile) ? Path.Combine(projectPath, DEFAULT_FILE_NAME) : Path.Combine(projectPath, configFile); - return fullPath; - } - } -} diff --git a/src/AWS.Deploy.Orchestration/Utilities/ICommandLineWrapper.cs b/src/AWS.Deploy.Orchestration/Utilities/ICommandLineWrapper.cs index 4d62aac8f..04da5c4f0 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/ICommandLineWrapper.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/ICommandLineWrapper.cs @@ -51,7 +51,8 @@ public Task Run( Action? onComplete = null, bool redirectIO = true, IDictionary? environmentVariables = null, - CancellationToken cancelToken = default); + CancellationToken cancelToken = default, + bool needAwsCredentials = false); /// /// Configure the child process that executes the command passed as parameter in method. @@ -103,7 +104,8 @@ public static async Task TryRunWithResult( bool streamOutputToInteractiveService = false, bool redirectIO = true, IDictionary? environmentVariables = null, - CancellationToken cancelToken = default) + CancellationToken cancelToken = default, + bool needAwsCredentials = false) { var result = new TryRunResult(); @@ -114,7 +116,8 @@ await commandLineWrapper.Run( onComplete: runResult => result = runResult, redirectIO: redirectIO, environmentVariables: environmentVariables, - cancelToken: cancelToken); + cancelToken: cancelToken, + needAwsCredentials: needAwsCredentials); return result; } diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs index b536ccf9c..bbfb1fde5 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs @@ -56,6 +56,16 @@ public class RecipeConfiguration /// public T Settings { get; set; } + /// + /// The Region used during deployment. + /// + public string AWSRegion { get; set; } + + /// + /// The account ID used during deployment. + /// + public string AWSAccountId { get; set; } + /// A parameterless constructor is needed for /// or the classes will fail to initialize. /// The warnings are disabled since a parameterless constructor will allow non-nullable properties to be initialized with null values. @@ -66,13 +76,16 @@ public RecipeConfiguration() } #nullable restore warnings - public RecipeConfiguration(string stackName, string projectPath, string recipeId, string recipeVersion, T settings) + public RecipeConfiguration(string stackName, string projectPath, string recipeId, string recipeVersion, + string awsAccountId, string awsRegion, T settings) { StackName = stackName; ProjectPath = projectPath; RecipeId = recipeId; RecipeVersion = recipeVersion; - Settings = settings; + AWSAccountId = awsAccountId; + AWSRegion = awsRegion; + Settings = settings; } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/.template.config/template.json index 23eeffdac..4fea35daf 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/.template.config/template.json +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/.template.config/template.json @@ -14,18 +14,6 @@ "sourceName": "AspNetAppEcsFargate", "preferNameDirectory": true, "symbols": { - "AWSAccountId": { - "type": "parameter", - "description": "Specifies the AWS Account ID", - "replaces": "AWSAccountId", - "datatype": "string" - }, - "AWSRegion": { - "type": "parameter", - "description": "Specifies the AWS Region", - "replaces": "AWSRegion", - "datatype": "string" - }, "AWSDeployRecipesCDKCommonVersion": { "type": "parameter", "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj index c21dbda7f..c6c2192c9 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs index fda8af359..8de5e9cdf 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Program.cs @@ -23,8 +23,8 @@ public static void Main(string[] args) { Env = new Environment { - Account = "AWSAccountId", - Region = "AWSRegion" + Account = recipeConfiguration.AWSAccountId, + Region = recipeConfiguration.AWSRegion } }), recipeConfiguration); diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/.template.config/template.json index cd73a6cbd..09788af30 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/.template.config/template.json +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/.template.config/template.json @@ -14,18 +14,6 @@ "sourceName": "AspNetAppElasticBeanstalkLinux", "preferNameDirectory": true, "symbols": { - "AWSAccountId": { - "type": "parameter", - "description": "Specifies the AWS Account ID", - "replaces": "AWSAccountId", - "datatype": "string" - }, - "AWSRegion": { - "type": "parameter", - "description": "Specifies the AWS Region", - "replaces": "AWSRegion", - "datatype": "string" - }, "AWSDeployRecipesCDKCommonVersion": { "type": "parameter", "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj index 81a8b6df7..93197d8ff 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Program.cs index ef3102309..0240694ef 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/Program.cs @@ -21,8 +21,8 @@ public static void Main(string[] args) { Env = new Environment { - Account = "AWSAccountId", - Region = "AWSRegion" + Account = recipeConfiguration.AWSAccountId, + Region = recipeConfiguration.AWSRegion } }), recipeConfiguration); diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/.template.config/template.json index 517f84dc0..92fd3e3bd 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/.template.config/template.json +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/.template.config/template.json @@ -14,18 +14,6 @@ "sourceName": "ConsoleAppECSFargateScheduleTask", "preferNameDirectory": true, "symbols": { - "AWSAccountId": { - "type": "parameter", - "description": "Specifies the AWS Account ID", - "replaces": "AWSAccountId", - "datatype": "string" - }, - "AWSRegion": { - "type": "parameter", - "description": "Specifies the AWS Region", - "replaces": "AWSRegion", - "datatype": "string" - }, "AWSDeployRecipesCDKCommonVersion": { "type": "parameter", "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj index 411712b70..cb3906e87 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/Program.cs index 2f1db9705..357bdcb2e 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/Program.cs @@ -18,8 +18,8 @@ public static void Main(string[] args) { Env = new Environment { - Account = "AWSAccountId", - Region = "AWSRegion" + Account = recipeConfiguration.AWSAccountId, + Region = recipeConfiguration.AWSRegion } }), recipeConfiguration); diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/.template.config/template.json index 394debb2c..f35d1550c 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/.template.config/template.json +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/.template.config/template.json @@ -14,18 +14,6 @@ "sourceName": "ConsoleAppECSFargateScheduleTask", "preferNameDirectory": true, "symbols": { - "AWSAccountId": { - "type": "parameter", - "description": "Specifies the AWS Account ID", - "replaces": "AWSAccountId", - "datatype": "string" - }, - "AWSRegion": { - "type": "parameter", - "description": "Specifies the AWS Region", - "replaces": "AWSRegion", - "datatype": "string" - }, "AWSDeployRecipesCDKCommonVersion": { "type": "parameter", "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj index c21dbda7f..c6c2192c9 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/Program.cs index 1f7b6bb74..0cfa4c8ec 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/Program.cs @@ -21,8 +21,8 @@ public static void Main(string[] args) { Env = new Environment { - Account = "AWSAccountId", - Region = "AWSRegion" + Account = recipeConfiguration.AWSAccountId, + Region = recipeConfiguration.AWSRegion } }), recipeConfiguration); diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/.template.config/template.json b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/.template.config/template.json index 9a1691f27..1257747ff 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/.template.config/template.json +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/.template.config/template.json @@ -14,18 +14,6 @@ "sourceName": "ConsoleAppEcsFargateService", "preferNameDirectory": true, "symbols": { - "AWSAccountId": { - "type": "parameter", - "description": "Specifies the AWS Account ID", - "replaces": "AWSAccountId", - "datatype": "string" - }, - "AWSRegion": { - "type": "parameter", - "description": "Specifies the AWS Region", - "replaces": "AWSRegion", - "datatype": "string" - }, "AWSDeployRecipesCDKCommonVersion": { "type": "parameter", "description": "The version number of AWS.Deploy.Recipes.CDK.Common to use", diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj index c21dbda7f..c6c2192c9 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Program.cs b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Program.cs index 8a03c7c6f..37f9f214e 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Program.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/Program.cs @@ -21,8 +21,8 @@ public static void Main(string[] args) { Env = new Environment { - Account = "AWSAccountId", - Region = "AWSRegion" + Account = recipeConfiguration.AWSAccountId, + Region = recipeConfiguration.AWSRegion } }), recipeConfiguration); diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 640670288..bfb53ea09 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -125,9 +125,9 @@ { "ValidatorType": "Regex", "Configuration" : { - "Regex": "^([A-Za-z0-9-]{1,255})$", + "Regex": "^([A-Za-z0-9_-]{1,255})$", "AllowEmptyString": true, - "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens and can't be longer than 255 character in length." + "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } } ], diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index 5ce440b70..55f73670d 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -242,8 +242,8 @@ { "ValidatorType": "Regex", "Configuration" : { - "Regex": "arn:[^:]+:elasticbeanstalk:[^:]*:[0-9]{12}:platform/.+", - "ValidationFailedMessage": "Invalid ElasticBeanstalkPlatform Arn. The ARN should contain the arn:[PARTITION]:elasticbeanstalk namespace, followed by the region, the account ID, and then the resource path. For example - arn:aws:elasticbeanstalk:us-east-2:123456789012:platform/MyPlatform/1.0 is a valid Arn. For more information visit https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.iam.policies.arn.html" + "Regex": "arn:[^:]+:elasticbeanstalk:[^:]+:[^:]*:platform/.+", + "ValidationFailedMessage": "Invalid ElasticBeanstalkPlatform Arn. The ARN should contain the arn:[PARTITION]:elasticbeanstalk namespace, followed by the region, an optional account ID, and then the resource path. For example - arn:aws:elasticbeanstalk:us-east-2:123456789012:platform/MyPlatform/1.0 is a valid Arn. For more information visit https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.iam.policies.arn.html" } } ] diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index 3dce954c0..43a12b997 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -134,9 +134,9 @@ { "ValidatorType": "Regex", "Configuration" : { - "Regex": "^([A-Za-z0-9-]{1,255})$", + "Regex": "^([A-Za-z0-9_-]{1,255})$", "AllowEmptyString": true, - "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens and can't be longer than 255 character in length." + "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } } ], diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index daaef7382..d0d3be755 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -134,9 +134,9 @@ { "ValidatorType": "Regex", "Configuration" : { - "Regex": "^([A-Za-z0-9-]{1,255})$", + "Regex": "^([A-Za-z0-9_-]{1,255})$", "AllowEmptyString": true, - "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens and can't be longer than 255 character in length." + "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } } ], diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index b1846da74..6c28f6a2b 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -62,6 +62,32 @@ public partial interface IRestAPIClient /// A server side error occurred. System.Threading.Tasks.Task GetRecommendationsAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + /// Gets the list of updatable option setting items for the selected recommendation. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetConfigSettingsAsync(string sessionId); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Gets the list of updatable option setting items for the selected recommendation. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetConfigSettingsAsync(string sessionId, System.Threading.CancellationToken cancellationToken); + + /// Applies a value for a list of option setting items on the selected recommendation. + /// Option setting updates are provided as Key Value pairs with the Key being the JSON path to the leaf node. + /// Only primitive data types are supported for Value updates. The Value is a string value which will be parsed as its corresponding data type. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task ApplyConfigSettingsAsync(string sessionId, ApplyConfigSettingsInput body); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Applies a value for a list of option setting items on the selected recommendation. + /// Option setting updates are provided as Key Value pairs with the Key being the JSON path to the leaf node. + /// Only primitive data types are supported for Value updates. The Value is a string value which will be parsed as its corresponding data type. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task ApplyConfigSettingsAsync(string sessionId, ApplyConfigSettingsInput body, System.Threading.CancellationToken cancellationToken); + /// Gets the list of existing deployments that are compatible with the session's project. /// Success /// A server side error occurred. @@ -456,6 +482,175 @@ public async System.Threading.Tasks.Task GetRecommenda } } + /// Gets the list of updatable option setting items for the selected recommendation. + /// Success + /// A server side error occurred. + public System.Threading.Tasks.Task GetConfigSettingsAsync(string sessionId) + { + return GetConfigSettingsAsync(sessionId, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Gets the list of updatable option setting items for the selected recommendation. + /// Success + /// A server side error occurred. + public async System.Threading.Tasks.Task GetConfigSettingsAsync(string sessionId, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v1/Deployment/session//settings?"); + 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(); + } + } + + /// Applies a value for a list of option setting items on the selected recommendation. + /// Option setting updates are provided as Key Value pairs with the Key being the JSON path to the leaf node. + /// Only primitive data types are supported for Value updates. The Value is a string value which will be parsed as its corresponding data type. + /// Success + /// A server side error occurred. + public System.Threading.Tasks.Task ApplyConfigSettingsAsync(string sessionId, ApplyConfigSettingsInput body) + { + return ApplyConfigSettingsAsync(sessionId, body, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Applies a value for a list of option setting items on the selected recommendation. + /// Option setting updates are provided as Key Value pairs with the Key being the JSON path to the leaf node. + /// Only primitive data types are supported for Value updates. The Value is a string value which will be parsed as its corresponding data type. + /// Success + /// A server side error occurred. + public async System.Threading.Tasks.Task ApplyConfigSettingsAsync(string sessionId, ApplyConfigSettingsInput body, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v1/Deployment/session//settings?"); + 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()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + 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(); + } + } + /// Gets the list of existing deployments that are compatible with the session's project. /// Success /// A server side error occurred. @@ -873,6 +1068,24 @@ private string ConvertToString(object value, System.Globalization.CultureInfo cu } } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class ApplyConfigSettingsInput + { + [Newtonsoft.Json.JsonProperty("updatedSettings", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.IDictionary UpdatedSettings { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class ApplyConfigSettingsOutput + { + [Newtonsoft.Json.JsonProperty("failedConfigUpdates", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.IDictionary FailedConfigUpdates { get; set; } + + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] public enum DeploymentStatus { @@ -919,6 +1132,15 @@ public partial class GetExistingDeploymentsOutput public System.Collections.Generic.ICollection ExistingDeployments { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class GetOptionSettingsOutput + { + [Newtonsoft.Json.JsonProperty("optionSettings", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection OptionSettings { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] @@ -938,6 +1160,39 @@ public partial class HealthStatusOutput public SystemStatus Status { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class OptionSettingItemSummary + { + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Id { get; set; } + + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Description { get; set; } + + [Newtonsoft.Json.JsonProperty("value", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public object Value { get; set; } + + [Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Type { get; set; } + + [Newtonsoft.Json.JsonProperty("typeHint", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string TypeHint { get; set; } + + [Newtonsoft.Json.JsonProperty("advanced", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Advanced { get; set; } + + [Newtonsoft.Json.JsonProperty("updatable", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Updatable { get; set; } + + [Newtonsoft.Json.JsonProperty("childOptionSettings", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection ChildOptionSettings { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeHttpClient.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeHttpClient.cs index b797df635..0f9767c9a 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeHttpClient.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeHttpClient.cs @@ -61,7 +61,7 @@ public static void AddAuthorizationHeader(HttpRequestMessage request, ImmutableC {"awsAccessKeyId", credentials.AccessKey }, {"awsSecretKey", credentials.SecretKey }, {"requestId", Guid.NewGuid().ToString() }, - {"issueDate", DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo) } + {"issueDate", DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffZ", DateTimeFormatInfo.InvariantInfo) } }; if(!string.IsNullOrEmpty(credentials.Token)) diff --git a/src/AWS.Deploy.ServerMode.ClientGenerator/AWS.Deploy.ServerMode.ClientGenerator.csproj b/src/AWS.Deploy.ServerMode.ClientGenerator/AWS.Deploy.ServerMode.ClientGenerator.csproj index a57673a92..88a613907 100644 --- a/src/AWS.Deploy.ServerMode.ClientGenerator/AWS.Deploy.ServerMode.ClientGenerator.csproj +++ b/src/AWS.Deploy.ServerMode.ClientGenerator/AWS.Deploy.ServerMode.ClientGenerator.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + Major diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj b/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj index 572f4266f..a4d8aede4 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj +++ b/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj @@ -7,6 +7,24 @@ Latest + + + + + + + Always + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs new file mode 100644 index 000000000..1f9d84058 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using AWS.Deploy.Common; +using Xunit; + +namespace AWS.Deploy.CLI.Common.UnitTests.ConfigFileDeployment +{ + public class ECSFargateDeploymentTest + { + private readonly UserDeploymentSettings _userDeploymentSettings; + public ECSFargateDeploymentTest() + { + var filePath = Path.Combine("ConfigFileDeployment", "TestFiles", "UnitTestFiles", "ECSFargateConfigFile.json"); + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(filePath); + _userDeploymentSettings = userDeploymentSettings; + } + + [Fact] + public void VerifyJsonParsing() + { + Assert.Equal("default", _userDeploymentSettings.AWSProfile); + Assert.Equal("us-west-2", _userDeploymentSettings.AWSRegion); + Assert.Equal("MyAppStack", _userDeploymentSettings.StackName); + Assert.Equal("AspNetAppEcsFargate", _userDeploymentSettings.RecipeId); + + var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; + Assert.Equal("True", optionSettingDictionary["ECSCluster.CreateNew"]); + Assert.Equal("MyNewCluster", optionSettingDictionary["ECSCluster.NewClusterName"]); + Assert.Equal("MyNewService", optionSettingDictionary["ECSServiceName"]); + Assert.Equal("3", optionSettingDictionary["DesiredCount"]); + Assert.Equal("True", optionSettingDictionary["ApplicationIAMRole.CreateNew"]); + Assert.Equal("True", optionSettingDictionary["Vpc.IsDefault"]); + Assert.Equal("256", optionSettingDictionary["TaskCpu"]); + Assert.Equal("512", optionSettingDictionary["TaskMemory"]); + Assert.Equal("C:\\codebase", optionSettingDictionary["DockerExecutionDirectory"]); + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs new file mode 100644 index 000000000..01cd0f04d --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using AWS.Deploy.Common; +using Xunit; + +namespace AWS.Deploy.CLI.Common.UnitTests.ConfigFileDeployment +{ + public class ElasticBeanStalkDeploymentTest + { + private readonly UserDeploymentSettings _userDeploymentSettings; + public ElasticBeanStalkDeploymentTest() + { + var filePath = Path.Combine("ConfigFileDeployment", "TestFiles", "UnitTestFiles", "ElasticBeanStalkConfigFile.json"); + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(filePath); + _userDeploymentSettings = userDeploymentSettings; + } + + [Fact] + public void VerifyJsonParsing() + { + Assert.Equal("default", _userDeploymentSettings.AWSProfile); + Assert.Equal("us-west-2", _userDeploymentSettings.AWSRegion); + Assert.Equal("MyAppStack", _userDeploymentSettings.StackName); + Assert.Equal("AspNetAppElasticBeanstalkLinux", _userDeploymentSettings.RecipeId); + + var optionSettingDictionary = _userDeploymentSettings.LeafOptionSettingItems; + Assert.Equal("True", optionSettingDictionary["BeanstalkApplication.CreateNew"]); + Assert.Equal("MyApplication", optionSettingDictionary["BeanstalkApplication.ApplicationName"]); + Assert.Equal("MyEnvironment", optionSettingDictionary["EnvironmentName"]); + Assert.Equal("MyInstance", optionSettingDictionary["InstanceType"]); + Assert.Equal("SingleInstance", optionSettingDictionary["EnvironmentType"]); + Assert.Equal("application", optionSettingDictionary["LoadBalancerType"]); + Assert.Equal("True", optionSettingDictionary["ApplicationIAMRole.CreateNew"]); + Assert.Equal("MyPlatformArn", optionSettingDictionary["ElasticBeanstalkPlatformArn"]); + Assert.Equal("True", optionSettingDictionary["ElasticBeanstalkManagedPlatformUpdates.ManagedActionsEnabled"]); + Assert.Equal("Mon:12:00", optionSettingDictionary["ElasticBeanstalkManagedPlatformUpdates.PreferredStartTime"]); + Assert.Equal("minor", optionSettingDictionary["ElasticBeanstalkManagedPlatformUpdates.UpdateLevel"]); + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ECSFargateConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ECSFargateConfigFile.json new file mode 100644 index 000000000..176321344 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ECSFargateConfigFile.json @@ -0,0 +1,24 @@ +{ + "StackName": "EcsFargateAppStack", + "RecipeId": "AspNetAppEcsFargate", + "OptionSettingsConfig":{ + "ECSCluster": + { + "CreateNew": true, + "NewClusterName": "MyNewCluster" + }, + "ECSServiceName": "MyNewService", + "DesiredCount": 3, + "ApplicationIAMRole": + { + "CreateNew": true + }, + "Vpc": + { + "IsDefault": true + }, + "AdditionalECSServiceSecurityGroups": "", + "TaskCpu": 256, + "TaskMemory": 512 + } + } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ElasticBeanStalkConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ElasticBeanStalkConfigFile.json new file mode 100644 index 000000000..75b24aad1 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/IntegrationTestFiles/ElasticBeanStalkConfigFile.json @@ -0,0 +1,19 @@ +{ + "StackName": "ElasticBeanStalkAppStack", + "RecipeId": "AspNetAppElasticBeanstalkLinux", + "OptionSettingsConfig": + { + "BeanstalkApplication": + { + "CreateNew": true, + "ApplicationName": "MyApplication" + }, + "EnvironmentName": "MyEnvironment", + "EnvironmentType": "LoadBalanced", + "LoadBalancerType": "application", + "ApplicationIAMRole": + { + "CreateNew": true + } + } + } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json new file mode 100644 index 000000000..f4eecf4f6 --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ECSFargateConfigFile.json @@ -0,0 +1,27 @@ +{ + "AWSProfile": "default", + "AWSRegion": "us-west-2", + "StackName": "MyAppStack", + "RecipeId": "AspNetAppEcsFargate", + "OptionSettingsConfig":{ + "ECSCluster": + { + "CreateNew": true, + "NewClusterName": "MyNewCluster" + }, + "ECSServiceName": "MyNewService", + "DesiredCount": 3, + "ApplicationIAMRole": + { + "CreateNew": true + }, + "Vpc": + { + "IsDefault": true + }, + "AdditionalECSServiceSecurityGroups": "", + "TaskCpu": 256, + "TaskMemory": 512, + "DockerExecutionDirectory": "C:\\codebase" + } + } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json new file mode 100644 index 000000000..f305572ed --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/ConfigFileDeployment/TestFiles/UnitTestFiles/ElasticBeanStalkConfigFile.json @@ -0,0 +1,30 @@ +{ + "AWSProfile": "default", + "AWSRegion": "us-west-2", + "StackName": "MyAppStack", + "RecipeId": "AspNetAppElasticBeanstalkLinux", + "OptionSettingsConfig": + { + "BeanstalkApplication": + { + "CreateNew": true, + "ApplicationName": "MyApplication" + }, + "EnvironmentName": "MyEnvironment", + "InstanceType": "MyInstance", + "EnvironmentType": "SingleInstance", + "LoadBalancerType": "application", + "ApplicationIAMRole": + { + "CreateNew": true + }, + "ElasticBeanstalkPlatformArn": "MyPlatformArn", + "ElasticBeanstalkManagedPlatformUpdates": + { + "ManagedActionsEnabled": true, + "PreferredStartTime": "Mon:12:00", + "UpdateLevel": "minor" + } + } + } + \ No newline at end of file diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs index cfbec0982..0c9b5c1cc 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs @@ -65,16 +65,16 @@ public void EC2KeyPairValidationTest(string value, bool isValid) } [Theory] - [InlineData("arn:aws:elasticbeanstalk::123456789012:platform/MyPlatform", true)] - [InlineData("arn:aws-cn:elasticbeanstalk::123456789012:platform/MyPlatform", true)] - [InlineData("arn:aws:elasticbeanstalk::123456789012:platform/MyPlatform/v1.0", true)] - [InlineData("arn:aws:elasticbeanstalk::123456789012:platform/", false)] //no resource path - [InlineData("arn:aws:elasticbeanstack::123456789012:platform/MyPlatform", false)] //Typo elasticbeanstack instead of elasticbeanstalk - [InlineData("arn:aws:elasticbeanstalk::1234567890121234:platform/MyPlatform", false)] //invalid account ID + [InlineData("arn:aws:elasticbeanstalk:us-east-1:123456789012:platform/MyPlatform", true)] + [InlineData("arn:aws-cn:elasticbeanstalk:us-west-1:123456789012:platform/MyPlatform", true)] + [InlineData("arn:aws:elasticbeanstalk:eu-west-1:123456789012:platform/MyPlatform/v1.0", true)] + [InlineData("arn:aws:elasticbeanstalk:us-west-2::platform/MyPlatform/v1.0", true)] + [InlineData("arn:aws:elasticbeanstalk:us-east-1:123456789012:platform/", false)] //no resource path + [InlineData("arn:aws:elasticbeanstack:eu-west-1:123456789012:platform/MyPlatform", false)] //Typo elasticbeanstack instead of elasticbeanstalk public void ElasticBeanstalkPlatformArnValidationTest(string value, bool isValid) { var optionSettingItem = new OptionSettingItem("id", "name", "description"); - optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:[^:]+:elasticbeanstalk:[^:]*:[0-9]{12}:platform/.+")); + optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:[^:]+:elasticbeanstalk:[^:]+:[^:]*:platform/.+")); Validate(optionSettingItem, value, isValid); } diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs index 0a34dafbd..5a77b56f6 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Services/InMemoryInteractiveService.cs @@ -134,6 +134,7 @@ public string ReadLine() } public bool Diagnostics { get; set; } + public bool DisableInteractive { get; set; } public ConsoleKeyInfo ReadKey(bool intercept) { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/AWS.Deploy.CLI.IntegrationTests.csproj b/test/AWS.Deploy.CLI.IntegrationTests/AWS.Deploy.CLI.IntegrationTests.csproj index 1631c6d9c..845b49de2 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/AWS.Deploy.CLI.IntegrationTests.csproj +++ b/test/AWS.Deploy.CLI.IntegrationTests/AWS.Deploy.CLI.IntegrationTests.csproj @@ -16,6 +16,7 @@ + diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs new file mode 100644 index 000000000..3a73958aa --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -0,0 +1,140 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using Amazon.CloudFormation; +using Amazon.ECS; +using Amazon.ECS.Model; +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.Common; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment +{ + [Collection("Serial")] + public class ECSFargateDeploymentTest : IDisposable + { + private readonly HttpHelper _httpHelper; + private readonly CloudFormationHelper _cloudFormationHelper; + private readonly ECSHelper _ecsHelper; + private readonly App _app; + private readonly InMemoryInteractiveService _interactiveService; + private bool _isDisposed; + private readonly string _stackName; + private readonly string _clusterName; + private readonly string _configFilePath; + + public ECSFargateDeploymentTest() + { + _httpHelper = new HttpHelper(); + + var cloudFormationClient = new AmazonCloudFormationClient(); + _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); + + var ecsClient = new AmazonECSClient(); + _ecsHelper = new ECSHelper(ecsClient); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _configFilePath = Path.Combine("ConfigFileDeployment", "TestFiles", "IntegrationTestFiles", "ECSFargateConfigFile.json"); + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(_configFilePath); + + _stackName = userDeploymentSettings.StackName; + _clusterName = userDeploymentSettings.LeafOptionSettingItems["ECSCluster.NewClusterName"]; + + _app = serviceProvider.GetService(); + Assert.NotNull(_app); + + _interactiveService = serviceProvider.GetService(); + Assert.NotNull(_interactiveService); + } + + [Fact] + public async Task PerformDeployment() + { + // Deploy + var projectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", _configFilePath, "--silent" }; + await _app.Run(deployArgs); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var cluster = await _ecsHelper.GetCluster(_clusterName); + Assert.Equal("ACTIVE", cluster.Status); + Assert.Equal(cluster.ClusterName, _clusterName); + + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + + // Example: WebAppWithDockerFile3d078c3ca551.FargateServiceServiceURL47701F45 = http://WebAp-Farga-12O3W5VNB5OLC-166471465.us-west-2.elb.amazonaws.com + var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.FargateServiceServiceURL")) + .Split("=")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // list + var listArgs = new[] { "list-deployments" }; + await _app.Run(listArgs); + + // Verify stack exists in list of deployments + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + + // Arrange input for delete + await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete + await _interactiveService.StdInWriter.FlushAsync(); + var deleteArgs = new[] { "delete-deployment", _stackName }; + + // Delete + await _app.Run(deleteArgs); + + // Verify application is delete + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + var isStackDeleted = _cloudFormationHelper.IsStackDeleted(_stackName).GetAwaiter().GetResult(); + if (!isStackDeleted) + { + _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); + } + } + + _isDisposed = true; + } + + ~ECSFargateDeploymentTest() + { + Dispose(false); + } + } + + + + +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs new file mode 100644 index 000000000..646dce09e --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Amazon.CloudFormation; +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.Common; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Environment = System.Environment; + +namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment +{ + [Collection("Serial")] + public class ElasticBeanStalkDeploymentTest : IDisposable + { + private readonly HttpHelper _httpHelper; + private readonly CloudFormationHelper _cloudFormationHelper; + private readonly App _app; + private readonly InMemoryInteractiveService _interactiveService; + private bool _isDisposed; + private readonly string _stackName; + private readonly string _configFilePath; + + public ElasticBeanStalkDeploymentTest() + { + _httpHelper = new HttpHelper(); + + var cloudFormationClient = new AmazonCloudFormationClient(); + _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _configFilePath = Path.Combine("ConfigFileDeployment", "TestFiles", "IntegrationTestFiles", "ElasticBeanStalkConfigFile.json"); + var userDeploymentSettings = UserDeploymentSettings.ReadSettings(_configFilePath); + + _stackName = userDeploymentSettings.StackName; + + _app = serviceProvider.GetService(); + Assert.NotNull(_app); + + _interactiveService = serviceProvider.GetService(); + Assert.NotNull(_interactiveService); + } + + [Fact] + public async Task PerformDeployment() + { + // Deploy + var projectPath = Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj"); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", _configFilePath, "--silent" }; + await _app.Run(deployArgs); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + + // Example: WebAppNoDockerFile-3cf258f103d2.EndpointURL = http://52.36.216.238/ + var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.EndpointURL")) + .Split("=")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // list + var listArgs = new[] { "list-deployments" }; + await _app.Run(listArgs); + + // Verify stack exists in list of deployments + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + + // Arrange input for delete + await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete + await _interactiveService.StdInWriter.FlushAsync(); + var deleteArgs = new[] { "delete-deployment", _stackName }; + + // Delete + await _app.Run(deleteArgs); + + // Verify application is delete + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + var isStackDeleted = _cloudFormationHelper.IsStackDeleted(_stackName).GetAwaiter().GetResult(); + if (!isStackDeleted) + { + _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); + } + } + + _isDisposed = true; + } + + ~ElasticBeanStalkDeploymentTest() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/TestToolInteractiveServiceImpl.cs b/test/AWS.Deploy.CLI.UnitTests/TestToolInteractiveServiceImpl.cs index 90682f24c..e96013216 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TestToolInteractiveServiceImpl.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TestToolInteractiveServiceImpl.cs @@ -81,6 +81,8 @@ public void QueueConsoleInfos(params ConsoleKey[] keys) } public Queue InputConsoleKeyInfos { get; } = new Queue(); + public bool DisableInteractive { get; set; } + public ConsoleKeyInfo ReadKey(bool intercept) { if(InputConsoleKeyInfos.Count == 0) diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolCommandLineWrapper.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolCommandLineWrapper.cs index c733554d4..747459e6b 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolCommandLineWrapper.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolCommandLineWrapper.cs @@ -57,7 +57,8 @@ public Task Run( Action onComplete = null, bool redirectIO = true, IDictionary environmentVariables = null, - CancellationToken cancelToken = default) + CancellationToken cancelToken = default, + bool needAwsCredentials = false) { CommandsToExecute.Add(new CommandLineRunObject { diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestCommandLineWrapper.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestCommandLineWrapper.cs index de4f394d0..fc2358ffb 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestCommandLineWrapper.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestCommandLineWrapper.cs @@ -23,7 +23,8 @@ public Task Run( Action onComplete = null, bool redirectIO = true, IDictionary environmentVariables = null, - CancellationToken cancelToken = default) + CancellationToken cancelToken = default, + bool needAwsCredentials = false) { Commands.Add((command, workingDirectory, streamOutputToInteractiveService)); onComplete?.Invoke(Results.Last()); diff --git a/version.json b/version.json index b4efb1c30..d1fe0a04a 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.10", + "version": "0.11", "publicReleaseRefSpec": [ ".*" ],