diff --git a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj
index f72b4f42f..5bf7a043c 100644
--- a/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj
+++ b/src/AWS.Deploy.CLI/AWS.Deploy.CLI.csproj
@@ -12,6 +12,7 @@
AWS;Amazon;ElasticBeanstalk;ECS;Deploy
AWS.Deploy.CLI
AWS.Deploy.CLI
+ Latest
icon.png
https://github.com/aws/aws-dotnet-deploy
true
@@ -29,8 +30,9 @@
-
+
+
diff --git a/src/AWS.Deploy.CLI/App.cs b/src/AWS.Deploy.CLI/App.cs
index 1a06b8ca1..5af78ea9e 100644
--- a/src/AWS.Deploy.CLI/App.cs
+++ b/src/AWS.Deploy.CLI/App.cs
@@ -1,96 +1,96 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-using System;
-using System.CommandLine;
-using System.Reflection;
-using System.Text;
using System.Threading.Tasks;
using AWS.Deploy.CLI.Commands;
+using AWS.Deploy.CLI.Utilities;
+using AWS.Deploy.Common;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
-namespace AWS.Deploy.CLI
+namespace AWS.Deploy.CLI;
+
+public class App
{
- public class App
+ public static CommandApp ConfigureServices(TypeRegistrar registrar)
{
- private readonly ICommandFactory _commandFactory;
- private readonly IToolInteractiveService _toolInteractiveService;
-
- public App(ICommandFactory commandFactory, IToolInteractiveService toolInteractiveService)
- {
- _commandFactory = commandFactory;
- _toolInteractiveService = toolInteractiveService;
- }
+ var app = new CommandApp(registrar);
- public async Task Run(string[] args)
+ app.Configure(config =>
{
- Console.OutputEncoding = Encoding.UTF8;
-
- SetExecutionEnvironment(args);
-
- _toolInteractiveService.WriteLine("AWS .NET deployment tool for deploying .NET Core applications to AWS.");
- _toolInteractiveService.WriteLine("Project Home: https://github.com/aws/aws-dotnet-deploy");
- _toolInteractiveService.WriteLine(string.Empty);
-
- // if user didn't specify a command, default to help
- if (args.Length == 0)
+ config.SetApplicationName(Constants.CLI.TOOL_NAME);
+ config.AddCommand("deploy")
+ .WithDescription("Inspect, build, and deploy the .NET project to AWS using the recommended AWS service.");
+ config.AddCommand("list-deployments")
+ .WithDescription("List existing deployments.");
+ config.AddCommand("delete-deployment")
+ .WithDescription("Delete an existing deployment.");
+ config.AddBranch("deployment-project", deploymentProject =>
{
- args = new[] { "-h" };
- }
-
- return await _commandFactory.BuildRootCommand().InvokeAsync(args);
- }
-
- ///
- /// Set up the execution environment variable picked up by the AWS .NET SDK. This can be useful for identify calls
- /// made by this tool in AWS CloudTrail.
- ///
- private static void SetExecutionEnvironment(string[] args)
- {
- const string envName = "AWS_EXECUTION_ENV";
-
- var toolVersion = GetToolVersion();
-
- // The leading and trailing whitespaces are intentional
- var userAgent = $" lib/aws-dotnet-deploy-cli#{toolVersion} ";
- if (args?.Length > 0)
+ deploymentProject.SetDescription("Save the deployment project inside a user provided directory path.");
+ deploymentProject.AddCommand("generate")
+ .WithDescription("Save the deployment project inside a user provided directory path without proceeding with a deployment");
+ });
+ config.AddCommand("server-mode")
+ .WithDescription("Launches the tool in a server mode for IDEs like Visual Studio to integrate with.");
+
+ config.SetExceptionHandler((exception, _) =>
{
- // The trailing whitespace is intentional
- userAgent = $"{userAgent}md/cli-args#{args[0]} ";
- }
-
-
- var envValue = new StringBuilder();
- var existingValue = Environment.GetEnvironmentVariable(envName);
-
- // If there is an existing execution environment variable add this tool as a suffix.
- if (!string.IsNullOrEmpty(existingValue))
- {
- envValue.Append(existingValue);
- }
+ var serviceProvider = registrar.GetServiceProvider();;
+ var toolInteractiveService = serviceProvider.GetRequiredService();
+
+ if (exception.IsAWSDeploymentExpectedException())
+ {
+ if (toolInteractiveService.Diagnostics)
+ toolInteractiveService.WriteErrorLine(exception.PrettyPrint());
+ else
+ {
+ toolInteractiveService.WriteErrorLine(string.Empty);
+ toolInteractiveService.WriteErrorLine(exception.Message);
+ }
+
+ toolInteractiveService.WriteErrorLine(string.Empty);
+ toolInteractiveService.WriteErrorLine("For more information, please visit our troubleshooting guide https://aws.github.io/aws-dotnet-deploy/troubleshooting-guide/.");
+ toolInteractiveService.WriteErrorLine("If you are still unable to solve this issue and believe this is an issue with the tooling, please cut a ticket https://github.com/aws/aws-dotnet-deploy/issues/new/choose.");
+
+ if (exception is TcpPortInUseException)
+ {
+ return CommandReturnCodes.TCP_PORT_ERROR;
+ }
+
+ // bail out with an non-zero return code.
+ return CommandReturnCodes.USER_ERROR;
+ }
+ else
+ {
+ // This is a bug
+ toolInteractiveService.WriteErrorLine(
+ "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
+ exception.PrettyPrint());
+
+ return CommandReturnCodes.UNHANDLED_EXCEPTION;
+ }
+ });
+ });
+
+ return app;
+ }
- envValue.Append(userAgent);
+ public static async Task RunAsync(string[] args, CommandApp app, TypeRegistrar registrar)
+ {
+ var serviceProvider = registrar.GetServiceProvider();;
+ var toolInteractiveService = serviceProvider.GetRequiredService();
- Environment.SetEnvironmentVariable(envName, envValue.ToString());
- }
+ toolInteractiveService.WriteLine("AWS .NET deployment tool for deploying .NET Core applications to AWS.");
+ toolInteractiveService.WriteLine("Project Home: https://github.com/aws/aws-dotnet-deploy");
+ toolInteractiveService.WriteLine(string.Empty);
- internal static string GetToolVersion()
+ // if user didn't specify a command, default to help
+ if (args.Length == 0)
{
- var assembly = typeof(App).GetTypeInfo().Assembly;
- var version = assembly.GetCustomAttribute()?.Version;
- if (version is null)
- {
- return string.Empty;
- }
-
- var versionParts = version.Split('.');
- if (versionParts.Length == 4)
- {
- // The revision part of the version number is intentionally set to 0 since package versioning on
- // NuGet follows semantic versioning consisting only of Major.Minor.Patch versions.
- versionParts[3] = "0";
- }
-
- return string.Join(".", versionParts);
+ args = ["-h"];
}
+
+ return await app.RunAsync(args);
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/CancellableAsyncCommand.cs b/src/AWS.Deploy.CLI/Commands/CancellableAsyncCommand.cs
new file mode 100644
index 000000000..4929a4eb6
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/CancellableAsyncCommand.cs
@@ -0,0 +1,41 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Spectre.Console.Cli;
+
+///
+/// Provides an abstract base class for asynchronous commands that support cancellation.
+///
+/// The type of the settings used for the command.
+public abstract class CancellableAsyncCommand : AsyncCommand where TSettings : CommandSettings
+{
+ ///
+ /// Executes the command asynchronously, with support for cancellation.
+ ///
+ public abstract Task ExecuteAsync(CommandContext context, TSettings settings, CancellationTokenSource cancellationTokenSource);
+
+ ///
+ /// Executes the command asynchronously with built-in cancellation handling.
+ ///
+ public sealed override async Task ExecuteAsync(CommandContext context, TSettings settings)
+ {
+ using var cancellationSource = new CancellationTokenSource();
+
+ using var sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, onSignal);
+ using var sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, onSignal);
+ using var sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, onSignal);
+
+ var cancellable = ExecuteAsync(context, settings, cancellationSource);
+ return await cancellable;
+
+ void onSignal(PosixSignalContext context)
+ {
+ context.Cancel = true;
+ cancellationSource.Cancel();
+ }
+ }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs
deleted file mode 100644
index ffc89b699..000000000
--- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs
+++ /dev/null
@@ -1,587 +0,0 @@
-// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-using System;
-using System.CommandLine;
-using System.CommandLine.Invocation;
-using System.IO;
-using Amazon;
-using AWS.Deploy.CLI.Commands.TypeHints;
-using AWS.Deploy.CLI.Utilities;
-using AWS.Deploy.Common;
-using AWS.Deploy.Common.Extensions;
-using AWS.Deploy.Orchestration;
-using AWS.Deploy.Orchestration.CDK;
-using AWS.Deploy.Orchestration.Utilities;
-using AWS.Deploy.CLI.Commands.CommandHandlerInput;
-using AWS.Deploy.Common.IO;
-using AWS.Deploy.Common.DeploymentManifest;
-using AWS.Deploy.Orchestration.DisplayedResources;
-using AWS.Deploy.Orchestration.LocalUserSettings;
-using AWS.Deploy.Orchestration.ServiceHandlers;
-using AWS.Deploy.Common.Recipes;
-using AWS.Deploy.Common.Recipes.Validation;
-using AWS.Deploy.Common.Data;
-using AWS.Deploy.Orchestration.Docker;
-
-namespace AWS.Deploy.CLI.Commands
-{
- public interface ICommandFactory
- {
- Command BuildRootCommand();
- }
-
- public class CommandFactory : ICommandFactory
- {
- private static readonly Option _optionVersion = new(new[] { "-v", "--version" }, "Show version information");
- private static readonly Option _optionProfile = new("--profile", "AWS credential profile used to make calls to AWS.");
- private static readonly Option _optionRegion = new("--region", "AWS region to deploy the application to. For example, us-west-2.");
- private static readonly Option _optionProjectPath = new("--project-path", () => Directory.GetCurrentDirectory(), "Path to the project to deploy.");
- private static readonly Option _optionApplicationName = new("--application-name", "Name of the cloud application. If you choose to deploy via CloudFormation, this name will be used to identify the CloudFormation stack.");
- 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 execute commands without any prompts for user input.");
- private static readonly Option _optionOutputDirectory = new(new[] { "-o", "--output" }, "Directory path in which the CDK deployment project will be saved.");
- private static readonly Option _optionProjectDisplayName = new(new[] { "--project-display-name" }, "The name of the deployment project that will be displayed in the list of available deployment options.");
- private static readonly Option _optionDeploymentProject = new(new[] { "--deployment-project" }, "The absolute or relative path of the CDK project that will be used for deployment");
- private static readonly Option _optionSaveSettings = new(new[] { "--save-settings" }, "The absolute or the relative JSON file path where the deployment settings will be saved. Only the settings modified by the user will be persisted");
- private static readonly Option _optionSaveAllSettings = new(new[] { "--save-all-settings" }, "The absolute or the relative JSON file path where the deployment settings will be saved. All deployment settings will be persisted");
- private static readonly object s_root_command_lock = new();
- private static readonly object s_child_command_lock = new();
-
- private readonly IServiceProvider _serviceProvider;
- private readonly IToolInteractiveService _toolInteractiveService;
- private readonly IOrchestratorInteractiveService _orchestratorInteractiveService;
- private readonly ICDKManager _cdkManager;
- private readonly ISystemCapabilityEvaluator _systemCapabilityEvaluator;
- private readonly ICloudApplicationNameGenerator _cloudApplicationNameGenerator;
- private readonly IAWSUtilities _awsUtilities;
- private readonly IAWSClientFactory _awsClientFactory;
- private readonly IAWSResourceQueryer _awsResourceQueryer;
- private readonly IProjectParserUtility _projectParserUtility;
- private readonly ICommandLineWrapper _commandLineWrapper;
- private readonly ICdkProjectHandler _cdkProjectHandler;
- private readonly IDeploymentBundleHandler _deploymentBundleHandler;
- private readonly ICloudFormationTemplateReader _cloudFormationTemplateReader;
- private readonly IDeployedApplicationQueryer _deployedApplicationQueryer;
- private readonly ITypeHintCommandFactory _typeHintCommandFactory;
- private readonly IDisplayedResourcesHandler _displayedResourceHandler;
- private readonly IConsoleUtilities _consoleUtilities;
- private readonly IDirectoryManager _directoryManager;
- private readonly IFileManager _fileManager;
- private readonly IDeploymentManifestEngine _deploymentManifestEngine;
- private readonly ILocalUserSettingsEngine _localUserSettingsEngine;
- private readonly ICDKVersionDetector _cdkVersionDetector;
- private readonly IAWSServiceHandler _awsServiceHandler;
- private readonly IOptionSettingHandler _optionSettingHandler;
- private readonly IValidatorFactory _validatorFactory;
- private readonly IRecipeHandler _recipeHandler;
- private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata;
- private readonly IDeploymentSettingsHandler _deploymentSettingsHandler;
-
- public CommandFactory(
- IServiceProvider serviceProvider,
- IToolInteractiveService toolInteractiveService,
- IOrchestratorInteractiveService orchestratorInteractiveService,
- ICDKManager cdkManager,
- ISystemCapabilityEvaluator systemCapabilityEvaluator,
- ICloudApplicationNameGenerator cloudApplicationNameGenerator,
- IAWSUtilities awsUtilities,
- IAWSClientFactory awsClientFactory,
- IAWSResourceQueryer awsResourceQueryer,
- IProjectParserUtility projectParserUtility,
- ICommandLineWrapper commandLineWrapper,
- ICdkProjectHandler cdkProjectHandler,
- IDeploymentBundleHandler deploymentBundleHandler,
- ICloudFormationTemplateReader cloudFormationTemplateReader,
- IDeployedApplicationQueryer deployedApplicationQueryer,
- ITypeHintCommandFactory typeHintCommandFactory,
- IDisplayedResourcesHandler displayedResourceHandler,
- IConsoleUtilities consoleUtilities,
- IDirectoryManager directoryManager,
- IFileManager fileManager,
- IDeploymentManifestEngine deploymentManifestEngine,
- ILocalUserSettingsEngine localUserSettingsEngine,
- ICDKVersionDetector cdkVersionDetector,
- IAWSServiceHandler awsServiceHandler,
- IOptionSettingHandler optionSettingHandler,
- IValidatorFactory validatorFactory,
- IRecipeHandler recipeHandler,
- IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata,
- IDeploymentSettingsHandler deploymentSettingsHandler)
- {
- _serviceProvider = serviceProvider;
- _toolInteractiveService = toolInteractiveService;
- _orchestratorInteractiveService = orchestratorInteractiveService;
- _cdkManager = cdkManager;
- _systemCapabilityEvaluator = systemCapabilityEvaluator;
- _cloudApplicationNameGenerator = cloudApplicationNameGenerator;
- _awsUtilities = awsUtilities;
- _awsClientFactory = awsClientFactory;
- _awsResourceQueryer = awsResourceQueryer;
- _projectParserUtility = projectParserUtility;
- _commandLineWrapper = commandLineWrapper;
- _cdkProjectHandler = cdkProjectHandler;
- _deploymentBundleHandler = deploymentBundleHandler;
- _cloudFormationTemplateReader = cloudFormationTemplateReader;
- _deployedApplicationQueryer = deployedApplicationQueryer;
- _typeHintCommandFactory = typeHintCommandFactory;
- _displayedResourceHandler = displayedResourceHandler;
- _consoleUtilities = consoleUtilities;
- _directoryManager = directoryManager;
- _fileManager = fileManager;
- _deploymentManifestEngine = deploymentManifestEngine;
- _localUserSettingsEngine = localUserSettingsEngine;
- _cdkVersionDetector = cdkVersionDetector;
- _awsServiceHandler = awsServiceHandler;
- _optionSettingHandler = optionSettingHandler;
- _validatorFactory = validatorFactory;
- _recipeHandler = recipeHandler;
- _deployToolWorkspaceMetadata = deployToolWorkspaceMetadata;
- _deploymentSettingsHandler = deploymentSettingsHandler;
- }
-
- public Command BuildRootCommand()
- {
- // Name is important to set here to show correctly in the CLI usage help.
- // Either dotnet-aws or dotnet aws works from the CLI. System.Commandline's help system does not like having a space with dotnet aws.
- var rootCommand = new RootCommand
- {
- Name = "dotnet-aws",
- Description = "The AWS .NET deployment tool for deploying .NET applications on AWS."
- };
-
- lock(s_root_command_lock)
- {
- rootCommand.AddOption(_optionVersion);
- rootCommand.Add(BuildDeployCommand());
- rootCommand.Add(BuildListCommand());
- rootCommand.Add(BuildDeleteCommand());
- rootCommand.Add(BuildDeploymentProjectCommand());
- rootCommand.Add(BuildServerModeCommand());
- }
-
- rootCommand.Handler = CommandHandler.Create((bool version) =>
- {
- if (version)
- {
- var toolVersion = App.GetToolVersion();
- _toolInteractiveService.WriteLine($"Version: {toolVersion}");
- }
- });
-
- return rootCommand;
- }
-
- private Command BuildDeployCommand()
- {
- var deployCommand = new Command(
- "deploy",
- "Inspect, build, and deploy the .NET project to AWS using the recommended AWS service.");
-
- lock (s_child_command_lock)
- {
- deployCommand.Add(_optionProfile);
- deployCommand.Add(_optionRegion);
- deployCommand.Add(_optionProjectPath);
- deployCommand.Add(_optionApplicationName);
- deployCommand.Add(_optionApply);
- deployCommand.Add(_optionDiagnosticLogging);
- deployCommand.Add(_optionDisableInteractive);
- deployCommand.Add(_optionDeploymentProject);
- deployCommand.Add(_optionSaveSettings);
- deployCommand.Add(_optionSaveAllSettings);
- }
-
- deployCommand.Handler = CommandHandler.Create(async (DeployCommandHandlerInput input) =>
- {
- try
- {
- _toolInteractiveService.Diagnostics = input.Diagnostics;
- _toolInteractiveService.DisableInteractive = input.Silent;
-
- var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? "");
- var targetApplicationDirectoryPath = new DirectoryInfo(projectDefinition.ProjectPath).Parent!.FullName;
-
- DeploymentSettings? deploymentSettings = null;
- if (!string.IsNullOrEmpty(input.Apply))
- {
- var applyPath = Path.GetFullPath(input.Apply, targetApplicationDirectoryPath);
- deploymentSettings = await _deploymentSettingsHandler.ReadSettings(applyPath);
- }
-
- var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile ?? deploymentSettings?.AWSProfile);
- var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? deploymentSettings?.AWSRegion ?? regionFromProfile);
-
- _commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion);
- _awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion);
-
- var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(awsRegion);
-
- var session = new OrchestratorSession(
- projectDefinition,
- awsCredentials,
- awsRegion,
- callerIdentity.Account)
- {
- AWSProfileName = input.Profile ?? deploymentSettings?.AWSProfile ?? null
- };
-
- var dockerEngine = new DockerEngine(projectDefinition, _fileManager, _directoryManager);
-
- var deploy = new DeployCommand(
- _serviceProvider,
- _toolInteractiveService,
- _orchestratorInteractiveService,
- _cdkProjectHandler,
- _cdkManager,
- _cdkVersionDetector,
- _deploymentBundleHandler,
- dockerEngine,
- _awsResourceQueryer,
- _cloudFormationTemplateReader,
- _deployedApplicationQueryer,
- _typeHintCommandFactory,
- _displayedResourceHandler,
- _cloudApplicationNameGenerator,
- _localUserSettingsEngine,
- _consoleUtilities,
- _systemCapabilityEvaluator,
- session,
- _directoryManager,
- _fileManager,
- _awsServiceHandler,
- _optionSettingHandler,
- _validatorFactory,
- _recipeHandler,
- _deployToolWorkspaceMetadata,
- _deploymentSettingsHandler);
-
- var deploymentProjectPath = input.DeploymentProject ?? string.Empty;
- if (!string.IsNullOrEmpty(deploymentProjectPath))
- {
- deploymentProjectPath = Path.GetFullPath(deploymentProjectPath, targetApplicationDirectoryPath);
- }
-
- var saveSettingsConfig = Helpers.GetSaveSettingsConfiguration(input.SaveSettings, input.SaveAllSettings, targetApplicationDirectoryPath, _fileManager);
-
- await deploy.ExecuteAsync(input.ApplicationName ?? string.Empty, deploymentProjectPath, saveSettingsConfig, deploymentSettings);
-
- return CommandReturnCodes.SUCCESS;
- }
- catch (Exception e) when (e.IsAWSDeploymentExpectedException())
- {
- if (input.Diagnostics)
- _toolInteractiveService.WriteErrorLine(e.PrettyPrint());
- else
- {
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine(e.Message);
- }
-
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine("For more information, please visit our troubleshooting guide https://aws.github.io/aws-dotnet-deploy/troubleshooting-guide/.");
- _toolInteractiveService.WriteErrorLine("If you are still unable to solve this issue and believe this is an issue with the tooling, please cut a ticket https://github.com/aws/aws-dotnet-deploy/issues/new/choose.");
-
- // bail out with an non-zero return code.
- return CommandReturnCodes.USER_ERROR;
- }
- catch (Exception e)
- {
- // This is a bug
- _toolInteractiveService.WriteErrorLine(
- "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
- e.PrettyPrint());
-
- return CommandReturnCodes.UNHANDLED_EXCEPTION;
- }
- });
- return deployCommand;
- }
-
- private Command BuildDeleteCommand()
- {
- var deleteCommand = new Command("delete-deployment", "Delete an existing deployment.");
- lock (s_child_command_lock)
- {
- deleteCommand.Add(_optionProfile);
- deleteCommand.Add(_optionRegion);
- deleteCommand.Add(_optionProjectPath);
- deleteCommand.Add(_optionDiagnosticLogging);
- deleteCommand.Add(_optionDisableInteractive);
- deleteCommand.AddArgument(new Argument("deployment-name"));
- }
-
- deleteCommand.Handler = CommandHandler.Create(async (DeleteCommandHandlerInput input) =>
- {
- try
- {
- _toolInteractiveService.Diagnostics = input.Diagnostics;
- _toolInteractiveService.DisableInteractive = input.Silent;
-
- var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile);
- var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? regionFromProfile);
-
- _awsClientFactory.ConfigureAWSOptions(awsOption =>
- {
- awsOption.Credentials = awsCredentials;
- awsOption.Region = RegionEndpoint.GetBySystemName(awsRegion);
- });
-
- 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;
- }
-
- OrchestratorSession? session = null;
-
- try
- {
- var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? string.Empty);
-
- var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(awsRegion);
-
- session = new OrchestratorSession(
- projectDefinition,
- awsCredentials,
- awsRegion,
- callerIdentity.Account);
- }
- catch (FailedToFindDeployableTargetException) { }
-
- await new DeleteDeploymentCommand(
- _awsClientFactory,
- _toolInteractiveService,
- _consoleUtilities,
- _localUserSettingsEngine,
- session).ExecuteAsync(input.DeploymentName);
-
- return CommandReturnCodes.SUCCESS;
- }
- catch (Exception e) when (e.IsAWSDeploymentExpectedException())
- {
- if (input.Diagnostics)
- _toolInteractiveService.WriteErrorLine(e.PrettyPrint());
- else
- {
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine(e.Message);
- }
-
- // bail out with an non-zero return code.
- return CommandReturnCodes.USER_ERROR;
- }
- catch (Exception e)
- {
- // This is a bug
- _toolInteractiveService.WriteErrorLine(
- "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
- e.PrettyPrint());
-
- return CommandReturnCodes.UNHANDLED_EXCEPTION;
- }
- });
- return deleteCommand;
- }
-
- private Command BuildListCommand()
- {
- var listCommand = new Command("list-deployments", "List existing deployments.");
- lock (s_child_command_lock)
- {
- listCommand.Add(_optionProfile);
- listCommand.Add(_optionRegion);
- listCommand.Add(_optionDiagnosticLogging);
- }
-
- listCommand.Handler = CommandHandler.Create(async (ListCommandHandlerInput input) =>
- {
- try
- {
- _toolInteractiveService.Diagnostics = input.Diagnostics;
-
- var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(input.Profile);
- var awsRegion = _awsUtilities.ResolveAWSRegion(input.Region ?? regionFromProfile);
-
- _awsClientFactory.ConfigureAWSOptions(awsOptions =>
- {
- awsOptions.Credentials = awsCredentials;
- awsOptions.Region = RegionEndpoint.GetBySystemName(awsRegion);
- });
-
- await _awsResourceQueryer.GetCallerIdentity(awsRegion);
-
- var listDeploymentsCommand = new ListDeploymentsCommand(_toolInteractiveService, _deployedApplicationQueryer);
-
- await listDeploymentsCommand.ExecuteAsync();
- }
- catch (Exception e) when (e.IsAWSDeploymentExpectedException())
- {
- if (input.Diagnostics)
- _toolInteractiveService.WriteErrorLine(e.PrettyPrint());
- else
- {
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine(e.Message);
- }
- }
- catch (Exception e)
- {
- // This is a bug
- _toolInteractiveService.WriteErrorLine(
- "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
- e.PrettyPrint());
- }
- });
- return listCommand;
- }
-
- ///
- /// Builds the top level command called "deployment-project" which supports the creation and saving of the
- /// CDK deployment project.
- ///
- /// An instance of the class
- private Command BuildDeploymentProjectCommand()
- {
- var deploymentProjectCommand = new Command("deployment-project",
- "Save the deployment project inside a user provided directory path.");
-
- var generateDeploymentProjectCommand = new Command("generate",
- "Save the deployment project inside a user provided directory path without proceeding with a deployment");
-
- lock (s_child_command_lock)
- {
- generateDeploymentProjectCommand.Add(_optionOutputDirectory);
- generateDeploymentProjectCommand.Add(_optionDiagnosticLogging);
- generateDeploymentProjectCommand.Add(_optionProjectPath);
- generateDeploymentProjectCommand.Add(_optionProjectDisplayName);
- }
-
- generateDeploymentProjectCommand.Handler = CommandHandler.Create(async (GenerateDeploymentProjectCommandHandlerInput input) =>
- {
- try
- {
- _toolInteractiveService.Diagnostics = input.Diagnostics;
- var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? "");
-
- var saveDirectory = input.Output;
- var projectDisplayName = input.ProjectDisplayName;
-
- OrchestratorSession session = new OrchestratorSession(projectDefinition);
-
- var targetApplicationFullPath = new DirectoryInfo(projectDefinition.ProjectPath).FullName;
-
- if (!string.IsNullOrEmpty(saveDirectory))
- {
- var targetApplicationDirectoryFullPath = new DirectoryInfo(targetApplicationFullPath).Parent!.FullName;
- saveDirectory = Path.GetFullPath(saveDirectory, targetApplicationDirectoryFullPath);
- }
-
- var generateDeploymentProject = new GenerateDeploymentProjectCommand(
- _toolInteractiveService,
- _consoleUtilities,
- _cdkProjectHandler,
- _commandLineWrapper,
- _directoryManager,
- _fileManager,
- session,
- _deploymentManifestEngine,
- _recipeHandler,
- targetApplicationFullPath);
-
- await generateDeploymentProject.ExecuteAsync(saveDirectory, projectDisplayName);
-
- return CommandReturnCodes.SUCCESS;
- }
- catch (Exception e) when (e.IsAWSDeploymentExpectedException())
- {
- if (input.Diagnostics)
- _toolInteractiveService.WriteErrorLine(e.PrettyPrint());
- else
- {
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine(e.Message);
- }
-
- // bail out with an non-zero return code.
- return CommandReturnCodes.USER_ERROR;
- }
- catch (Exception e)
- {
- // This is a bug
- _toolInteractiveService.WriteErrorLine(
- "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
- e.PrettyPrint());
-
- return CommandReturnCodes.UNHANDLED_EXCEPTION;
- }
- });
-
- lock (s_child_command_lock)
- {
- deploymentProjectCommand.Add(generateDeploymentProjectCommand);
- }
-
- return deploymentProjectCommand;
- }
-
- private Command BuildServerModeCommand()
- {
- var serverModeCommand = new Command(
- "server-mode",
- "Launches the tool in a server mode for IDEs like Visual Studio to integrate with.");
-
- lock (s_child_command_lock)
- {
- serverModeCommand.Add(new Option(new[] { "--port" }, description: "Port the server mode will listen to."));
- serverModeCommand.Add(new Option(new[] { "--parent-pid" }, description: "The ID of the process that is launching server mode. Server mode will exit when the parent pid terminates."));
- serverModeCommand.Add(new Option(new[] { "--unsecure-mode" }, description: "If set the cli uses an unsecure mode without encryption."));
- serverModeCommand.Add(_optionDiagnosticLogging);
- }
-
- serverModeCommand.Handler = CommandHandler.Create(async (ServerModeCommandHandlerInput input) =>
- {
- try
- {
- _toolInteractiveService.Diagnostics = input.Diagnostics;
- var serverMode = new ServerModeCommand(_toolInteractiveService, input.Port, input.ParentPid, input.UnsecureMode);
-
- await serverMode.ExecuteAsync();
-
- return CommandReturnCodes.SUCCESS;
- }
- catch (Exception e) when (e.IsAWSDeploymentExpectedException())
- {
- if (input.Diagnostics)
- _toolInteractiveService.WriteErrorLine(e.PrettyPrint());
- else
- {
- _toolInteractiveService.WriteErrorLine(string.Empty);
- _toolInteractiveService.WriteErrorLine(e.Message);
- }
-
- if (e is TcpPortInUseException)
- {
- return CommandReturnCodes.TCP_PORT_ERROR;
- }
-
- // bail out with an non-zero return code.
- return CommandReturnCodes.USER_ERROR;
- }
- catch (Exception e)
- {
- // This is a bug
- _toolInteractiveService.WriteErrorLine(
- "Unhandled exception. This is a bug. Please copy the stack trace below and file a bug at https://github.com/aws/aws-dotnet-deploy. " +
- e.PrettyPrint());
-
- return CommandReturnCodes.UNHANDLED_EXCEPTION;
- }
- });
-
- return serverModeCommand;
- }
- }
-}
diff --git a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs
index 401def651..846ea01a6 100644
--- a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs
+++ b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs
@@ -3,203 +3,256 @@
using System;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
+using Amazon;
using Amazon.CloudFormation;
using Amazon.CloudFormation.Model;
using AWS.Deploy.CLI.CloudFormation;
+using AWS.Deploy.CLI.Commands.Settings;
+using AWS.Deploy.CLI.Utilities;
using AWS.Deploy.Common;
-using AWS.Deploy.Common.IO;
+using AWS.Deploy.Common.Data;
using AWS.Deploy.Orchestration;
using AWS.Deploy.Orchestration.LocalUserSettings;
+using Spectre.Console.Cli;
-namespace AWS.Deploy.CLI.Commands
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// Represents a Delete command allows to delete a CloudFormation stack
+///
+public class DeleteDeploymentCommand : CancellableAsyncCommand
{
+ private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5);
+
+ private readonly IAWSClientFactory _awsClientFactory;
+ private readonly IToolInteractiveService _interactiveService;
+ private readonly IAmazonCloudFormation _cloudFormationClient;
+ private readonly IConsoleUtilities _consoleUtilities;
+ private readonly ILocalUserSettingsEngine _localUserSettingsEngine;
+ private readonly IAWSUtilities _awsUtilities;
+ private readonly IProjectParserUtility _projectParserUtility;
+ private readonly IAWSResourceQueryer _awsResourceQueryer;
+ private const int MAX_RETRIES = 4;
+
///
- /// Represents a Delete command allows to delete a CloudFormation stack
+ /// Constructor for
///
- public class DeleteDeploymentCommand
+ public DeleteDeploymentCommand(
+ IAWSClientFactory awsClientFactory,
+ IToolInteractiveService interactiveService,
+ IConsoleUtilities consoleUtilities,
+ ILocalUserSettingsEngine localUserSettingsEngine,
+ IAWSUtilities awsUtilities,
+ IProjectParserUtility projectParserUtility,
+ IAWSResourceQueryer awsResourceQueryer)
{
- private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5);
-
- private readonly IAWSClientFactory _awsClientFactory;
- private readonly IToolInteractiveService _interactiveService;
- private readonly IAmazonCloudFormation _cloudFormationClient;
- private readonly IConsoleUtilities _consoleUtilities;
- private readonly ILocalUserSettingsEngine _localUserSettingsEngine;
- private readonly OrchestratorSession? _session;
- private const int MAX_RETRIES = 4;
-
- public DeleteDeploymentCommand(
- IAWSClientFactory awsClientFactory,
- IToolInteractiveService interactiveService,
- IConsoleUtilities consoleUtilities,
- ILocalUserSettingsEngine localUserSettingsEngine,
- OrchestratorSession? session)
+ _awsClientFactory = awsClientFactory;
+ _interactiveService = interactiveService;
+ _consoleUtilities = consoleUtilities;
+ _cloudFormationClient = _awsClientFactory.GetAWSClient();
+ _localUserSettingsEngine = localUserSettingsEngine;
+ _awsUtilities = awsUtilities;
+ _projectParserUtility = projectParserUtility;
+ _awsResourceQueryer = awsResourceQueryer;
+ }
+
+ ///
+ /// Deletes given CloudFormation stack
+ ///
+ /// Command context
+ /// Command settings
+ /// Cancellation token source
+ /// Thrown when deletion fails
+ /// The command exit code
+ public override async Task ExecuteAsync(CommandContext context, DeleteDeploymentCommandSettings settings, CancellationTokenSource cancellationTokenSource)
+ {
+ _interactiveService.Diagnostics = settings.Diagnostics;
+ _interactiveService.DisableInteractive = settings.Silent;
+
+ var (awsCredentials, regionFromProfile) = await _awsUtilities.ResolveAWSCredentials(settings.Profile);
+ var awsRegion = _awsUtilities.ResolveAWSRegion(settings.Region ?? regionFromProfile);
+
+ _awsClientFactory.ConfigureAWSOptions(awsOption =>
{
- _awsClientFactory = awsClientFactory;
- _interactiveService = interactiveService;
- _consoleUtilities = consoleUtilities;
- _cloudFormationClient = _awsClientFactory.GetAWSClient();
- _localUserSettingsEngine = localUserSettingsEngine;
- _session = session;
- }
+ awsOption.Credentials = awsCredentials;
+ awsOption.Region = RegionEndpoint.GetBySystemName(awsRegion);
+ });
- ///
- /// Deletes given CloudFormation stack
- ///
- /// The stack name to be deleted
- /// Thrown when deletion fails
- public async Task ExecuteAsync(string stackName)
+ if (string.IsNullOrEmpty(settings.DeploymentName))
{
- var canDelete = await CanDeleteAsync(stackName);
- if (!canDelete)
- {
- return;
- }
+ _interactiveService.WriteErrorLine(string.Empty);
+ _interactiveService.WriteErrorLine("Deployment name cannot be empty. Please provide a valid deployment name and try again.");
+ return CommandReturnCodes.USER_ERROR;
+ }
- var confirmDelete = _interactiveService.DisableInteractive
- ? YesNo.Yes
- : _consoleUtilities.AskYesNoQuestion($"Are you sure you want to delete {stackName}?", YesNo.No);
+ OrchestratorSession? session = null;
- if (confirmDelete == YesNo.No)
- {
- return;
- }
+ try
+ {
+ var projectDefinition = await _projectParserUtility.Parse(settings.ProjectPath);
- _interactiveService.WriteLine($"{stackName}: deleting...");
- var monitor = new StackEventMonitor(stackName, _awsClientFactory, _consoleUtilities, _interactiveService);
+ var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(awsRegion);
- try
- {
- await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest
- {
- StackName = stackName
- });
-
- // Fire and forget the monitor
- // Monitor updates the stdout with current status of the CloudFormation stack
- var _ = monitor.StartAsync();
+ session = new OrchestratorSession(
+ projectDefinition,
+ awsCredentials,
+ awsRegion,
+ callerIdentity.Account);
+ }
+ catch (FailedToFindDeployableTargetException) { }
- await WaitForStackDelete(stackName);
+ var canDelete = await CanDeleteAsync(settings.DeploymentName);
+ if (!canDelete)
+ {
+ return CommandReturnCodes.SUCCESS;
+ }
- if (_session != null)
- {
- await _localUserSettingsEngine.DeleteLastDeployedStack(stackName, _session.ProjectDefinition.ProjectName, _session.AWSAccountId, _session.AWSRegion);
- }
+ var confirmDelete = _interactiveService.DisableInteractive
+ ? YesNo.Yes
+ : _consoleUtilities.AskYesNoQuestion($"Are you sure you want to delete {settings.DeploymentName}?", YesNo.No);
- _interactiveService.WriteLine($"{stackName}: deleted");
- }
- finally
- {
- // Stop monitoring CloudFormation stack status once the deletion operation finishes
- monitor.Stop();
- }
+ if (confirmDelete == YesNo.No)
+ {
+ return CommandReturnCodes.SUCCESS;
}
- private async Task CanDeleteAsync(string stackName)
+ _interactiveService.WriteLine($"{settings.DeploymentName}: deleting...");
+ var monitor = new StackEventMonitor(settings.DeploymentName, _awsClientFactory, _consoleUtilities, _interactiveService);
+
+ try
{
- var stack = await GetStackAsync(stackName);
- if (stack == null)
+ await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest
{
- _interactiveService.WriteErrorLine($"Stack with name {stackName} does not exist.");
- return false;
- }
+ StackName = settings.DeploymentName
+ });
+
+ // Fire and forget the monitor
+ // Monitor updates the stdout with current status of the CloudFormation stack
+ var _ = monitor.StartAsync();
+
+ await WaitForStackDelete(settings.DeploymentName);
- var canDelete = stack.Tags.Any(tag => tag.Key.Equals(Constants.CloudFormationIdentifier.STACK_TAG));
- if (!canDelete)
+ if (session != null)
{
- _interactiveService.WriteErrorLine("Only stacks that were deployed with this tool can be deleted.");
+ await _localUserSettingsEngine.DeleteLastDeployedStack(settings.DeploymentName, session.ProjectDefinition.ProjectName, session.AWSAccountId, session.AWSRegion);
}
- return canDelete;
+ _interactiveService.WriteLine($"{settings.DeploymentName}: deleted");
}
-
- private async Task WaitForStackDelete(string stackName)
+ finally
{
- var stack = await StabilizeStack(stackName);
- if (stack == null)
- {
- return;
- }
+ // Stop monitoring CloudFormation stack status once the deletion operation finishes
+ monitor.Stop();
+ }
- if (stack.StackStatus.IsDeleted())
- {
- return;
- }
+ return CommandReturnCodes.SUCCESS;
+ }
- if (stack.StackStatus.IsFailed())
- {
- throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"The stack {stackName} is in a failed state. You may need to delete it from the AWS Console.");
- }
+ private async Task CanDeleteAsync(string stackName)
+ {
+ var stack = await GetStackAsync(stackName);
+ if (stack == null)
+ {
+ _interactiveService.WriteErrorLine($"Stack with name {stackName} does not exist.");
+ return false;
+ }
- throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"Failed to delete {stackName} stack: {stack.StackStatus}");
+ var canDelete = stack.Tags.Any(tag => tag.Key.Equals(Constants.CloudFormationIdentifier.STACK_TAG));
+ if (!canDelete)
+ {
+ _interactiveService.WriteErrorLine("Only stacks that were deployed with this tool can be deleted.");
}
- private async Task StabilizeStack(string stackName)
+ return canDelete;
+ }
+
+ private async Task WaitForStackDelete(string stackName)
+ {
+ var stack = await StabilizeStack(stackName);
+ if (stack == null)
{
- Stack? stack;
- do
- {
- stack = await GetStackAsync(stackName);
- if (stack == null)
- {
- return null;
- }
- await Task.Delay(s_pollingPeriod);
- } while (stack.StackStatus.IsInProgress());
+ return;
+ }
- return stack;
+ if (stack.StackStatus.IsDeleted())
+ {
+ return;
}
- private async Task GetStackAsync(string stackName)
+ if (stack.StackStatus.IsFailed())
{
- var retryCount = 0;
- var shouldRetry = false;
+ throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"The stack {stackName} is in a failed state. You may need to delete it from the AWS Console.");
+ }
+
+ throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"Failed to delete {stackName} stack: {stack.StackStatus}");
+ }
- Stack? stack = null;
- do
+ private async Task StabilizeStack(string stackName)
+ {
+ Stack? stack;
+ do
+ {
+ stack = await GetStackAsync(stackName);
+ if (stack == null)
{
- var waitTime = GetWaitTime(retryCount);
- try
- {
- await Task.Delay(waitTime);
+ return null;
+ }
+ await Task.Delay(s_pollingPeriod);
+ } while (stack.StackStatus.IsInProgress());
- var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest
- {
- StackName = stackName
- });
+ return stack;
+ }
- stack = response.Stacks.Count == 0 ? null : response.Stacks[0];
- shouldRetry = false;
- }
- catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist"))
- {
- _interactiveService.WriteDebugLine(exception.PrettyPrint());
- shouldRetry = false;
- }
- catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("Throttling"))
- {
- _interactiveService.WriteDebugLine(exception.PrettyPrint());
- shouldRetry = true;
- }
- } while (shouldRetry && retryCount++ < MAX_RETRIES);
+ private async Task GetStackAsync(string stackName)
+ {
+ var retryCount = 0;
+ bool shouldRetry;
- return stack;
- }
+ Stack? stack = null;
+ do
+ {
+ var waitTime = GetWaitTime(retryCount);
+ try
+ {
+ await Task.Delay(waitTime);
- ///
- /// Returns the next wait interval, in milliseconds, using an exponential backoff algorithm
- /// Read more here https://docs.aws.amazon.com/general/latest/gr/api-retries.html
- ///
- ///
- ///
- private static TimeSpan GetWaitTime(int retryCount) {
- if (retryCount == 0) {
- return TimeSpan.Zero;
+ var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest
+ {
+ StackName = stackName
+ });
+
+ stack = response.Stacks.Count == 0 ? null : response.Stacks[0];
+ shouldRetry = false;
+ }
+ catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist"))
+ {
+ _interactiveService.WriteDebugLine(exception.PrettyPrint());
+ shouldRetry = false;
+ }
+ catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("Throttling"))
+ {
+ _interactiveService.WriteDebugLine(exception.PrettyPrint());
+ shouldRetry = true;
}
+ } while (shouldRetry && retryCount++ < MAX_RETRIES);
- var waitTime = Math.Pow(2, retryCount) * 5;
- return TimeSpan.FromSeconds(waitTime);
+ return stack;
+ }
+
+ ///
+ /// Returns the next wait interval, in milliseconds, using an exponential backoff algorithm
+ /// Read more here https://docs.aws.amazon.com/general/latest/gr/api-retries.html
+ ///
+ ///
+ ///
+ private static TimeSpan GetWaitTime(int retryCount) {
+ if (retryCount == 0) {
+ return TimeSpan.Zero;
}
+
+ var waitTime = Math.Pow(2, retryCount) * 5;
+ return TimeSpan.FromSeconds(waitTime);
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs
index 2371127d5..5b512d43c 100644
--- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs
+++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs
@@ -6,866 +6,866 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
+using AWS.Deploy.CLI.Commands.Settings;
using AWS.Deploy.CLI.Commands.TypeHints;
+using AWS.Deploy.CLI.Utilities;
using AWS.Deploy.Common;
using AWS.Deploy.Common.Extensions;
using AWS.Deploy.Common.Recipes;
-using AWS.Deploy.Common.Recipes.Validation;
using AWS.Deploy.Orchestration;
using AWS.Deploy.Orchestration.CDK;
-using AWS.Deploy.Recipes;
-using AWS.Deploy.Orchestration.Data;
using AWS.Deploy.Orchestration.Utilities;
using AWS.Deploy.Orchestration.DisplayedResources;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Orchestration.LocalUserSettings;
using Newtonsoft.Json;
using AWS.Deploy.Orchestration.ServiceHandlers;
-using Microsoft.Extensions.DependencyInjection;
using AWS.Deploy.Common.Data;
using AWS.Deploy.Orchestration.Docker;
-
-namespace AWS.Deploy.CLI.Commands
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// Represents a Deploy command that allows deploying applications
+///
+public class DeployCommand(
+ IToolInteractiveService toolInteractiveService,
+ IOrchestratorInteractiveService orchestratorInteractiveService,
+ ICdkProjectHandler cdkProjectHandler,
+ ICDKManager cdkManager,
+ ICDKVersionDetector cdkVersionDetector,
+ IDeploymentBundleHandler deploymentBundleHandler,
+ IAWSResourceQueryer awsResourceQueryer,
+ ICloudFormationTemplateReader cloudFormationTemplateReader,
+ IDeployedApplicationQueryer deployedApplicationQueryer,
+ ITypeHintCommandFactory typeHintCommandFactory,
+ IDisplayedResourcesHandler displayedResourcesHandler,
+ ICloudApplicationNameGenerator cloudApplicationNameGenerator,
+ ILocalUserSettingsEngine localUserSettingsEngine,
+ IConsoleUtilities consoleUtilities,
+ ISystemCapabilityEvaluator systemCapabilityEvaluator,
+ IDirectoryManager directoryManager,
+ IFileManager fileManager,
+ IAWSServiceHandler awsServiceHandler,
+ IOptionSettingHandler optionSettingHandler,
+ IRecipeHandler recipeHandler,
+ IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata,
+ IDeploymentSettingsHandler deploymentSettingsHandler,
+ IProjectParserUtility projectParserUtility,
+ IAWSUtilities awsUtilities,
+ ICommandLineWrapper commandLineWrapper,
+ IAWSClientFactory awsClientFactory) : CancellableAsyncCommand
{
- public class DeployCommand
+ ///
+ /// Deploys given application
+ ///
+ /// Command context
+ /// Command settings
+ /// Cancellation token source
+ /// The command exit code
+ public override async Task ExecuteAsync(CommandContext context, DeployCommandSettings settings, CancellationTokenSource cancellationTokenSource)
{
- private readonly IServiceProvider _serviceProvider;
- private readonly IToolInteractiveService _toolInteractiveService;
- private readonly IOrchestratorInteractiveService _orchestratorInteractiveService;
- private readonly ICdkProjectHandler _cdkProjectHandler;
- private readonly ICDKManager _cdkManager;
- private readonly IDeploymentBundleHandler _deploymentBundleHandler;
- private readonly IDockerEngine _dockerEngine;
- private readonly IAWSResourceQueryer _awsResourceQueryer;
- private readonly ICloudFormationTemplateReader _cloudFormationTemplateReader;
- private readonly IDeployedApplicationQueryer _deployedApplicationQueryer;
- private readonly ITypeHintCommandFactory _typeHintCommandFactory;
- private readonly IDisplayedResourcesHandler _displayedResourcesHandler;
- private readonly ICloudApplicationNameGenerator _cloudApplicationNameGenerator;
- private readonly ILocalUserSettingsEngine _localUserSettingsEngine;
- private readonly IConsoleUtilities _consoleUtilities;
- private readonly ISystemCapabilityEvaluator _systemCapabilityEvaluator;
- private readonly OrchestratorSession _session;
- private readonly IDirectoryManager _directoryManager;
- private readonly IFileManager _fileManager;
- private readonly ICDKVersionDetector _cdkVersionDetector;
- private readonly IAWSServiceHandler _awsServiceHandler;
- private readonly IOptionSettingHandler _optionSettingHandler;
- private readonly IValidatorFactory _validatorFactory;
- private readonly IRecipeHandler _recipeHandler;
- private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata;
- private readonly IDeploymentSettingsHandler _deploymentSettingsHandler;
-
- public DeployCommand(
- IServiceProvider serviceProvider,
- IToolInteractiveService toolInteractiveService,
- IOrchestratorInteractiveService orchestratorInteractiveService,
- ICdkProjectHandler cdkProjectHandler,
- ICDKManager cdkManager,
- ICDKVersionDetector cdkVersionDetector,
- IDeploymentBundleHandler deploymentBundleHandler,
- IDockerEngine dockerEngine,
- IAWSResourceQueryer awsResourceQueryer,
- ICloudFormationTemplateReader cloudFormationTemplateReader,
- IDeployedApplicationQueryer deployedApplicationQueryer,
- ITypeHintCommandFactory typeHintCommandFactory,
- IDisplayedResourcesHandler displayedResourcesHandler,
- ICloudApplicationNameGenerator cloudApplicationNameGenerator,
- ILocalUserSettingsEngine localUserSettingsEngine,
- IConsoleUtilities consoleUtilities,
- ISystemCapabilityEvaluator systemCapabilityEvaluator,
- OrchestratorSession session,
- IDirectoryManager directoryManager,
- IFileManager fileManager,
- IAWSServiceHandler awsServiceHandler,
- IOptionSettingHandler optionSettingHandler,
- IValidatorFactory validatorFactory,
- IRecipeHandler recipeHandler,
- IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata,
- IDeploymentSettingsHandler deploymentSettingsHandler)
- {
- _serviceProvider = serviceProvider;
- _toolInteractiveService = toolInteractiveService;
- _orchestratorInteractiveService = orchestratorInteractiveService;
- _cdkProjectHandler = cdkProjectHandler;
- _deploymentBundleHandler = deploymentBundleHandler;
- _dockerEngine = dockerEngine;
- _awsResourceQueryer = awsResourceQueryer;
- _cloudFormationTemplateReader = cloudFormationTemplateReader;
- _deployedApplicationQueryer = deployedApplicationQueryer;
- _typeHintCommandFactory = typeHintCommandFactory;
- _displayedResourcesHandler = displayedResourcesHandler;
- _cloudApplicationNameGenerator = cloudApplicationNameGenerator;
- _localUserSettingsEngine = localUserSettingsEngine;
- _consoleUtilities = consoleUtilities;
- _session = session;
- _directoryManager = directoryManager;
- _fileManager = fileManager;
- _cdkVersionDetector = cdkVersionDetector;
- _cdkManager = cdkManager;
- _systemCapabilityEvaluator = systemCapabilityEvaluator;
- _awsServiceHandler = awsServiceHandler;
- _optionSettingHandler = optionSettingHandler;
- _validatorFactory = validatorFactory;
- _recipeHandler = recipeHandler;
- _deployToolWorkspaceMetadata = deployToolWorkspaceMetadata;
- _deploymentSettingsHandler = deploymentSettingsHandler;
- }
-
- public async Task ExecuteAsync(string applicationName, string deploymentProjectPath, SaveSettingsConfiguration saveSettingsConfig, DeploymentSettings? deploymentSettings = null)
- {
- var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(applicationName, deploymentSettings, deploymentProjectPath);
-
- // Verify Docker installation and minimum NodeJS version.
- await EvaluateSystemCapabilities(selectedRecommendation);
-
- // Configure option settings.
- await ConfigureDeployment(cloudApplication, orchestrator, selectedRecommendation, deploymentSettings);
-
- if (!ConfirmDeployment(selectedRecommendation))
- {
- return;
- }
+ toolInteractiveService.Diagnostics = settings.Diagnostics;
+ toolInteractiveService.DisableInteractive = settings.Silent;
- // Because we're starting a deployment, clear the cached system capabilities checks
- // in case the deployment fails and the user reruns it after modifying Docker or Node
- _systemCapabilityEvaluator.ClearCachedCapabilityChecks();
+ var projectDefinition = await projectParserUtility.Parse(settings.ProjectPath ?? "");
+ var targetApplicationDirectoryPath = new DirectoryInfo(projectDefinition.ProjectPath).Parent!.FullName;
- await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication);
+ DeploymentSettings? deploymentSettings = null;
+ if (!string.IsNullOrEmpty(settings.Apply))
+ {
+ var applyPath = Path.GetFullPath(settings.Apply, targetApplicationDirectoryPath);
+ deploymentSettings = await deploymentSettingsHandler.ReadSettings(applyPath);
+ }
- if (saveSettingsConfig.SettingsType != SaveSettingsType.None)
- {
- await _deploymentSettingsHandler.SaveSettings(saveSettingsConfig, selectedRecommendation, cloudApplication, _session);
- _toolInteractiveService.WriteLine($"{Environment.NewLine}Successfully saved the deployment settings at {saveSettingsConfig.FilePath}");
- }
+ var (awsCredentials, regionFromProfile) = await awsUtilities.ResolveAWSCredentials(settings.Profile ?? deploymentSettings?.AWSProfile);
+ var awsRegion = awsUtilities.ResolveAWSRegion(settings.Region ?? deploymentSettings?.AWSRegion ?? regionFromProfile);
- await orchestrator.DeployRecommendation(cloudApplication, selectedRecommendation);
+ commandLineWrapper.RegisterAWSContext(awsCredentials, awsRegion);
+ awsClientFactory.RegisterAWSContext(awsCredentials, awsRegion);
- var displayedResources = await _displayedResourcesHandler.GetDeploymentOutputs(cloudApplication, selectedRecommendation);
- DisplayOutputResources(displayedResources);
- }
+ var callerIdentity = await awsResourceQueryer.GetCallerIdentity(awsRegion);
- private void DisplayOutputResources(List displayedResourceItems)
+ var session = new OrchestratorSession(
+ projectDefinition,
+ awsCredentials,
+ awsRegion,
+ callerIdentity.Account)
{
- _orchestratorInteractiveService.LogSectionStart("AWS Resource Details", null);
- foreach (var resource in displayedResourceItems)
- {
- _toolInteractiveService.WriteLine($"{resource.Description}:");
- _toolInteractiveService.WriteLine($"\t{nameof(resource.Id)}: {resource.Id}");
- _toolInteractiveService.WriteLine($"\t{nameof(resource.Type)}: {resource.Type}");
- foreach (var resourceKey in resource.Data.Keys)
- {
- _toolInteractiveService.WriteLine($"\t{resourceKey}: {resource.Data[resourceKey]}");
- }
- }
+ AWSProfileName = settings.Profile ?? deploymentSettings?.AWSProfile ?? null
+ };
+
+ var deploymentProjectPath = settings.DeploymentProject ?? string.Empty;
+ if (!string.IsNullOrEmpty(deploymentProjectPath))
+ {
+ deploymentProjectPath = Path.GetFullPath(deploymentProjectPath, targetApplicationDirectoryPath);
}
- ///
- /// Initiates a deployment or a re-deployment.
- /// If a new Cloudformation stack name is selected, then a fresh deployment is initiated with the user-selected deployment recipe.
- /// If an existing deployment target is selected, then a re-deployment is initiated with the same deployment recipe.
- ///
- /// The cloud application name provided via the --application-name CLI argument
- /// The deserialized object from the user provided config file.
- /// The absolute or relative path of the CDK project that will be used for deployment
- /// A tuple consisting of the Orchestrator object, Selected Recommendation, Cloud Application metadata.
- public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string cloudApplicationName, DeploymentSettings? deploymentSettings, string deploymentProjectPath)
- {
- var orchestrator = new Orchestrator(
- _session,
- _orchestratorInteractiveService,
- _cdkProjectHandler,
- _cdkManager,
- _cdkVersionDetector,
- _awsResourceQueryer,
- _deploymentBundleHandler,
- _localUserSettingsEngine,
- _dockerEngine,
- _recipeHandler,
- _fileManager,
- _directoryManager,
- _awsServiceHandler,
- _optionSettingHandler,
- _deployToolWorkspaceMetadata);
-
- // Determine what recommendations are possible for the project.
- var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath);
-
- // Get all existing applications that were previously deployed using our deploy tool.
- var allDeployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList());
-
- // Filter compatible applications that can be re-deployed using the current set of recommendations.
- var compatibleApplications = await _deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, _session);
+ var saveSettingsConfig = Helpers.GetSaveSettingsConfiguration(settings.SaveSettings, settings.SaveAllSettings, targetApplicationDirectoryPath, fileManager);
- if (string.IsNullOrEmpty(cloudApplicationName))
- // Try finding the CloudApplication name via the user provided config settings.
- cloudApplicationName = deploymentSettings?.ApplicationName ?? string.Empty;
+ var (orchestrator, selectedRecommendation, cloudApplication) =
+ await InitializeDeployment(
+ settings.ApplicationName ?? string.Empty,
+ deploymentSettings,
+ deploymentProjectPath,
+ projectDefinition,
+ session);
- // Prompt the user with a choice to re-deploy to existing targets or deploy to a new cloud application.
- // This prompt is NOT needed if the user is just pushing the docker image to ECR.
- if (string.IsNullOrEmpty(cloudApplicationName) && !string.Equals(deploymentSettings?.RecipeId, Constants.RecipeIdentifier.PUSH_TO_ECR_RECIPE_ID))
- cloudApplicationName = AskForCloudApplicationNameFromDeployedApplications(compatibleApplications);
+ // Verify Docker installation and minimum NodeJS version.
+ await EvaluateSystemCapabilities(selectedRecommendation);
- // Find existing application with the same CloudApplication name.
- CloudApplication? deployedApplication = null;
- if (!string.IsNullOrEmpty(deploymentSettings?.RecipeId))
- {
- // if the recommendation is specified via a config file then find the deployed application by matching the deployment type along with the cloudApplicationName
- var recommendation = recommendations.FirstOrDefault(x => string.Equals(x.Recipe.Id, deploymentSettings.RecipeId));
- if (recommendation == null)
- {
- var errorMsg = "The recipe ID specified in the deployment settings file does not match any compatible deployment recipes.";
- throw new InvalidDeploymentSettingsException(DeployToolErrorCode.DeploymentConfigurationNeedsAdjusting, errorMsg);
- }
- deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName) && x.DeploymentType == recommendation.Recipe.DeploymentType);
- }
- else
- {
- deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName));
- }
+ // Configure option settings.
+ await ConfigureDeployment(session, selectedRecommendation, deploymentSettings);
- Recommendation? selectedRecommendation = null;
- if (deployedApplication != null)
- {
- // Verify that the target application can be deployed using the current set of recommendations
- if (!compatibleApplications.Any(app => app.Name.Equals(deployedApplication.Name, StringComparison.Ordinal)))
- {
- var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation to perform a redeployment was not found";
- throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage);
- }
+ if (!ConfirmDeployment(selectedRecommendation))
+ {
+ return CommandReturnCodes.SUCCESS;
+ }
- // preset settings for deployment based on last deployment.
- selectedRecommendation = await GetSelectedRecommendationFromPreviousDeployment(orchestrator, recommendations, deployedApplication, deploymentSettings, deploymentProjectPath);
- }
- else
- {
- if (!string.IsNullOrEmpty(deploymentProjectPath))
- {
- selectedRecommendation = recommendations.First();
- }
- else
- {
- // Filter the recommendation list for a NEW deployment with recipes which have the DisableNewDeployments property set to false.
- selectedRecommendation = GetSelectedRecommendation(deploymentSettings, recommendations.Where(x => !x.Recipe.DisableNewDeployments).ToList());
- }
+ // Because we're starting a deployment, clear the cached system capabilities checks
+ // in case the deployment fails and the user reruns it after modifying Docker or Node
+ systemCapabilityEvaluator.ClearCachedCapabilityChecks();
- // Ask the user for a new Cloud Application name based on the deployment type of the recipe.
- if (string.IsNullOrEmpty(cloudApplicationName))
- {
- // Don't prompt for a new name if a user just wants to push images to ECR
- // The ECR repository name is already configurable as part of the recipe option settings.
- if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.ElasticContainerRegistryImage)
- {
- cloudApplicationName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, compatibleApplications, selectedRecommendation.Recipe.DeploymentType);
- }
- else
- {
- cloudApplicationName = AskForNewCloudApplicationName(selectedRecommendation.Recipe.DeploymentType, compatibleApplications);
- }
- }
- // cloudApplication name was already provided via CLI args or the deployment config file
- else
- {
- var validationResult = _cloudApplicationNameGenerator.IsValidName(cloudApplicationName, allDeployedApplications, selectedRecommendation.Recipe.DeploymentType);
- if (!validationResult.IsValid)
- throw new InvalidCloudApplicationNameException(DeployToolErrorCode.InvalidCloudApplicationName, validationResult.ErrorMessage);
- }
- }
+ await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication);
- await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, cloudApplicationName);
+ if (saveSettingsConfig.SettingsType != SaveSettingsType.None)
+ {
+ await deploymentSettingsHandler.SaveSettings(saveSettingsConfig, selectedRecommendation, cloudApplication, session);
+ toolInteractiveService.WriteLine($"{Environment.NewLine}Successfully saved the deployment settings at {saveSettingsConfig.FilePath}");
+ }
- var cloudApplication = new CloudApplication(cloudApplicationName, deployedApplication?.UniqueIdentifier ?? string.Empty, orchestrator.GetCloudApplicationResourceType(selectedRecommendation.Recipe.DeploymentType), selectedRecommendation.Recipe.Id);
+ await orchestrator.DeployRecommendation(cloudApplication, selectedRecommendation);
- return (orchestrator, selectedRecommendation, cloudApplication);
- }
+ var displayedResources = await displayedResourcesHandler.GetDeploymentOutputs(cloudApplication, selectedRecommendation);
+ DisplayOutputResources(displayedResources);
+
+ return CommandReturnCodes.SUCCESS;
+ }
- ///
- /// Checks if the system meets all the necessary requirements for deployment.
- ///
- /// The selected recommendation settings used for deployment.
- public async Task EvaluateSystemCapabilities(Recommendation selectedRecommendation)
+ private void DisplayOutputResources(List displayedResourceItems)
+ {
+ orchestratorInteractiveService.LogSectionStart("AWS Resource Details", null);
+ foreach (var resource in displayedResourceItems)
{
- var missingSystemCapabilities = await _systemCapabilityEvaluator.EvaluateSystemCapabilities(selectedRecommendation);
- var missingCapabilitiesMessage = "";
- foreach (var capability in missingSystemCapabilities)
+ toolInteractiveService.WriteLine($"{resource.Description}:");
+ toolInteractiveService.WriteLine($"\t{nameof(resource.Id)}: {resource.Id}");
+ toolInteractiveService.WriteLine($"\t{nameof(resource.Type)}: {resource.Type}");
+ foreach (var resourceKey in resource.Data.Keys)
{
- missingCapabilitiesMessage = $"{missingCapabilitiesMessage}{Environment.NewLine}{capability.GetMessage()}{Environment.NewLine}";
+ toolInteractiveService.WriteLine($"\t{resourceKey}: {resource.Data[resourceKey]}");
}
-
- if (missingSystemCapabilities.Any())
- throw new MissingSystemCapabilityException(DeployToolErrorCode.MissingSystemCapabilities, missingCapabilitiesMessage);
}
+ }
- ///
- /// Configure option setings using the CLI or a user provided configuration file.
- ///
- ///
- ///
- ///
- ///
- public async Task ConfigureDeployment(CloudApplication cloudApplication, Orchestrator orchestrator, Recommendation selectedRecommendation, DeploymentSettings? deploymentSettings)
+ ///
+ /// Initiates a deployment or a re-deployment.
+ /// If a new Cloudformation stack name is selected, then a fresh deployment is initiated with the user-selected deployment recipe.
+ /// If an existing deployment target is selected, then a re-deployment is initiated with the same deployment recipe.
+ ///
+ /// The cloud application name provided via the --application-name CLI argument
+ /// The deserialized object from the user provided config file.
+ /// The absolute or relative path of the CDK project that will be used for deployment
+ ///
+ ///
+ /// A tuple consisting of the Orchestrator object, Selected Recommendation, Cloud Application metadata.
+ public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string cloudApplicationName, DeploymentSettings? deploymentSettings, string deploymentProjectPath, ProjectDefinition projectDefinition, OrchestratorSession session)
+ {
+ var dockerEngine = new DockerEngine(projectDefinition, fileManager, directoryManager);
+ var orchestrator = new Orchestrator(
+ session,
+ orchestratorInteractiveService,
+ cdkProjectHandler,
+ cdkManager,
+ cdkVersionDetector,
+ awsResourceQueryer,
+ deploymentBundleHandler,
+ localUserSettingsEngine,
+ dockerEngine,
+ recipeHandler,
+ fileManager,
+ directoryManager,
+ awsServiceHandler,
+ optionSettingHandler,
+ deployToolWorkspaceMetadata);
+
+ // Determine what recommendations are possible for the project.
+ var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath);
+
+ // Get all existing applications that were previously deployed using our deploy tool.
+ var allDeployedApplications = await deployedApplicationQueryer.GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList());
+
+ // Filter compatible applications that can be re-deployed using the current set of recommendations.
+ var compatibleApplications = await deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, session);
+
+ if (string.IsNullOrEmpty(cloudApplicationName))
+ // Try finding the CloudApplication name via the user provided config settings.
+ cloudApplicationName = deploymentSettings?.ApplicationName ?? string.Empty;
+
+ // Prompt the user with a choice to re-deploy to existing targets or deploy to a new cloud application.
+ // This prompt is NOT needed if the user is just pushing the docker image to ECR.
+ if (string.IsNullOrEmpty(cloudApplicationName) && !string.Equals(deploymentSettings?.RecipeId, Constants.RecipeIdentifier.PUSH_TO_ECR_RECIPE_ID))
+ cloudApplicationName = AskForCloudApplicationNameFromDeployedApplications(compatibleApplications);
+
+ // Find existing application with the same CloudApplication name.
+ CloudApplication? deployedApplication = null;
+ if (!string.IsNullOrEmpty(deploymentSettings?.RecipeId))
{
- var configurableOptionSettings = selectedRecommendation.GetConfigurableOptionSettingItems();
-
- if (deploymentSettings != null)
+ // if the recommendation is specified via a config file then find the deployed application by matching the deployment type along with the cloudApplicationName
+ var recommendation = recommendations.FirstOrDefault(x => string.Equals(x.Recipe.Id, deploymentSettings.RecipeId));
+ if (recommendation == null)
{
- await _deploymentSettingsHandler.ApplySettings(deploymentSettings, selectedRecommendation, _session);
+ var errorMsg = "The recipe ID specified in the deployment settings file does not match any compatible deployment recipes.";
+ throw new InvalidDeploymentSettingsException(DeployToolErrorCode.DeploymentConfigurationNeedsAdjusting, errorMsg);
}
+ deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName) && x.DeploymentType == recommendation.Recipe.DeploymentType);
+ }
+ else
+ {
+ deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName));
+ }
- if (!_toolInteractiveService.DisableInteractive)
+ Recommendation? selectedRecommendation = null;
+ if (deployedApplication != null)
+ {
+ // Verify that the target application can be deployed using the current set of recommendations
+ if (!compatibleApplications.Any(app => app.Name.Equals(deployedApplication.Name, StringComparison.Ordinal)))
{
- await ConfigureDeploymentFromCli(selectedRecommendation, configurableOptionSettings, false);
+ var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation to perform a redeployment was not found";
+ throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage);
}
- }
- private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator, string deploymentProjectPath)
+ // preset settings for deployment based on last deployment.
+ selectedRecommendation = await GetSelectedRecommendationFromPreviousDeployment(orchestrator, recommendations, deployedApplication, deploymentSettings, deploymentProjectPath);
+ }
+ else
{
- List recommendations;
if (!string.IsNullOrEmpty(deploymentProjectPath))
{
- recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(deploymentProjectPath);
- if (!recommendations.Any())
- {
- var errorMessage = $"Could not find any deployment recipe located inside '{deploymentProjectPath}' that can be used for deployment of the target application";
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.NoDeploymentRecipesFound, errorMessage);
- }
+ selectedRecommendation = recommendations.First();
}
else
{
- recommendations = await orchestrator.GenerateDeploymentRecommendations();
- if (!recommendations.Any())
- {
- var errorMessage = "There are no compatible deployment recommendations for this application.";
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.NoCompatibleDeploymentRecipesFound, errorMessage);
- }
+ // Filter the recommendation list for a NEW deployment with recipes which have the DisableNewDeployments property set to false.
+ selectedRecommendation = GetSelectedRecommendation(deploymentSettings, recommendations.Where(x => !x.Recipe.DisableNewDeployments).ToList());
}
- return recommendations;
- }
- private async Task GetSelectedRecommendationFromPreviousDeployment(Orchestrator orchestrator, List recommendations, CloudApplication deployedApplication, DeploymentSettings? deploymentSettings, string deploymentProjectPath)
- {
- var deploymentSettingRecipeId = deploymentSettings?.RecipeId;
- var selectedRecommendation = await GetRecommendationForRedeployment(recommendations, deployedApplication, deploymentProjectPath);
- if (selectedRecommendation == null)
- {
- var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation used to perform a re-deployment was not found.";
- throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage);
- }
- if (!string.IsNullOrEmpty(deploymentSettingRecipeId) && !string.Equals(deploymentSettingRecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase))
+ // Ask the user for a new Cloud Application name based on the deployment type of the recipe.
+ if (string.IsNullOrEmpty(cloudApplicationName))
{
- var errorMessage = $"The existing {deployedApplication.ResourceType} {deployedApplication.Name} was created from a different deployment recommendation. " +
- "Deploying to an existing target must be performed with the original deployment recommendation to avoid unintended destructive changes to the resources.";
- if (_toolInteractiveService.Diagnostics)
+ // Don't prompt for a new name if a user just wants to push images to ECR
+ // The ECR repository name is already configurable as part of the recipe option settings.
+ if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.ElasticContainerRegistryImage)
{
- errorMessage += Environment.NewLine + $"The original deployment recipe ID was {deployedApplication.RecipeId} and the current deployment recipe ID is {deploymentSettingRecipeId}";
+ cloudApplicationName = cloudApplicationNameGenerator.GenerateValidName(session.ProjectDefinition, compatibleApplications, selectedRecommendation.Recipe.DeploymentType);
+ }
+ else
+ {
+ cloudApplicationName = AskForNewCloudApplicationName(session.ProjectDefinition, selectedRecommendation.Recipe.DeploymentType, compatibleApplications);
}
- throw new InvalidDeploymentSettingsException(DeployToolErrorCode.StackCreatedFromDifferentDeploymentRecommendation, errorMessage.Trim());
- }
-
- IDictionary previousSettings;
- if (deployedApplication.ResourceType == CloudApplicationResourceType.CloudFormationStack)
- {
- var metadata = await _cloudFormationTemplateReader.LoadCloudApplicationMetadata(deployedApplication.Name);
- previousSettings = metadata.Settings.Union(metadata.DeploymentBundleSettings).ToDictionary(x => x.Key, x => x.Value);
}
+ // cloudApplication name was already provided via CLI args or the deployment config file
else
{
- previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication, selectedRecommendation);
+ var validationResult = cloudApplicationNameGenerator.IsValidName(cloudApplicationName, allDeployedApplications, selectedRecommendation.Recipe.DeploymentType);
+ if (!validationResult.IsValid)
+ throw new InvalidCloudApplicationNameException(DeployToolErrorCode.InvalidCloudApplicationName, validationResult.ErrorMessage);
}
+ }
- await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, deployedApplication.Name);
+ await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, cloudApplicationName);
- selectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(selectedRecommendation, previousSettings);
+ var cloudApplication = new CloudApplication(cloudApplicationName, deployedApplication?.UniqueIdentifier ?? string.Empty, orchestrator.GetCloudApplicationResourceType(selectedRecommendation.Recipe.DeploymentType), selectedRecommendation.Recipe.Id);
- var header = $"Loading {deployedApplication.DisplayName} settings:";
+ return (orchestrator, selectedRecommendation, cloudApplication);
+ }
- _toolInteractiveService.WriteLine(header);
- _toolInteractiveService.WriteLine(new string('-', header.Length));
- var optionSettings =
- selectedRecommendation
- .Recipe
- .OptionSettings
- .Where(x => _optionSettingHandler.IsSummaryDisplayable(selectedRecommendation, x))
- .ToArray();
+ ///
+ /// Checks if the system meets all the necessary requirements for deployment.
+ ///
+ /// The selected recommendation settings used for deployment.
+ public async Task EvaluateSystemCapabilities(Recommendation selectedRecommendation)
+ {
+ var missingSystemCapabilities = await systemCapabilityEvaluator.EvaluateSystemCapabilities(selectedRecommendation);
+ var missingCapabilitiesMessage = "";
+ foreach (var capability in missingSystemCapabilities)
+ {
+ missingCapabilitiesMessage = $"{missingCapabilitiesMessage}{Environment.NewLine}{capability.GetMessage()}{Environment.NewLine}";
+ }
- foreach (var setting in optionSettings)
- {
- DisplayOptionSetting(selectedRecommendation, setting, -1, optionSettings.Length, DisplayOptionSettingsMode.Readonly);
- }
+ if (missingSystemCapabilities.Any())
+ throw new MissingSystemCapabilityException(DeployToolErrorCode.MissingSystemCapabilities, missingCapabilitiesMessage);
+ }
- return selectedRecommendation;
+ ///
+ /// Configure option setings using the CLI or a user provided configuration file.
+ ///
+ ///
+ ///
+ ///
+ public async Task ConfigureDeployment(OrchestratorSession session, Recommendation selectedRecommendation, DeploymentSettings? deploymentSettings)
+ {
+ var configurableOptionSettings = selectedRecommendation.GetConfigurableOptionSettingItems();
+
+ if (deploymentSettings != null)
+ {
+ await deploymentSettingsHandler.ApplySettings(deploymentSettings, selectedRecommendation, session);
}
- private async Task GetRecommendationForRedeployment(List recommendations, CloudApplication deployedApplication, string deploymentProjectPath)
+ if (!toolInteractiveService.DisableInteractive)
{
- var targetRecipeId = !string.IsNullOrEmpty(deploymentProjectPath) ?
- await GetDeploymentProjectRecipeId(deploymentProjectPath) : deployedApplication.RecipeId;
+ await ConfigureDeploymentFromCli(session, selectedRecommendation, configurableOptionSettings, false);
+ }
+ }
- foreach (var recommendation in recommendations)
+ private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator, string deploymentProjectPath)
+ {
+ List recommendations;
+ if (!string.IsNullOrEmpty(deploymentProjectPath))
+ {
+ recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(deploymentProjectPath);
+ if (!recommendations.Any())
{
- if (string.Equals(recommendation.Recipe.Id, targetRecipeId) && _deployedApplicationQueryer.IsCompatible(deployedApplication, recommendation))
- {
- return recommendation;
- }
+ var errorMessage = $"Could not find any deployment recipe located inside '{deploymentProjectPath}' that can be used for deployment of the target application";
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.NoDeploymentRecipesFound, errorMessage);
}
- return null;
}
-
- private async Task GetDeploymentProjectRecipeId(string deploymentProjectPath)
+ else
{
- if (!_directoryManager.Exists(deploymentProjectPath))
+ recommendations = await orchestrator.GenerateDeploymentRecommendations();
+ if (!recommendations.Any())
{
- throw new InvalidOperationException($"Invalid deployment project path. {deploymentProjectPath} does not exist on the file system.");
+ var errorMessage = "There are no compatible deployment recommendations for this application.";
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.NoCompatibleDeploymentRecipesFound, errorMessage);
}
+ }
+ return recommendations;
+ }
- try
- {
- var recipeFiles = _directoryManager.GetFiles(deploymentProjectPath, "*.recipe");
- if (recipeFiles.Length == 0)
- {
- throw new InvalidOperationException($"Failed to find a recipe file at {deploymentProjectPath}");
- }
- if (recipeFiles.Length > 1)
- {
- throw new InvalidOperationException($"Found more than one recipe files at {deploymentProjectPath}. Only one recipe file per deployment project is supported.");
- }
-
- var recipeFilePath = recipeFiles.First();
- var recipeBody = await _fileManager.ReadAllTextAsync(recipeFilePath);
- var recipe = JsonConvert.DeserializeObject(recipeBody);
- if (recipe == null)
- throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentProjectRecipe, $"Failed to deserialize Deployment Project Recipe '{recipeFilePath}'");
- return recipe.Id;
- }
- catch (Exception ex)
+ private async Task GetSelectedRecommendationFromPreviousDeployment(Orchestrator orchestrator, List recommendations, CloudApplication deployedApplication, DeploymentSettings? deploymentSettings, string deploymentProjectPath)
+ {
+ var deploymentSettingRecipeId = deploymentSettings?.RecipeId;
+ var selectedRecommendation = await GetRecommendationForRedeployment(recommendations, deployedApplication, deploymentProjectPath);
+ if (selectedRecommendation == null)
+ {
+ var errorMessage = $"{deployedApplication.Name} already exists as a {deployedApplication.ResourceType} but a compatible recommendation used to perform a re-deployment was not found.";
+ throw new FailedToFindCompatibleRecipeException(DeployToolErrorCode.CompatibleRecommendationForRedeploymentNotFound, errorMessage);
+ }
+ if (!string.IsNullOrEmpty(deploymentSettingRecipeId) && !string.Equals(deploymentSettingRecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase))
+ {
+ var errorMessage = $"The existing {deployedApplication.ResourceType} {deployedApplication.Name} was created from a different deployment recommendation. " +
+ "Deploying to an existing target must be performed with the original deployment recommendation to avoid unintended destructive changes to the resources.";
+ if (toolInteractiveService.Diagnostics)
{
- throw new FailedToFindDeploymentProjectRecipeIdException(DeployToolErrorCode.FailedToFindDeploymentProjectRecipeId, $"Failed to find a recipe ID for the deployment project located at {deploymentProjectPath}", ex);
+ errorMessage += Environment.NewLine + $"The original deployment recipe ID was {deployedApplication.RecipeId} and the current deployment recipe ID is {deploymentSettingRecipeId}";
}
+ throw new InvalidDeploymentSettingsException(DeployToolErrorCode.StackCreatedFromDifferentDeploymentRecommendation, errorMessage.Trim());
}
- // This method prompts the user to select a CloudApplication name for existing deployments or create a new one.
- // If a user chooses to create a new CloudApplication, then this method returns string.Empty
- private string AskForCloudApplicationNameFromDeployedApplications(List deployedApplications)
+ IDictionary previousSettings;
+ if (deployedApplication.ResourceType == CloudApplicationResourceType.CloudFormationStack)
+ {
+ var metadata = await cloudFormationTemplateReader.LoadCloudApplicationMetadata(deployedApplication.Name);
+ previousSettings = metadata.Settings.Union(metadata.DeploymentBundleSettings).ToDictionary(x => x.Key, x => x.Value);
+ }
+ else
{
- if (!deployedApplications.Any())
- return string.Empty;
+ previousSettings = await deployedApplicationQueryer.GetPreviousSettings(deployedApplication, selectedRecommendation);
+ }
- var title = "Select an existing AWS deployment target to deploy your application to.";
+ await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, deployedApplication.Name);
- var userInputConfiguration = new UserInputConfiguration(
- idSelector: app => app.DisplayName,
- displaySelector: app => app.DisplayName,
- defaultSelector: app => app.DisplayName.Equals(deployedApplications.First().DisplayName))
- {
- AskNewName = false,
- CanBeEmpty = false
- };
+ selectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(selectedRecommendation, previousSettings);
- var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(
- options: deployedApplications,
- title: title,
- userInputConfiguration: userInputConfiguration,
- defaultChoosePrompt: Constants.CLI.PROMPT_CHOOSE_DEPLOYMENT_TARGET,
- defaultCreateNewLabel: Constants.CLI.CREATE_NEW_APPLICATION_LABEL);
+ var header = $"Loading {deployedApplication.DisplayName} settings:";
- var cloudApplicationName = userResponse.SelectedOption != null ? userResponse.SelectedOption.Name : string.Empty;
- return cloudApplicationName;
+ toolInteractiveService.WriteLine(header);
+ toolInteractiveService.WriteLine(new string('-', header.Length));
+ var optionSettings =
+ selectedRecommendation
+ .Recipe
+ .OptionSettings
+ .Where(x => optionSettingHandler.IsSummaryDisplayable(selectedRecommendation, x))
+ .ToArray();
+
+ foreach (var setting in optionSettings)
+ {
+ DisplayOptionSetting(selectedRecommendation, setting, -1, optionSettings.Length, DisplayOptionSettingsMode.Readonly);
}
- // This method prompts the user for a new CloudApplication name and also generate a valid default name by respecting existing applications.
- private string AskForNewCloudApplicationName(DeploymentTypes deploymentType, List deployedApplications)
+ return selectedRecommendation;
+ }
+
+ private async Task GetRecommendationForRedeployment(List recommendations, CloudApplication deployedApplication, string deploymentProjectPath)
+ {
+ var targetRecipeId = !string.IsNullOrEmpty(deploymentProjectPath) ?
+ await GetDeploymentProjectRecipeId(deploymentProjectPath) : deployedApplication.RecipeId;
+
+ foreach (var recommendation in recommendations)
{
- if (_toolInteractiveService.DisableInteractive)
+ if (string.Equals(recommendation.Recipe.Id, targetRecipeId) && deployedApplicationQueryer.IsCompatible(deployedApplication, recommendation))
{
- var message = "The \"--silent\" CLI argument can only be used if a cloud application name is provided either via the CLI argument \"--application-name\" or through a deployment-settings file. " +
- "Please provide an application name and try again";
- throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsApplicationNameArgument, message);
+ return recommendation;
}
+ }
+ return null;
+ }
- var defaultName = "";
+ private async Task GetDeploymentProjectRecipeId(string deploymentProjectPath)
+ {
+ if (!directoryManager.Exists(deploymentProjectPath))
+ {
+ throw new InvalidOperationException($"Invalid deployment project path. {deploymentProjectPath} does not exist on the file system.");
+ }
- try
+ try
+ {
+ var recipeFiles = directoryManager.GetFiles(deploymentProjectPath, "*.recipe");
+ if (recipeFiles.Length == 0)
{
- defaultName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, deployedApplications, deploymentType);
+ throw new InvalidOperationException($"Failed to find a recipe file at {deploymentProjectPath}");
}
- catch (Exception exception)
+ if (recipeFiles.Length > 1)
{
- _toolInteractiveService.WriteDebugLine(exception.PrettyPrint());
+ throw new InvalidOperationException($"Found more than one recipe files at {deploymentProjectPath}. Only one recipe file per deployment project is supported.");
}
- var cloudApplicationName = string.Empty;
+ var recipeFilePath = recipeFiles.First();
+ var recipeBody = await fileManager.ReadAllTextAsync(recipeFilePath);
+ var recipe = JsonConvert.DeserializeObject(recipeBody);
+ if (recipe == null)
+ throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentProjectRecipe, $"Failed to deserialize Deployment Project Recipe '{recipeFilePath}'");
+ return recipe.Id;
+ }
+ catch (Exception ex)
+ {
+ throw new FailedToFindDeploymentProjectRecipeIdException(DeployToolErrorCode.FailedToFindDeploymentProjectRecipeId, $"Failed to find a recipe ID for the deployment project located at {deploymentProjectPath}", ex);
+ }
+ }
- while (true)
- {
- _toolInteractiveService.WriteLine();
+ // This method prompts the user to select a CloudApplication name for existing deployments or create a new one.
+ // If a user chooses to create a new CloudApplication, then this method returns string.Empty
+ private string AskForCloudApplicationNameFromDeployedApplications(List deployedApplications)
+ {
+ if (!deployedApplications.Any())
+ return string.Empty;
- var title = "Name the Cloud Application to deploy your project to" + Environment.NewLine +
- "--------------------------------------------------------------------------------";
+ var title = "Select an existing AWS deployment target to deploy your application to.";
- string inputPrompt;
+ var userInputConfiguration = new UserInputConfiguration(
+ idSelector: app => app.DisplayName,
+ displaySelector: app => app.DisplayName,
+ defaultSelector: app => app.DisplayName.Equals(deployedApplications.First().DisplayName))
+ {
+ AskNewName = false,
+ CanBeEmpty = false
+ };
+
+ var userResponse = consoleUtilities.AskUserToChooseOrCreateNew(
+ options: deployedApplications,
+ title: title,
+ userInputConfiguration: userInputConfiguration,
+ defaultChoosePrompt: Constants.CLI.PROMPT_CHOOSE_DEPLOYMENT_TARGET,
+ defaultCreateNewLabel: Constants.CLI.CREATE_NEW_APPLICATION_LABEL);
+
+ var cloudApplicationName = userResponse.SelectedOption != null ? userResponse.SelectedOption.Name : string.Empty;
+ return cloudApplicationName;
+ }
- switch (deploymentType)
- {
- case DeploymentTypes.CdkProject:
- inputPrompt = Constants.CLI.PROMPT_NEW_STACK_NAME;
- break;
- case DeploymentTypes.ElasticContainerRegistryImage:
- inputPrompt = Constants.CLI.PROMPT_ECR_REPOSITORY_NAME;
- break;
- default:
- throw new InvalidOperationException($"The {nameof(DeploymentTypes)} {deploymentType} does not have an input prompt");
- }
+ // This method prompts the user for a new CloudApplication name and also generate a valid default name by respecting existing applications.
+ private string AskForNewCloudApplicationName(ProjectDefinition projectDefinition, DeploymentTypes deploymentType, List deployedApplications)
+ {
+ if (toolInteractiveService.DisableInteractive)
+ {
+ var message = "The \"--silent\" CLI argument can only be used if a cloud application name is provided either via the CLI argument \"--application-name\" or through a deployment-settings file. " +
+ "Please provide an application name and try again";
+ throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsApplicationNameArgument, message);
+ }
- cloudApplicationName =
- _consoleUtilities.AskUserForValue(
- title,
- defaultName,
- allowEmpty: false,
- defaultAskValuePrompt: inputPrompt);
+ var defaultName = "";
- var validationResult = _cloudApplicationNameGenerator.IsValidName(cloudApplicationName, deployedApplications, deploymentType);
- if (validationResult.IsValid)
- {
- return cloudApplicationName;
- }
-
- _toolInteractiveService.WriteLine();
- _toolInteractiveService.WriteErrorLine(validationResult.ErrorMessage);
- }
+ try
+ {
+ defaultName = cloudApplicationNameGenerator.GenerateValidName(projectDefinition, deployedApplications, deploymentType);
+ }
+ catch (Exception exception)
+ {
+ toolInteractiveService.WriteDebugLine(exception.PrettyPrint());
}
- ///
- /// This method is responsible for selecting a deployment recommendation.
- ///
- /// The deserialized object from the user provided config file.
- /// A List of available recommendations to choose from.
- ///
- private Recommendation GetSelectedRecommendation(DeploymentSettings? deploymentSettings, List recommendations)
+ var cloudApplicationName = string.Empty;
+
+ while (true)
{
- var deploymentSettingsRecipeId = deploymentSettings?.RecipeId;
+ toolInteractiveService.WriteLine();
- if (string.IsNullOrEmpty(deploymentSettingsRecipeId))
- {
- 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 or if a path to a custom CDK deployment project is provided via the '--deployment-project' CLI argument." +
- $"{Environment.NewLine}Please provide a deployment recipe and try again";
+ var title = "Name the Cloud Application to deploy your project to" + Environment.NewLine +
+ "--------------------------------------------------------------------------------";
- throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsDeploymentRecipe, message);
- }
- return _consoleUtilities.AskToChooseRecommendation(recommendations);
+ string inputPrompt;
+
+ switch (deploymentType)
+ {
+ case DeploymentTypes.CdkProject:
+ inputPrompt = Constants.CLI.PROMPT_NEW_STACK_NAME;
+ break;
+ case DeploymentTypes.ElasticContainerRegistryImage:
+ inputPrompt = Constants.CLI.PROMPT_ECR_REPOSITORY_NAME;
+ break;
+ default:
+ throw new InvalidOperationException($"The {nameof(DeploymentTypes)} {deploymentType} does not have an input prompt");
}
- var selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(deploymentSettingsRecipeId, StringComparison.Ordinal));
- if (selectedRecommendation == null)
+ cloudApplicationName =
+ consoleUtilities.AskUserForValue(
+ title,
+ defaultName,
+ allowEmpty: false,
+ defaultAskValuePrompt: inputPrompt);
+
+ var validationResult = cloudApplicationNameGenerator.IsValidName(cloudApplicationName, deployedApplications, deploymentType);
+ if (validationResult.IsValid)
{
- throw new InvalidDeploymentSettingsException(DeployToolErrorCode.InvalidPropertyValueForUserDeployment, $"The user deployment settings provided contains an invalid value for the property '{nameof(deploymentSettings.RecipeId)}'.");
+ return cloudApplicationName;
}
- _toolInteractiveService.WriteLine();
- _toolInteractiveService.WriteLine($"Configuring Recommendation with: '{selectedRecommendation.Name}'.");
- return selectedRecommendation;
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteErrorLine(validationResult.ErrorMessage);
}
+ }
+
+ ///
+ /// This method is responsible for selecting a deployment recommendation.
+ ///
+ /// The deserialized object from the user provided config file.
+ /// A List of available recommendations to choose from.
+ ///
+ private Recommendation GetSelectedRecommendation(DeploymentSettings? deploymentSettings, List recommendations)
+ {
+ var deploymentSettingsRecipeId = deploymentSettings?.RecipeId;
- private bool ConfirmDeployment(Recommendation recommendation)
+ if (string.IsNullOrEmpty(deploymentSettingsRecipeId))
{
- var message = recommendation.Recipe.DeploymentConfirmation?.DefaultMessage;
- if (string.IsNullOrEmpty(message))
- return true;
+ 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 or if a path to a custom CDK deployment project is provided via the '--deployment-project' CLI argument." +
+ $"{Environment.NewLine}Please provide a deployment recipe and try again";
- var result = _consoleUtilities.AskYesNoQuestion(message);
+ throw new InvalidCliArgumentException(DeployToolErrorCode.SilentArgumentNeedsDeploymentRecipe, message);
+ }
+ return consoleUtilities.AskToChooseRecommendation(recommendations);
+ }
- return result == YesNo.Yes;
+ var selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(deploymentSettingsRecipeId, StringComparison.Ordinal));
+ if (selectedRecommendation == null)
+ {
+ throw new InvalidDeploymentSettingsException(DeployToolErrorCode.InvalidPropertyValueForUserDeployment, $"The user deployment settings provided contains an invalid value for the property '{nameof(deploymentSettings.RecipeId)}'.");
}
- private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendation selectedRecommendation, CloudApplication cloudApplication)
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteLine($"Configuring Recommendation with: '{selectedRecommendation.Name}'.");
+ return selectedRecommendation;
+ }
+
+ private bool ConfirmDeployment(Recommendation recommendation)
+ {
+ var message = recommendation.Recipe.DeploymentConfirmation?.DefaultMessage;
+ if (string.IsNullOrEmpty(message))
+ return true;
+
+ var result = consoleUtilities.AskYesNoQuestion(message);
+
+ return result == YesNo.Yes;
+ }
+
+ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendation selectedRecommendation, CloudApplication cloudApplication)
+ {
+ try
{
- try
+ await orchestrator.CreateDeploymentBundle(cloudApplication, selectedRecommendation);
+ }
+ catch(FailedToCreateDeploymentBundleException ex) when (ex.ErrorCode == DeployToolErrorCode.DockerBuildFailed)
+ {
+ if (toolInteractiveService.DisableInteractive)
{
- await orchestrator.CreateDeploymentBundle(cloudApplication, selectedRecommendation);
+ throw;
}
- catch(FailedToCreateDeploymentBundleException ex) when (ex.ErrorCode == DeployToolErrorCode.DockerBuildFailed)
- {
- if (_toolInteractiveService.DisableInteractive)
- {
- throw;
- }
- _toolInteractiveService.WriteLine("Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile." +
- " You can try setting the 'Docker Execution Directory' in the option settings.");
+ toolInteractiveService.WriteLine("Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile." +
+ " You can try setting the 'Docker Execution Directory' in the option settings.");
- _toolInteractiveService.WriteLine(string.Empty);
- var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "false");
- if (answer == YesNo.Yes)
+ toolInteractiveService.WriteLine(string.Empty);
+ var answer = consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "false");
+ if (answer == YesNo.Yes)
+ {
+ string dockerExecutionDirectory;
+ do
{
- string dockerExecutionDirectory;
- do
- {
- dockerExecutionDirectory = _consoleUtilities.AskUserForValue(
- "Enter the docker execution directory where the docker build command will be executed from:",
- selectedRecommendation.DeploymentBundle.DockerExecutionDirectory,
- allowEmpty: true);
+ dockerExecutionDirectory = consoleUtilities.AskUserForValue(
+ "Enter the docker execution directory where the docker build command will be executed from:",
+ selectedRecommendation.DeploymentBundle.DockerExecutionDirectory,
+ allowEmpty: true);
- if (!_directoryManager.Exists(dockerExecutionDirectory))
- {
- _toolInteractiveService.WriteErrorLine($"Error, directory does not exist \"{dockerExecutionDirectory}\"");
- }
- } while (!_directoryManager.Exists(dockerExecutionDirectory));
+ if (!directoryManager.Exists(dockerExecutionDirectory))
+ {
+ toolInteractiveService.WriteErrorLine($"Error, directory does not exist \"{dockerExecutionDirectory}\"");
+ }
+ } while (!directoryManager.Exists(dockerExecutionDirectory));
- selectedRecommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory;
- await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication);
- }
- else
- {
- throw;
- }
+ selectedRecommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory;
+ await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication);
}
- }
-
- private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEnumerable configurableOptionSettings, bool showAdvancedSettings)
- {
- _toolInteractiveService.WriteLine(string.Empty);
-
- while (true)
+ else
{
- var message = "Current settings (select number to change its value)";
- var title = message + Environment.NewLine + new string('-', message.Length);
+ throw;
+ }
+ }
+ }
- _toolInteractiveService.WriteLine(title);
+ private async Task ConfigureDeploymentFromCli(OrchestratorSession session, Recommendation recommendation, IEnumerable configurableOptionSettings, bool showAdvancedSettings)
+ {
+ toolInteractiveService.WriteLine(string.Empty);
- var optionSettings =
- configurableOptionSettings
- .Where(x => (!recommendation.IsExistingCloudApplication || x.Updatable) && (!x.AdvancedSetting || showAdvancedSettings) && _optionSettingHandler.IsOptionSettingDisplayable(recommendation, x))
- .ToArray();
+ while (true)
+ {
+ var message = "Current settings (select number to change its value)";
+ var title = message + Environment.NewLine + new string('-', message.Length);
- for (var i = 1; i <= optionSettings.Length; i++)
- {
- DisplayOptionSetting(recommendation, optionSettings[i - 1], i, optionSettings.Length, DisplayOptionSettingsMode.Editable);
- }
+ toolInteractiveService.WriteLine(title);
- _toolInteractiveService.WriteLine();
- if (!showAdvancedSettings)
- {
- // Don't bother showing 'more' for advanced options if there aren't any advanced options.
- if (configurableOptionSettings.Any(x => x.AdvancedSetting))
- {
- _toolInteractiveService.WriteLine("Enter 'more' to display Advanced settings. ");
- }
- }
- _toolInteractiveService.WriteLine("Or press 'Enter' to deploy:");
+ var optionSettings =
+ configurableOptionSettings
+ .Where(x => (!recommendation.IsExistingCloudApplication || x.Updatable) && (!x.AdvancedSetting || showAdvancedSettings) && optionSettingHandler.IsOptionSettingDisplayable(recommendation, x))
+ .ToArray();
- var input = _toolInteractiveService.ReadLine();
+ for (var i = 1; i <= optionSettings.Length; i++)
+ {
+ DisplayOptionSetting(recommendation, optionSettings[i - 1], i, optionSettings.Length, DisplayOptionSettingsMode.Editable);
+ }
- // advanced - break to main loop to reprint menu
- if (input.Trim().ToLower().Equals("more"))
+ toolInteractiveService.WriteLine();
+ if (!showAdvancedSettings)
+ {
+ // Don't bother showing 'more' for advanced options if there aren't any advanced options.
+ if (configurableOptionSettings.Any(x => x.AdvancedSetting))
{
- showAdvancedSettings = true;
- _toolInteractiveService.WriteLine();
- continue;
+ toolInteractiveService.WriteLine("Enter 'more' to display Advanced settings. ");
}
+ }
+ toolInteractiveService.WriteLine("Or press 'Enter' to deploy:");
- // deploy case, nothing more to configure
- if (string.IsNullOrEmpty(input))
- {
- var settingValidatorFailedResults = _optionSettingHandler.RunOptionSettingValidators(recommendation);
-
- var recipeValidatorFailedResults = _recipeHandler.RunRecipeValidators(recommendation, _session);
+ var input = toolInteractiveService.ReadLine();
- if (!settingValidatorFailedResults.Any() && !recipeValidatorFailedResults.Any())
- {
- // validation successful
- // deployment configured
- return;
- }
+ // advanced - break to main loop to reprint menu
+ if (input.Trim().ToLower().Equals("more"))
+ {
+ showAdvancedSettings = true;
+ toolInteractiveService.WriteLine();
+ continue;
+ }
- _toolInteractiveService.WriteLine();
- _toolInteractiveService.WriteErrorLine("The deployment configuration needs to be adjusted before it can be deployed:");
- foreach (var result in settingValidatorFailedResults)
- _toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}");
- foreach (var result in recipeValidatorFailedResults)
- _toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}");
+ // deploy case, nothing more to configure
+ if (string.IsNullOrEmpty(input))
+ {
+ var settingValidatorFailedResults = optionSettingHandler.RunOptionSettingValidators(recommendation);
- _toolInteractiveService.WriteLine();
- _toolInteractiveService.WriteErrorLine("Please adjust your settings");
- }
+ var recipeValidatorFailedResults = recipeHandler.RunRecipeValidators(recommendation, session);
- // configure option setting
- if (int.TryParse(input, out var selectedNumber) &&
- selectedNumber >= 1 &&
- selectedNumber <= optionSettings.Length)
+ if (!settingValidatorFailedResults.Any() && !recipeValidatorFailedResults.Any())
{
- await ConfigureDeploymentFromCli(recommendation, optionSettings[selectedNumber - 1]);
+ // validation successful
+ // deployment configured
+ return;
}
- _toolInteractiveService.WriteLine();
- }
- }
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteErrorLine("The deployment configuration needs to be adjusted before it can be deployed:");
+ foreach (var result in settingValidatorFailedResults)
+ toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}");
+ foreach (var result in recipeValidatorFailedResults)
+ toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}");
- enum DisplayOptionSettingsMode { Editable, Readonly }
- private void DisplayOptionSetting(Recommendation recommendation, OptionSettingItem optionSetting, int optionSettingNumber, int optionSettingsCount, DisplayOptionSettingsMode mode)
- {
- var value = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting);
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteErrorLine("Please adjust your settings");
+ }
- Type? typeHintResponseType = null;
- if (optionSetting.Type == OptionSettingValueType.Object)
+ // configure option setting
+ if (int.TryParse(input, out var selectedNumber) &&
+ selectedNumber >= 1 &&
+ selectedNumber <= optionSettings.Length)
{
- var typeHintResponseTypeFullName = $"AWS.Deploy.CLI.TypeHintResponses.{optionSetting.TypeHint}TypeHintResponse";
- typeHintResponseType = Assembly.GetExecutingAssembly().GetType(typeHintResponseTypeFullName);
+ await ConfigureDeploymentFromCli(recommendation, optionSettings[selectedNumber - 1]);
}
- DisplayValue(recommendation, optionSetting, optionSettingNumber, optionSettingsCount, typeHintResponseType, mode);
+ toolInteractiveService.WriteLine();
}
+ }
+
+ enum DisplayOptionSettingsMode { Editable, Readonly }
+ private void DisplayOptionSetting(Recommendation recommendation, OptionSettingItem optionSetting, int optionSettingNumber, int optionSettingsCount, DisplayOptionSettingsMode mode)
+ {
+ var value = optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting);
- private async Task ConfigureDeploymentFromCli(Recommendation recommendation, OptionSettingItem setting)
+ Type? typeHintResponseType = null;
+ if (optionSetting.Type == OptionSettingValueType.Object)
{
- _toolInteractiveService.WriteLine(string.Empty);
- _toolInteractiveService.WriteLine($"{setting.Name}:");
- _toolInteractiveService.WriteLine($"{setting.Description}");
+ var typeHintResponseTypeFullName = $"AWS.Deploy.CLI.TypeHintResponses.{optionSetting.TypeHint}TypeHintResponse";
+ typeHintResponseType = Assembly.GetExecutingAssembly().GetType(typeHintResponseTypeFullName);
+ }
- object currentValue = _optionSettingHandler.GetOptionSettingValue(recommendation, setting);
- object? settingValue = null;
+ DisplayValue(recommendation, optionSetting, optionSettingNumber, optionSettingsCount, typeHintResponseType, mode);
+ }
- if (setting.TypeHint.HasValue && _typeHintCommandFactory.GetCommand(setting.TypeHint.Value) is var typeHintCommand && typeHintCommand != null)
- {
- settingValue = await typeHintCommand.Execute(recommendation, setting);
- }
- else
+ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, OptionSettingItem setting)
+ {
+ toolInteractiveService.WriteLine(string.Empty);
+ toolInteractiveService.WriteLine($"{setting.Name}:");
+ toolInteractiveService.WriteLine($"{setting.Description}");
+
+ object currentValue = optionSettingHandler.GetOptionSettingValue(recommendation, setting);
+ object? settingValue = null;
+
+ if (setting.TypeHint.HasValue && typeHintCommandFactory.GetCommand(setting.TypeHint.Value) is var typeHintCommand && typeHintCommand != null)
+ {
+ settingValue = await typeHintCommand.Execute(recommendation, setting);
+ }
+ else
+ {
+ if (setting.AllowedValues?.Count > 0)
{
- if (setting.AllowedValues?.Count > 0)
+ var userInputConfig = new UserInputConfiguration(
+ idSelector: x => x,
+ displaySelector: x => setting.ValueMapping.ContainsKey(x) ? setting.ValueMapping[x] : x,
+ defaultSelector: x => x.Equals(currentValue))
{
- var userInputConfig = new UserInputConfiguration(
- idSelector: x => x,
- displaySelector: x => setting.ValueMapping.ContainsKey(x) ? setting.ValueMapping[x] : x,
- defaultSelector: x => x.Equals(currentValue))
- {
- CreateNew = false
- };
+ CreateNew = false
+ };
- var userResponse = _consoleUtilities.AskUserToChooseOrCreateNew(setting.AllowedValues, string.Empty, userInputConfig);
- settingValue = userResponse.SelectedOption;
+ var userResponse = consoleUtilities.AskUserToChooseOrCreateNew(setting.AllowedValues, string.Empty, userInputConfig);
+ settingValue = userResponse.SelectedOption;
- // If they didn't change the value then don't store so we can rely on using the default in the recipe.
- if (Equals(settingValue, currentValue))
- return;
- }
- else
- {
- switch (setting.Type)
- {
- case OptionSettingValueType.String:
- case OptionSettingValueType.Int:
- case OptionSettingValueType.Double:
- settingValue = _consoleUtilities.AskUserForValue(string.Empty, currentValue.ToString() ?? "", allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, setting) ?? "");
- break;
- case OptionSettingValueType.Bool:
- var answer = _consoleUtilities.AskYesNoQuestion(string.Empty, _optionSettingHandler.GetOptionSettingValue(recommendation, setting).ToString());
- settingValue = answer == YesNo.Yes ? "true" : "false";
- break;
- case OptionSettingValueType.KeyValue:
- settingValue = _consoleUtilities.AskUserForKeyValue(!string.IsNullOrEmpty(currentValue.ToString()) ? (Dictionary) currentValue : new Dictionary());
- break;
- case OptionSettingValueType.List:
- var valueList = new SortedSet();
- if (!string.IsNullOrEmpty(currentValue.ToString()))
- valueList = ((SortedSet) currentValue).DeepCopy();
- settingValue = _consoleUtilities.AskUserForList(valueList);
- break;
- case OptionSettingValueType.Object:
- foreach (var childSetting in setting.ChildOptionSettings)
- {
- if (_optionSettingHandler.IsOptionSettingDisplayable(recommendation, childSetting) && (!recommendation.IsExistingCloudApplication || childSetting.Updatable))
- await ConfigureDeploymentFromCli(recommendation, childSetting);
- }
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
+ // If they didn't change the value then don't store so we can rely on using the default in the recipe.
+ if (Equals(settingValue, currentValue))
+ return;
}
-
- if (settingValue != null)
+ else
{
- try
+ switch (setting.Type)
{
- await _optionSettingHandler.SetOptionSettingValue(recommendation, setting, settingValue);
- }
- catch (ValidationFailedException ex)
- {
- _toolInteractiveService.WriteErrorLine(ex.Message);
-
- await ConfigureDeploymentFromCli(recommendation, setting);
+ case OptionSettingValueType.String:
+ case OptionSettingValueType.Int:
+ case OptionSettingValueType.Double:
+ settingValue = consoleUtilities.AskUserForValue(string.Empty, currentValue.ToString() ?? "", allowEmpty: true, resetValue: optionSettingHandler.GetOptionSettingDefaultValue(recommendation, setting) ?? "");
+ break;
+ case OptionSettingValueType.Bool:
+ var answer = consoleUtilities.AskYesNoQuestion(string.Empty, optionSettingHandler.GetOptionSettingValue(recommendation, setting).ToString());
+ settingValue = answer == YesNo.Yes ? "true" : "false";
+ break;
+ case OptionSettingValueType.KeyValue:
+ settingValue = consoleUtilities.AskUserForKeyValue(!string.IsNullOrEmpty(currentValue.ToString()) ? (Dictionary) currentValue : new Dictionary());
+ break;
+ case OptionSettingValueType.List:
+ var valueList = new SortedSet();
+ if (!string.IsNullOrEmpty(currentValue.ToString()))
+ valueList = ((SortedSet) currentValue).DeepCopy();
+ settingValue = consoleUtilities.AskUserForList(valueList);
+ break;
+ case OptionSettingValueType.Object:
+ foreach (var childSetting in setting.ChildOptionSettings)
+ {
+ if (optionSettingHandler.IsOptionSettingDisplayable(recommendation, childSetting) && (!recommendation.IsExistingCloudApplication || childSetting.Updatable))
+ await ConfigureDeploymentFromCli(recommendation, childSetting);
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
}
}
- ///
- /// Uses reflection to call with the Object type option setting value
- /// This allows to use a generic implementation to display Object type option setting values without casting the response to
- /// the specific TypeHintResponse type.
- ///
- private void DisplayValue(Recommendation recommendation, OptionSettingItem optionSetting, int optionSettingNumber, int optionSettingsCount, Type? typeHintResponseType, DisplayOptionSettingsMode mode)
+ if (settingValue != null)
{
- object? displayValue = null;
- Dictionary? keyValuePair = null;
- Dictionary? objectValues = null;
- SortedSet? listValues = null;
- if (typeHintResponseType != null)
+ try
{
- var methodInfo = typeof(IOptionSettingHandler)
- .GetMethod(nameof(IOptionSettingHandler.GetOptionSettingValue), 1, new[] { typeof(Recommendation), typeof(OptionSettingItem) });
- var genericMethodInfo = methodInfo?.MakeGenericMethod(typeHintResponseType);
- var response = genericMethodInfo?.Invoke(_optionSettingHandler, new object[] { recommendation, optionSetting });
-
- displayValue = ((IDisplayable?)response)?.ToDisplayString();
+ await optionSettingHandler.SetOptionSettingValue(recommendation, setting, settingValue);
}
-
- if (displayValue == null)
+ catch (ValidationFailedException ex)
{
- var value = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting);
- objectValues = value as Dictionary;
- keyValuePair = value as Dictionary;
- listValues = value as SortedSet;
- displayValue = objectValues == null && keyValuePair == null && listValues == null ? value : string.Empty;
- }
+ toolInteractiveService.WriteErrorLine(ex.Message);
- if (mode == DisplayOptionSettingsMode.Editable)
- {
- _toolInteractiveService.WriteLine($"{optionSettingNumber.ToString().PadRight(optionSettingsCount.ToString().Length)}. {optionSetting.Name}: {displayValue}");
- }
- else if (mode == DisplayOptionSettingsMode.Readonly)
- {
- _toolInteractiveService.WriteLine($"{optionSetting.Name}: {displayValue}");
+ await ConfigureDeploymentFromCli(recommendation, setting);
}
+ }
+ }
+
+ ///
+ /// Uses reflection to call with the Object type option setting value
+ /// This allows to use a generic implementation to display Object type option setting values without casting the response to
+ /// the specific TypeHintResponse type.
+ ///
+ private void DisplayValue(Recommendation recommendation, OptionSettingItem optionSetting, int optionSettingNumber, int optionSettingsCount, Type? typeHintResponseType, DisplayOptionSettingsMode mode)
+ {
+ object? displayValue = null;
+ Dictionary? keyValuePair = null;
+ Dictionary? objectValues = null;
+ SortedSet? listValues = null;
+ if (typeHintResponseType != null)
+ {
+ var methodInfo = typeof(IOptionSettingHandler)
+ .GetMethod(nameof(IOptionSettingHandler.GetOptionSettingValue), 1, new[] { typeof(Recommendation), typeof(OptionSettingItem) });
+ var genericMethodInfo = methodInfo?.MakeGenericMethod(typeHintResponseType);
+ var response = genericMethodInfo?.Invoke(optionSettingHandler, new object[] { recommendation, optionSetting });
+
+ displayValue = ((IDisplayable?)response)?.ToDisplayString();
+ }
+
+ if (displayValue == null)
+ {
+ var value = optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting);
+ objectValues = value as Dictionary;
+ keyValuePair = value as Dictionary;
+ listValues = value as SortedSet;
+ displayValue = objectValues == null && keyValuePair == null && listValues == null ? value : string.Empty;
+ }
- if (keyValuePair != null)
+ if (mode == DisplayOptionSettingsMode.Editable)
+ {
+ toolInteractiveService.WriteLine($"{optionSettingNumber.ToString().PadRight(optionSettingsCount.ToString().Length)}. {optionSetting.Name}: {displayValue}");
+ }
+ else if (mode == DisplayOptionSettingsMode.Readonly)
+ {
+ toolInteractiveService.WriteLine($"{optionSetting.Name}: {displayValue}");
+ }
+
+ if (keyValuePair != null)
+ {
+ foreach (var (key, value) in keyValuePair)
{
- foreach (var (key, value) in keyValuePair)
- {
- _toolInteractiveService.WriteLine($"\t{key}: {value}");
- }
+ toolInteractiveService.WriteLine($"\t{key}: {value}");
}
+ }
- if (listValues != null)
+ if (listValues != null)
+ {
+ foreach (var value in listValues)
{
- foreach (var value in listValues)
- {
- _toolInteractiveService.WriteLine($"\t{value}");
- }
+ toolInteractiveService.WriteLine($"\t{value}");
}
+ }
- if (objectValues != null)
+ if (objectValues != null)
+ {
+ var displayableValues = new Dictionary();
+ foreach (var child in optionSetting.ChildOptionSettings)
{
- var displayableValues = new Dictionary();
- foreach (var child in optionSetting.ChildOptionSettings)
- {
- if (!objectValues.ContainsKey(child.Id))
- continue;
- displayableValues.Add(child.Name, objectValues[child.Id]);
- }
- _consoleUtilities.DisplayValues(displayableValues, "\t");
+ if (!objectValues.ContainsKey(child.Id))
+ continue;
+ displayableValues.Add(child.Name, objectValues[child.Id]);
}
+ consoleUtilities.DisplayValues(displayableValues, "\t");
}
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs
index e465b1b8e..5d39cdf9b 100644
--- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs
+++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs
@@ -4,267 +4,267 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
using System.Threading.Tasks;
+using AWS.Deploy.CLI.Commands.Settings;
+using AWS.Deploy.CLI.Utilities;
using AWS.Deploy.Common;
using AWS.Deploy.Common.DeploymentManifest;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Common.Recipes;
using AWS.Deploy.Orchestration;
using AWS.Deploy.Orchestration.Utilities;
-using AWS.Deploy.Recipes;
using Newtonsoft.Json;
+using Spectre.Console.Cli;
-namespace AWS.Deploy.CLI.Commands
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// The class supports the functionality to create a new CDK project and save it at a customer
+/// specified directory location.
+///
+public class GenerateDeploymentProjectCommand(
+ IToolInteractiveService toolInteractiveService,
+ IConsoleUtilities consoleUtilities,
+ ICdkProjectHandler cdkProjectHandler,
+ ICommandLineWrapper commandLineWrapper,
+ IDirectoryManager directoryManager,
+ IFileManager fileManager,
+ IDeploymentManifestEngine deploymentManifestEngine,
+ IRecipeHandler recipeHandler,
+ IProjectParserUtility projectParserUtility)
+ : CancellableAsyncCommand
{
+ private const int DEFAULT_PERSISTED_RECIPE_PRIORITY = 1000;
+
///
- /// The class supports the functionality to create a new CDK project and save it at a customer
- /// specified directory location.
+ /// This method takes a user specified directory path and generates the CDK deployment project at this location.
+ /// If the provided directory path is an empty string, then a default directory is created to save the CDK deployment project.
///
- public class GenerateDeploymentProjectCommand
+ /// Command context
+ /// Command settings
+ /// Cancellation token source
+ /// The command exit code
+ public override async Task ExecuteAsync(CommandContext context, GenerateDeploymentProjectCommandSettings settings, CancellationTokenSource cancellationTokenSource)
{
- private const int DEFAULT_PERSISTED_RECIPE_PRIORITY = 1000;
-
- private readonly IToolInteractiveService _toolInteractiveService;
- private readonly IConsoleUtilities _consoleUtilities;
- private readonly ICdkProjectHandler _cdkProjectHandler;
- private readonly ICommandLineWrapper _commandLineWrapper;
- private readonly IDirectoryManager _directoryManager;
- private readonly IFileManager _fileManager;
- private readonly OrchestratorSession _session;
- private readonly IDeploymentManifestEngine _deploymentManifestEngine;
- private readonly IRecipeHandler _recipeHandler;
- private readonly string _targetApplicationFullPath;
-
- public GenerateDeploymentProjectCommand(
- IToolInteractiveService toolInteractiveService,
- IConsoleUtilities consoleUtilities,
- ICdkProjectHandler cdkProjectHandler,
- ICommandLineWrapper commandLineWrapper,
- IDirectoryManager directoryManager,
- IFileManager fileManager,
- OrchestratorSession session,
- IDeploymentManifestEngine deploymentManifestEngine,
- IRecipeHandler recipeHandler,
- string targetApplicationFullPath)
+ toolInteractiveService.Diagnostics = settings.Diagnostics;
+
+ var projectDefinition = await projectParserUtility.Parse(settings.ProjectPath ?? "");
+
+ var saveDirectory = settings.Output;
+ var projectDisplayName = settings.ProjectDisplayName;
+
+ OrchestratorSession session = new OrchestratorSession(projectDefinition);
+
+ var targetApplicationFullPath = new DirectoryInfo(projectDefinition.ProjectPath).FullName;
+
+ if (!string.IsNullOrEmpty(saveDirectory))
{
- _toolInteractiveService = toolInteractiveService;
- _consoleUtilities = consoleUtilities;
- _cdkProjectHandler = cdkProjectHandler;
- _commandLineWrapper = commandLineWrapper;
- _directoryManager = directoryManager;
- _fileManager = fileManager;
- _session = session;
- _deploymentManifestEngine = deploymentManifestEngine;
- _recipeHandler = recipeHandler;
- _targetApplicationFullPath = targetApplicationFullPath;
+ var targetApplicationDirectoryFullPath = new DirectoryInfo(targetApplicationFullPath).Parent!.FullName;
+ saveDirectory = Path.GetFullPath(saveDirectory, targetApplicationDirectoryFullPath);
}
- ///
- /// This method takes a user specified directory path and generates the CDK deployment project at this location.
- /// If the provided directory path is an empty string, then a default directory is created to save the CDK deployment project.
- ///
- /// An absolute or a relative path provided by the user.
- /// The name of the deployment project that will be displayed in the list of available deployment options.
- ///
- public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDisplayName)
+ var orchestrator = new Orchestrator(session, recipeHandler);
+ var recommendations = await GenerateRecommendationsToSaveDeploymentProject(orchestrator);
+ var selectedRecommendation = consoleUtilities.AskToChooseRecommendation(recommendations);
+
+ if (string.IsNullOrEmpty(saveDirectory))
+ saveDirectory = GenerateDefaultSaveDirectoryPath(targetApplicationFullPath);
+
+ var newDirectoryCreated = CreateSaveCdkDirectory(saveDirectory);
+
+ var (isValid, errorMessage) = ValidateSaveCdkDirectory(saveDirectory, targetApplicationFullPath);
+ if (!isValid)
{
- var orchestrator = new Orchestrator(_session, _recipeHandler);
- var recommendations = await GenerateRecommendationsToSaveDeploymentProject(orchestrator);
- var selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations);
+ if (newDirectoryCreated)
+ directoryManager.Delete(saveDirectory);
+ errorMessage = $"Failed to generate deployment project.{Environment.NewLine}{errorMessage}";
+ throw new InvalidSaveDirectoryForCdkProject(DeployToolErrorCode.InvalidSaveDirectoryForCdkProject, errorMessage.Trim());
+ }
- if (string.IsNullOrEmpty(saveCdkDirectoryPath))
- saveCdkDirectoryPath = GenerateDefaultSaveDirectoryPath();
+ var directoryUnderSourceControl = await IsDirectoryUnderSourceControl(saveDirectory);
+ if (!directoryUnderSourceControl)
+ {
+ var userPrompt = "Warning: The target directory is not being tracked by source control. If the saved deployment " +
+ "project is used for deployment it is important that the deployment project is retained to allow " +
+ "future redeployments to previously deployed applications. " + Environment.NewLine + Environment.NewLine +
+ "Do you still want to continue?";
- var newDirectoryCreated = CreateSaveCdkDirectory(saveCdkDirectoryPath);
+ toolInteractiveService.WriteLine();
+ var yesNoResult = consoleUtilities.AskYesNoQuestion(userPrompt, YesNo.Yes);
- var (isValid, errorMessage) = ValidateSaveCdkDirectory(saveCdkDirectoryPath);
- if (!isValid)
+ if (yesNoResult == YesNo.No)
{
if (newDirectoryCreated)
- _directoryManager.Delete(saveCdkDirectoryPath);
- errorMessage = $"Failed to generate deployment project.{Environment.NewLine}{errorMessage}";
- throw new InvalidSaveDirectoryForCdkProject(DeployToolErrorCode.InvalidSaveDirectoryForCdkProject, errorMessage.Trim());
- }
-
- var directoryUnderSourceControl = await IsDirectoryUnderSourceControl(saveCdkDirectoryPath);
- if (!directoryUnderSourceControl)
- {
- var userPrompt = "Warning: The target directory is not being tracked by source control. If the saved deployment " +
- "project is used for deployment it is important that the deployment project is retained to allow " +
- "future redeployments to previously deployed applications. " + Environment.NewLine + Environment.NewLine +
- "Do you still want to continue?";
-
- _toolInteractiveService.WriteLine();
- var yesNoResult = _consoleUtilities.AskYesNoQuestion(userPrompt, YesNo.Yes);
-
- if (yesNoResult == YesNo.No)
- {
- if (newDirectoryCreated)
- _directoryManager.Delete(saveCdkDirectoryPath);
- return;
- }
+ directoryManager.Delete(saveDirectory);
+ return CommandReturnCodes.SUCCESS;
}
+ }
- _cdkProjectHandler.CreateCdkProject(selectedRecommendation, _session, saveCdkDirectoryPath);
- await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveCdkDirectoryPath, projectDisplayName);
+ cdkProjectHandler.CreateCdkProject(selectedRecommendation, session, saveDirectory);
+ await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveDirectory, projectDisplayName, targetApplicationFullPath);
- var saveCdkDirectoryFullPath = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).FullName;
- _toolInteractiveService.WriteLine();
- _toolInteractiveService.WriteLine($"Saving AWS CDK deployment project to: {saveCdkDirectoryFullPath}");
+ var saveCdkDirectoryFullPath = directoryManager.GetDirectoryInfo(saveDirectory).FullName;
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteLine($"Saving AWS CDK deployment project to: {saveCdkDirectoryFullPath}");
- await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, _targetApplicationFullPath);
- }
+ await deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, targetApplicationFullPath);
+
+ return CommandReturnCodes.SUCCESS;
+ }
- ///
- /// This method generates the appropriate recommendations for the target deployment project.
- ///
- ///
- /// A list of
- private async Task> GenerateRecommendationsToSaveDeploymentProject(Orchestrator orchestrator)
+ ///
+ /// This method generates the appropriate recommendations for the target deployment project.
+ ///
+ ///
+ /// A list of
+ private async Task> GenerateRecommendationsToSaveDeploymentProject(Orchestrator orchestrator)
+ {
+ var recommendations = await orchestrator.GenerateRecommendationsToSaveDeploymentProject();
+ if (recommendations.Count == 0)
{
- var recommendations = await orchestrator.GenerateRecommendationsToSaveDeploymentProject();
- if (recommendations.Count == 0)
- {
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.DeploymentProjectNotSupported, "The project you are trying to deploy is currently not supported.");
- }
- return recommendations;
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.DeploymentProjectNotSupported, "The project you are trying to deploy is currently not supported.");
}
+ return recommendations;
+ }
- ///
- /// This method takes the path to the target deployment project and creates
- /// a default save directory inside the parent folder of the current directory.
- /// For example:
- /// Target project directory - C:\Codebase\MyWebApp
- /// Generated default save directory - C:\Codebase\MyWebApp.Deployment If the save directory already exists, then a suffix number is attached.
- ///
- /// The default save directory path.
- private string GenerateDefaultSaveDirectoryPath()
+ ///
+ /// This method takes the path to the target deployment project and creates
+ /// a default save directory inside the parent folder of the current directory.
+ /// For example:
+ /// Target project directory - C:\Codebase\MyWebApp
+ /// Generated default save directory - C:\Codebase\MyWebApp.Deployment If the save directory already exists, then a suffix number is attached.
+ ///
+ /// The full path of the target application.
+ /// The default save directory path.
+ private string GenerateDefaultSaveDirectoryPath(string targetApplicationFullPath)
+ {
+ var targetApplicationDi = directoryManager.GetDirectoryInfo(targetApplicationFullPath);
+ if(targetApplicationDi.Parent == null)
{
- var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
- if(targetApplicationDi.Parent == null)
- {
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
- }
- var applicatonDirectoryFullPath = targetApplicationDi.Parent.FullName;
- var saveCdkDirectoryFullPath = applicatonDirectoryFullPath + ".Deployment";
-
- var suffixNumber = 0;
- while (_directoryManager.Exists(saveCdkDirectoryFullPath))
- saveCdkDirectoryFullPath = applicatonDirectoryFullPath + $".Deployment{++suffixNumber}";
-
- return saveCdkDirectoryFullPath;
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {targetApplicationFullPath}.");
}
+ var applicatonDirectoryFullPath = targetApplicationDi.Parent.FullName;
+ var saveCdkDirectoryFullPath = applicatonDirectoryFullPath + ".Deployment";
+
+ var suffixNumber = 0;
+ while (directoryManager.Exists(saveCdkDirectoryFullPath))
+ saveCdkDirectoryFullPath = applicatonDirectoryFullPath + $".Deployment{++suffixNumber}";
+
+ return saveCdkDirectoryFullPath;
+ }
- ///
- /// This method takes a path and creates a new directory at this path if one does not already exist.
- ///
- /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
- /// A boolean to indicate if a new directory was created.
- private bool CreateSaveCdkDirectory(string saveCdkDirectoryPath)
+ ///
+ /// This method takes a path and creates a new directory at this path if one does not already exist.
+ ///
+ /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
+ /// A boolean to indicate if a new directory was created.
+ private bool CreateSaveCdkDirectory(string saveCdkDirectoryPath)
+ {
+ var newDirectoryCreated = false;
+ if (!directoryManager.Exists(saveCdkDirectoryPath))
{
- var newDirectoryCreated = false;
- if (!_directoryManager.Exists(saveCdkDirectoryPath))
- {
- _directoryManager.CreateDirectory(saveCdkDirectoryPath);
- newDirectoryCreated = true;
- }
- return newDirectoryCreated;
+ directoryManager.CreateDirectory(saveCdkDirectoryPath);
+ newDirectoryCreated = true;
}
+ return newDirectoryCreated;
+ }
- ///
- /// This method takes the path to the intended location of the CDK deployment project and performs validations on it.
- ///
- /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
- /// A tuple containing a boolean that indicates if the directory is valid and a corresponding string error message.
- private Tuple ValidateSaveCdkDirectory(string saveCdkDirectoryPath)
+ ///
+ /// This method takes the path to the intended location of the CDK deployment project and performs validations on it.
+ ///
+ /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
+ /// The full path of the target application.
+ /// A tuple containing a boolean that indicates if the directory is valid and a corresponding string error message.
+ private Tuple ValidateSaveCdkDirectory(string saveCdkDirectoryPath, string targetApplicationFullPath)
+ {
+ var targetApplicationDi = directoryManager.GetDirectoryInfo(targetApplicationFullPath);
+ if (targetApplicationDi.Parent == null)
{
- var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
- if (targetApplicationDi.Parent == null)
- {
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
- }
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {targetApplicationFullPath}.");
+ }
- var errorMessage = string.Empty;
- var isValid = true;
- var targetApplicationDirectoryFullPath = targetApplicationDi.Parent.FullName;
-
- if (!_directoryManager.IsEmpty(saveCdkDirectoryPath))
- {
- errorMessage += "The directory specified for saving the CDK project is non-empty. " +
- "Please provide an empty directory path and try again." + Environment.NewLine;
+ var errorMessage = string.Empty;
+ var isValid = true;
+ var targetApplicationDirectoryFullPath = targetApplicationDi.Parent.FullName;
- isValid = false;
- }
- if (_directoryManager.ExistsInsideDirectory(targetApplicationDirectoryFullPath, saveCdkDirectoryPath))
- {
- errorMessage += "The directory used to save the CDK deployment project is contained inside of " +
- "the target application project directory. Please specify a different directory and try again.";
+ if (!directoryManager.IsEmpty(saveCdkDirectoryPath))
+ {
+ errorMessage += "The directory specified for saving the CDK project is non-empty. " +
+ "Please provide an empty directory path and try again." + Environment.NewLine;
- isValid = false;
- }
+ isValid = false;
+ }
+ if (directoryManager.ExistsInsideDirectory(targetApplicationDirectoryFullPath, saveCdkDirectoryPath))
+ {
+ errorMessage += "The directory used to save the CDK deployment project is contained inside of " +
+ "the target application project directory. Please specify a different directory and try again.";
- return new Tuple(isValid, errorMessage.Trim());
+ isValid = false;
}
- ///
- /// Checks if the location of the saved CDK deployment project is monitored by a source control system.
- ///
- /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
- ///
- private async Task IsDirectoryUnderSourceControl(string saveCdkDirectoryPath)
+ return new Tuple(isValid, errorMessage.Trim());
+ }
+
+ ///
+ /// Checks if the location of the saved CDK deployment project is monitored by a source control system.
+ ///
+ /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
+ ///
+ private async Task IsDirectoryUnderSourceControl(string saveCdkDirectoryPath)
+ {
+ var gitStatusResult = await commandLineWrapper.TryRunWithResult("git status", saveCdkDirectoryPath);
+ var svnStatusResult = await commandLineWrapper.TryRunWithResult("svn status", saveCdkDirectoryPath);
+ return gitStatusResult.Success || svnStatusResult.Success;
+ }
+
+ ///
+ /// Generates a snapshot of the deployment recipe inside the location at which the CDK deployment project is saved.
+ ///
+ ///
+ /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
+ /// The name of the deployment project that will be displayed in the list of available deployment options.
+ /// The full path of the target application.
+ private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendation, string saveCdkDirectoryPath, string? projectDisplayName, string targetApplicationFullPath)
+ {
+ var targetApplicationDi = directoryManager.GetDirectoryInfo(targetApplicationFullPath);
+ if (targetApplicationDi.Parent == null)
{
- var gitStatusResult = await _commandLineWrapper.TryRunWithResult("git status", saveCdkDirectoryPath);
- var svnStatusResult = await _commandLineWrapper.TryRunWithResult("svn status", saveCdkDirectoryPath);
- return gitStatusResult.Success || svnStatusResult.Success;
+ throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {targetApplicationFullPath}.");
}
- ///
- /// Generates a snapshot of the deployment recipe inside the location at which the CDK deployment project is saved.
- ///
- ///
- /// Relative or absolute path of the directory at which the CDK deployment project will be saved.
- /// The name of the deployment project that will be displayed in the list of available deployment options.
- private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendation, string saveCdkDirectoryPath, string projectDisplayName)
- {
- var targetApplicationDi = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath);
- if (targetApplicationDi.Parent == null)
- {
- throw new FailedToGenerateAnyRecommendations(DeployToolErrorCode.InvalidFilePath, $"Failed to find parent directory for directory {_targetApplicationFullPath}.");
- }
+ var targetApplicationDirectoryName = targetApplicationDi.Name;
+ var recipeSnapshotFileName = directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).Name + ".recipe";
+ var recipeSnapshotFilePath = Path.Combine(saveCdkDirectoryPath, recipeSnapshotFileName);
+ var recipePath = recommendation.Recipe.RecipePath;
- var targetApplicationDirectoryName = targetApplicationDi.Name;
- var recipeSnapshotFileName = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).Name + ".recipe";
- var recipeSnapshotFilePath = Path.Combine(saveCdkDirectoryPath, recipeSnapshotFileName);
- var recipePath = recommendation.Recipe.RecipePath;
-
- if (string.IsNullOrEmpty(recipePath))
- throw new InvalidOperationException("The recipe path cannot be null or empty as part " +
- $"of the {nameof(recommendation.Recipe)} object");
-
- var recipeBody = await _fileManager.ReadAllTextAsync(recipePath);
- var recipe = JsonConvert.DeserializeObject(recipeBody);
- if (recipe == null)
- throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentProjectRecipe, "Failed to deserialize deployment project recipe");
-
- var recipeName = string.IsNullOrEmpty(projectDisplayName) ?
- $"Deployment project for {targetApplicationDirectoryName} to {recommendation.Recipe.TargetService}"
- : projectDisplayName;
-
- recipe.Id = Guid.NewGuid().ToString();
- recipe.Name = recipeName;
- recipe.CdkProjectTemplateId = null;
- recipe.CdkProjectTemplate = null;
- recipe.PersistedDeploymentProject = true;
- recipe.RecipePriority = DEFAULT_PERSISTED_RECIPE_PRIORITY;
- recipe.BaseRecipeId = recommendation.Recipe.Id;
-
- var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, new JsonSerializerSettings
- {
- Formatting = Formatting.Indented,
- NullValueHandling = NullValueHandling.Ignore,
- ContractResolver = new SerializeModelContractResolver()
- });
- await _fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody);
- }
+ if (string.IsNullOrEmpty(recipePath))
+ throw new InvalidOperationException("The recipe path cannot be null or empty as part " +
+ $"of the {nameof(recommendation.Recipe)} object");
+
+ var recipeBody = await fileManager.ReadAllTextAsync(recipePath);
+ var recipe = JsonConvert.DeserializeObject(recipeBody);
+ if (recipe == null)
+ throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentProjectRecipe, "Failed to deserialize deployment project recipe");
+
+ var recipeName = string.IsNullOrEmpty(projectDisplayName) ?
+ $"Deployment project for {targetApplicationDirectoryName} to {recommendation.Recipe.TargetService}"
+ : projectDisplayName;
+
+ recipe.Id = Guid.NewGuid().ToString();
+ recipe.Name = recipeName;
+ recipe.CdkProjectTemplateId = null;
+ recipe.CdkProjectTemplate = null;
+ recipe.PersistedDeploymentProject = true;
+ recipe.RecipePriority = DEFAULT_PERSISTED_RECIPE_PRIORITY;
+ recipe.BaseRecipeId = recommendation.Recipe.Id;
+
+ var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, new JsonSerializerSettings
+ {
+ Formatting = Formatting.Indented,
+ NullValueHandling = NullValueHandling.Ignore,
+ ContractResolver = new SerializeModelContractResolver()
+ });
+ await fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody);
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs b/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs
index 65f7bc0a3..12d21b142 100644
--- a/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs
+++ b/src/AWS.Deploy.CLI/Commands/ListDeploymentsCommand.cs
@@ -1,39 +1,64 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using Amazon;
+using AWS.Deploy.CLI.Commands.Settings;
+using AWS.Deploy.Common;
+using AWS.Deploy.Common.Data;
using AWS.Deploy.Common.Recipes;
using AWS.Deploy.Orchestration.Utilities;
+using Spectre.Console.Cli;
-namespace AWS.Deploy.CLI.Commands
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// Represents a List command that allows listing deployed applications
+///
+public class ListDeploymentsCommand(
+ IToolInteractiveService toolInteractiveService,
+ IDeployedApplicationQueryer deployedApplicationQueryer,
+ IAWSUtilities awsUtilities,
+ IAWSClientFactory awsClientFactory,
+ IAWSResourceQueryer awsResourceQueryer) : CancellableAsyncCommand
{
- public class ListDeploymentsCommand
+ ///
+ /// List deployed applications
+ ///
+ /// Command context
+ /// Command settings
+ /// Cancellation token source
+ /// The command exit code
+ public override async Task ExecuteAsync(CommandContext context, ListDeploymentsCommandSettings settings, CancellationTokenSource cancellationTokenSource)
{
- private readonly IToolInteractiveService _interactiveService;
- private readonly IDeployedApplicationQueryer _deployedApplicationQueryer;
-
- public ListDeploymentsCommand(
- IToolInteractiveService interactiveService,
- IDeployedApplicationQueryer deployedApplicationQueryer)
+ toolInteractiveService.Diagnostics = settings.Diagnostics;
+
+ var (awsCredentials, regionFromProfile) = await awsUtilities.ResolveAWSCredentials(settings.Profile);
+ var awsRegion = awsUtilities.ResolveAWSRegion(settings.Region ?? regionFromProfile);
+
+ awsClientFactory.ConfigureAWSOptions(awsOptions =>
{
- _interactiveService = interactiveService;
- _deployedApplicationQueryer = deployedApplicationQueryer;
- }
+ awsOptions.Credentials = awsCredentials;
+ awsOptions.Region = RegionEndpoint.GetBySystemName(awsRegion);
+ });
- public async Task ExecuteAsync()
+ await awsResourceQueryer.GetCallerIdentity(awsRegion);
+
+ // Add Header
+ toolInteractiveService.WriteLine();
+ toolInteractiveService.WriteLine("Cloud Applications:");
+ toolInteractiveService.WriteLine("-------------------");
+
+ var deploymentTypes = new List(){ DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment };
+ var existingApplications = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes);
+ foreach (var app in existingApplications)
{
- // Add Header
- _interactiveService.WriteLine();
- _interactiveService.WriteLine("Cloud Applications:");
- _interactiveService.WriteLine("-------------------");
-
- var deploymentTypes = new List(){ DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment };
- var existingApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes);
- foreach (var app in existingApplications)
- {
- _interactiveService.WriteLine(app.DisplayName);
- }
+ toolInteractiveService.WriteLine(app.DisplayName);
}
+
+ return CommandReturnCodes.SUCCESS;
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/RootCommand.cs b/src/AWS.Deploy.CLI/Commands/RootCommand.cs
new file mode 100644
index 000000000..f75ea2978
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/RootCommand.cs
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using AWS.Deploy.CLI.Commands.Settings;
+using AWS.Deploy.CLI.Utilities;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// Represents the root command which displays information about the tool
+///
+public class RootCommand(
+ IToolInteractiveService toolInteractiveService) : Command
+{
+ ///
+ /// Displays information about the tool
+ ///
+ /// Command context
+ /// Command settings
+ /// The command exit code
+ public override int Execute(CommandContext context, RootCommandSettings settings)
+ {
+ if (settings.Version)
+ {
+ var toolVersion = CommandLineHelpers.GetToolVersion();
+ toolInteractiveService.WriteLine($"Version: {toolVersion}");
+ }
+
+ return CommandReturnCodes.SUCCESS;
+ }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs
index 52cc2fb76..160439dff 100644
--- a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs
+++ b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs
@@ -2,131 +2,148 @@
// SPDX-License-Identifier: Apache-2.0
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+using AWS.Deploy.CLI.Commands.Settings;
using AWS.Deploy.CLI.ServerMode;
using AWS.Deploy.CLI.ServerMode.Services;
using AWS.Deploy.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
-namespace AWS.Deploy.CLI.Commands
+namespace AWS.Deploy.CLI.Commands;
+
+///
+/// Represents a Server mode command that allows communication between this CLI and the AWS Toolkit for Visual Studio.
+///
+public class ServerModeCommand(
+ IToolInteractiveService toolInteractiveService) : CancellableAsyncCommand
{
- public class ServerModeCommand
+ ///
+ /// Runs tool in server mode
+ ///
+ /// Command context
+ /// Command settings
+ /// Cancellation token source
+ /// The command exit code
+ public override async Task ExecuteAsync(CommandContext context, ServerModeCommandSettings settings, CancellationTokenSource cancellationTokenSource)
{
- private readonly IToolInteractiveService _interactiveService;
- private readonly int _port;
- private readonly int? _parentPid;
- private readonly bool _noEncryptionKeyInfo;
+ toolInteractiveService.Diagnostics = settings.Diagnostics;
- public ServerModeCommand(IToolInteractiveService interactiveService, int port, int? parentPid, bool noEncryptionKeyInfo)
- {
- _interactiveService = interactiveService;
- _port = port;
- _parentPid = parentPid;
- _noEncryptionKeyInfo = noEncryptionKeyInfo;
- }
+ toolInteractiveService.WriteLine("Server mode allows communication between this CLI and the AWS Toolkit for Visual Studio.");
- public async Task ExecuteAsync(CancellationToken cancellationToken = default(CancellationToken))
- {
- _interactiveService.WriteLine("Server mode allows communication between this CLI and the AWS Toolkit for Visual Studio.");
+ IEncryptionProvider encryptionProvider = CreateEncryptionProvider(settings.UnsecureMode);
- IEncryptionProvider encryptionProvider = CreateEncryptionProvider();
+ if (IsPortInUse(settings.Port))
+ throw new TcpPortInUseException(DeployToolErrorCode.TcpPortInUse, "The port you have selected is currently in use by another process.");
- if (IsPortInUse(_port))
- throw new TcpPortInUseException(DeployToolErrorCode.TcpPortInUse, "The port you have selected is currently in use by another process.");
+ var url = $"http://localhost:{settings.Port}";
- var url = $"http://localhost:{_port}";
-
- var builder = new WebHostBuilder()
- .UseKestrel()
- .UseUrls(url)
- .ConfigureServices(services =>
- {
- services.AddSingleton(encryptionProvider);
- })
- .UseStartup();
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .UseUrls(url)
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(encryptionProvider);
+ })
+ .UseStartup();
- var host = builder.Build();
+ var host = builder.Build();
- if (_parentPid.GetValueOrDefault() == 0)
+ if (settings.ParentPid.GetValueOrDefault() == 0)
+ {
+ await host.RunAsync(cancellationTokenSource.Token);
+ }
+ else
+ {
+ try
{
- await host.RunAsync(cancellationToken);
+ var process = Process.GetProcessById(settings.ParentPid.GetValueOrDefault());
+ process.EnableRaisingEvents = true;
+ process.Exited += async (sender, args) => { await ShutDownHost(host, cancellationTokenSource.Token); };
}
- else
+ catch (Exception exception)
{
- try
- {
- var process = Process.GetProcessById(_parentPid.GetValueOrDefault());
- process.EnableRaisingEvents = true;
- process.Exited += async (sender, args) => { await ShutDownHost(host, cancellationToken); };
- }
- catch (Exception exception)
- {
- _interactiveService.WriteDebugLine(exception.PrettyPrint());
- return;
- }
-
- await host.RunAsync(cancellationToken);
+ toolInteractiveService.WriteDebugLine(exception.PrettyPrint());
+ return CommandReturnCodes.SUCCESS;
}
+
+ await host.RunAsync(cancellationTokenSource.Token);
}
- private async Task ShutDownHost(IWebHost host, CancellationToken cancellationToken)
+ return CommandReturnCodes.SUCCESS;
+ }
+
+ ///
+ /// Shuts down the web host
+ ///
+ /// Web host
+ /// Cancellation token
+ private async Task ShutDownHost(IWebHost host, CancellationToken cancellationToken)
+ {
+ toolInteractiveService.WriteLine(string.Empty);
+ toolInteractiveService.WriteLine("The parent process is no longer running.");
+ toolInteractiveService.WriteLine("Server mode is shutting down...");
+ await host.StopAsync(cancellationToken);
+ }
+
+ ///
+ /// Creates encryption provider
+ ///
+ /// Indicates that no encryption key info will be provided
+ /// Encryption provider
+ private IEncryptionProvider CreateEncryptionProvider(bool noEncryptionKeyInfo)
+ {
+ IEncryptionProvider encryptionProvider;
+ if (noEncryptionKeyInfo)
{
- _interactiveService.WriteLine(string.Empty);
- _interactiveService.WriteLine("The parent process is no longer running.");
- _interactiveService.WriteLine("Server mode is shutting down...");
- await host.StopAsync(cancellationToken);
+ encryptionProvider = new NoEncryptionProvider();
}
-
- private IEncryptionProvider CreateEncryptionProvider()
+ else
{
- IEncryptionProvider encryptionProvider;
- if (_noEncryptionKeyInfo)
- {
- encryptionProvider = new NoEncryptionProvider();
- }
- else
+ toolInteractiveService.WriteLine("Waiting on symmetric key from stdin");
+ var input = toolInteractiveService.ReadLine();
+ var keyInfo = EncryptionKeyInfo.ParseStdInKeyInfo(input);
+
+ switch(keyInfo.Version)
{
- _interactiveService.WriteLine("Waiting on symmetric key from stdin");
- var input = _interactiveService.ReadLine();
- var keyInfo = EncryptionKeyInfo.ParseStdInKeyInfo(input);
-
- switch(keyInfo.Version)
- {
- case EncryptionKeyInfo.VERSION_1_0:
- var aes = Aes.Create();
-
- if (keyInfo.Key != null)
- {
- aes.Key = Convert.FromBase64String(keyInfo.Key);
- }
-
- encryptionProvider = new AesEncryptionProvider(aes);
- break;
- case null:
- throw new InvalidEncryptionKeyInfoException("Missing required \"Version\" property in the symmetric key");
- default:
- throw new InvalidEncryptionKeyInfoException($"Unsupported symmetric key {keyInfo.Version}");
- }
-
- _interactiveService.WriteLine("Encryption provider enabled");
+ case EncryptionKeyInfo.VERSION_1_0:
+ var aes = Aes.Create();
+
+ if (keyInfo.Key != null)
+ {
+ aes.Key = Convert.FromBase64String(keyInfo.Key);
+ }
+
+ encryptionProvider = new AesEncryptionProvider(aes);
+ break;
+ case null:
+ throw new InvalidEncryptionKeyInfoException("Missing required \"Version\" property in the symmetric key");
+ default:
+ throw new InvalidEncryptionKeyInfoException($"Unsupported symmetric key {keyInfo.Version}");
}
- return encryptionProvider;
+ toolInteractiveService.WriteLine("Encryption provider enabled");
}
- private bool IsPortInUse(int port)
- {
- var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
- var listeners = ipGlobalProperties.GetActiveTcpListeners();
+ return encryptionProvider;
+ }
- return listeners.Any(x => x.Port == port);
- }
+ ///
+ /// Checks if a port is in use
+ ///
+ /// Tcp port
+ /// true, if port is in use. false if not.
+ private bool IsPortInUse(int port)
+ {
+ var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
+ var listeners = ipGlobalProperties.GetActiveTcpListeners();
+
+ return listeners.Any(x => x.Port == port);
}
}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/DeleteDeploymentCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/DeleteDeploymentCommandSettings.cs
new file mode 100644
index 000000000..934503239
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/DeleteDeploymentCommandSettings.cs
@@ -0,0 +1,56 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using System.IO;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public class DeleteDeploymentCommandSettings : CommandSettings
+{
+ ///
+ /// AWS credential profile used to make calls to AWS
+ ///
+ [CommandOption("--profile")]
+ [Description("AWS credential profile used to make calls to AWS.")]
+ public string? Profile { get; set; }
+
+ ///
+ /// AWS region to deploy the application to
+ ///
+ [CommandOption("--region")]
+ [Description("AWS region to deploy the application to. For example, us-west-2.")]
+ public string? Region { get; set; }
+
+ ///
+ /// Path to the project to deploy
+ ///
+ [CommandOption("--project-path")]
+ [Description("Path to the project to deploy.")]
+ public string ProjectPath { get; set; } = Directory.GetCurrentDirectory();
+
+ ///
+ /// Enable diagnostic output
+ ///
+ [CommandOption("-d|--diagnostics")]
+ [Description("Enable diagnostic output.")]
+ public bool Diagnostics { get; set; }
+
+ ///
+ /// Disable interactivity to execute commands without any prompts for user input
+ ///
+ [CommandOption("-s|--silent")]
+ [Description("Disable interactivity to execute commands without any prompts for user input.")]
+ public bool Silent { get; set; }
+
+ ///
+ /// The name of the deployment to be deleted
+ ///
+ [CommandArgument(0, "")]
+ [Description("The name of the deployment to be deleted.")]
+ public required string DeploymentName { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/DeployCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/DeployCommandSettings.cs
new file mode 100644
index 000000000..55a6bd9be
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/DeployCommandSettings.cs
@@ -0,0 +1,86 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using System.IO;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public class DeployCommandSettings : CommandSettings
+{
+ ///
+ /// AWS credential profile used to make calls to AWS
+ ///
+ [CommandOption("--profile")]
+ [Description("AWS credential profile used to make calls to AWS.")]
+ public string? Profile { get; set; }
+
+ ///
+ /// AWS region to deploy the application to
+ ///
+ [CommandOption("--region")]
+ [Description("AWS region to deploy the application to. For example, us-west-2.")]
+ public string? Region { get; set; }
+
+ ///
+ /// Path to the project to deploy
+ ///
+ [CommandOption("--project-path")]
+ [Description("Path to the project to deploy.")]
+ public string ProjectPath { get; set; } = Directory.GetCurrentDirectory();
+
+ ///
+ /// Name of the cloud application
+ ///
+ [CommandOption("--application-name")]
+ [Description("Name of the cloud application. If you choose to deploy via CloudFormation, this name will be used to identify the CloudFormation stack.")]
+ public string? ApplicationName { get; set; }
+
+ ///
+ /// Path to the deployment settings file to be applied
+ ///
+ [CommandOption("--apply")]
+ [Description("Path to the deployment settings file to be applied.")]
+ public string? Apply { get; set; }
+
+ ///
+ /// Enable diagnostic output
+ ///
+ [CommandOption("-d|--diagnostics")]
+ [Description("Enable diagnostic output.")]
+ public bool Diagnostics { get; set; }
+
+ ///
+ /// Disable interactivity to execute commands without any prompts for user input
+ ///
+ [CommandOption("-s|--silent")]
+ [Description("Disable interactivity to execute commands without any prompts for user input.")]
+ public bool Silent { get; set; }
+
+ ///
+ /// The absolute or relative path of the CDK project that will be used for deployment
+ ///
+ [CommandOption("--deployment-project")]
+ [Description("The absolute or relative path of the CDK project that will be used for deployment.")]
+ public string? DeploymentProject { get; set; }
+
+ ///
+ /// The absolute or the relative JSON file path where the deployment settings will be saved.
+ /// Only the settings modified by the user will be persisted.
+ ///
+ [CommandOption("--save-settings")]
+ [Description("The absolute or the relative JSON file path where the deployment settings will be saved. Only the settings modified by the user will be persisted.")]
+ public string? SaveSettings { get; set; }
+
+ ///
+ /// The absolute or the relative JSON file path where the deployment settings will be saved.
+ /// All deployment settings will be persisted.
+ ///
+ [CommandOption("--save-all-settings")]
+ [Description("The absolute or the relative JSON file path where the deployment settings will be saved. All deployment settings will be persisted.")]
+ public string? SaveAllSettings { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/GenerateDeploymentProjectCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/GenerateDeploymentProjectCommandSettings.cs
new file mode 100644
index 000000000..537eba155
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/GenerateDeploymentProjectCommandSettings.cs
@@ -0,0 +1,42 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using System.IO;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public class GenerateDeploymentProjectCommandSettings : CommandSettings
+{
+ ///
+ /// Directory path in which the CDK deployment project will be saved
+ ///
+ [CommandOption("-o|--output")]
+ [Description("Directory path in which the CDK deployment project will be saved.")]
+ public string? Output { get; set; }
+
+ ///
+ /// Enable diagnostic output
+ ///
+ [CommandOption("-d|--diagnostics")]
+ [Description("Enable diagnostic output.")]
+ public bool Diagnostics { get; set; }
+
+ ///
+ /// Path to the project to deploy
+ ///
+ [CommandOption("--project-path")]
+ [Description("Path to the project to deploy.")]
+ public string ProjectPath { get; set; } = Directory.GetCurrentDirectory();
+
+ ///
+ /// The name of the deployment project that will be displayed in the list of available deployment options
+ ///
+ [CommandOption("--project-display-name")]
+ [Description("The name of the deployment project that will be displayed in the list of available deployment options.")]
+ public string? ProjectDisplayName { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/ListDeploymentsCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/ListDeploymentsCommandSettings.cs
new file mode 100644
index 000000000..7b1633ed0
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/ListDeploymentsCommandSettings.cs
@@ -0,0 +1,34 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public class ListDeploymentsCommandSettings : CommandSettings
+{
+ ///
+ /// AWS credential profile used to make calls to AWS
+ ///
+ [CommandOption("--profile")]
+ [Description("AWS credential profile used to make calls to AWS.")]
+ public string? Profile { get; set; }
+
+ ///
+ /// AWS region to deploy the application to
+ ///
+ [CommandOption("--region")]
+ [Description("AWS region to deploy the application to. For example, us-west-2.")]
+ public string? Region { get; set; }
+
+ ///
+ /// Enable diagnostic output
+ ///
+ [CommandOption("-d|--diagnostics")]
+ [Description("Enable diagnostic output.")]
+ public bool Diagnostics { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/RootCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/RootCommandSettings.cs
new file mode 100644
index 000000000..e1c6b4525
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/RootCommandSettings.cs
@@ -0,0 +1,20 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public sealed class RootCommandSettings : CommandSettings
+{
+ ///
+ /// Show help and usage information
+ ///
+ [CommandOption("-v|--version")]
+ [Description("Show help and usage information")]
+ public bool Version { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Commands/Settings/ServerModeCommandSettings.cs b/src/AWS.Deploy.CLI/Commands/Settings/ServerModeCommandSettings.cs
new file mode 100644
index 000000000..482c9d8af
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Commands/Settings/ServerModeCommandSettings.cs
@@ -0,0 +1,41 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Commands.Settings;
+
+///
+/// Represents the settings for configuring the .
+///
+public class ServerModeCommandSettings : CommandSettings
+{
+ ///
+ /// Port the server mode will listen to
+ ///
+ [CommandOption("--port")]
+ [Description("Port the server mode will listen to.")]
+ public required int Port { get; set; }
+
+ ///
+ /// The ID of the process that is launching server mode
+ ///
+ [CommandOption("--parent-pid")]
+ [Description("The ID of the process that is launching server mode. Server mode will exit when the parent pid terminates.")]
+ public int? ParentPid { get; set; }
+
+ ///
+ /// If set the cli uses an unsecure mode without encryption
+ ///
+ [CommandOption("--unsecure-mode")]
+ [Description("If set the cli uses an unsecure mode without encryption.")]
+ public bool UnsecureMode { get; set; }
+
+ ///
+ /// Enable diagnostic output
+ ///
+ [CommandOption("-d|--diagnostics")]
+ [Description("Enable diagnostic output.")]
+ public bool Diagnostics { get; set; }
+}
diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs
index f19a7bea3..8f9e01c42 100644
--- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs
+++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs
@@ -1,7 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-using AWS.Deploy.CLI.Commands;
using AWS.Deploy.CLI.Commands.TypeHints;
using AWS.Deploy.CLI.Utilities;
using AWS.Deploy.Common;
@@ -20,6 +19,7 @@
using AWS.Deploy.Orchestration.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Spectre.Console.Cli;
namespace AWS.Deploy.CLI.Extensions
{
@@ -60,7 +60,6 @@ public static void AddCustomServices(this IServiceCollection serviceCollection,
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IZipFileManager), typeof(ZipFileManager), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentManifestEngine), typeof(DeploymentManifestEngine), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(ILocalUserSettingsEngine), typeof(LocalUserSettingsEngine), lifetime));
- serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKVersionDetector), typeof(CDKVersionDetector), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IAWSServiceHandler), typeof(AWSServiceHandler), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IS3Handler), typeof(AWSS3Handler), lifetime));
@@ -77,9 +76,6 @@ public static void AddCustomServices(this IServiceCollection serviceCollection,
var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier);
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IPackageJsonGenerator), (serviceProvider) => new PackageJsonGenerator(packageJsonTemplate), lifetime));
-
- // required to run the application
- serviceCollection.AddSingleton();
}
}
}
diff --git a/src/AWS.Deploy.CLI/Program.cs b/src/AWS.Deploy.CLI/Program.cs
index 5f1cbb553..d686c5156 100644
--- a/src/AWS.Deploy.CLI/Program.cs
+++ b/src/AWS.Deploy.CLI/Program.cs
@@ -2,35 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
using System;
-using System.Linq;
-using System.Reflection;
using System.Text;
-using System.Threading.Tasks;
+using AWS.Deploy.CLI;
using AWS.Deploy.CLI.Extensions;
-using AWS.Deploy.Common;
+using AWS.Deploy.CLI.Utilities;
using Microsoft.Extensions.DependencyInjection;
-namespace AWS.Deploy.CLI
-{
- internal class Program
- {
- private static async Task Main(string[] args)
- {
- var serviceCollection = new ServiceCollection();
-
- serviceCollection.AddCustomServices();
-
- var serviceProvider = serviceCollection.BuildServiceProvider();
-
- // calls the Run method in App, which is replacing Main
- var app = serviceProvider.GetService();
- if (app == null)
- {
- throw new Exception("App dependencies aren't injected correctly." +
- " Verify CustomServiceCollectionExtension has all the required dependencies to instantiate App.");
- }
-
- return await app.Run(args);
- }
- }
-}
+Console.OutputEncoding = Encoding.UTF8;
+
+CommandLineHelpers.SetExecutionEnvironment(args);
+
+var serviceCollection = new ServiceCollection();
+
+serviceCollection.AddCustomServices();
+
+var registrar = new TypeRegistrar(serviceCollection);
+
+var app = App.ConfigureServices(registrar);
+
+return await App.RunAsync(args, app, registrar);
diff --git a/src/AWS.Deploy.CLI/Utilities/CommandLineHelpers.cs b/src/AWS.Deploy.CLI/Utilities/CommandLineHelpers.cs
new file mode 100644
index 000000000..e6f286bf3
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Utilities/CommandLineHelpers.cs
@@ -0,0 +1,70 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Reflection;
+using System.Text;
+
+namespace AWS.Deploy.CLI.Utilities;
+
+///
+/// A helper class for utility methods.
+///
+public static class CommandLineHelpers
+{
+ ///
+ /// Set up the execution environment variable picked up by the AWS .NET SDK. This can be useful for identify calls
+ /// made by this tool in AWS CloudTrail.
+ ///
+ internal static void SetExecutionEnvironment(string[] args)
+ {
+ const string envName = "AWS_EXECUTION_ENV";
+
+ var toolVersion = GetToolVersion();
+
+ // The leading and trailing whitespaces are intentional
+ var userAgent = $" lib/aws-dotnet-deploy-cli#{toolVersion} ";
+ if (args?.Length > 0)
+ {
+ // The trailing whitespace is intentional
+ userAgent = $"{userAgent}md/cli-args#{args[0]} ";
+ }
+
+
+ var envValue = new StringBuilder();
+ var existingValue = Environment.GetEnvironmentVariable(envName);
+
+ // If there is an existing execution environment variable add this tool as a suffix.
+ if (!string.IsNullOrEmpty(existingValue))
+ {
+ envValue.Append(existingValue);
+ }
+
+ envValue.Append(userAgent);
+
+ Environment.SetEnvironmentVariable(envName, envValue.ToString());
+ }
+
+ ///
+ /// Retrieve the deploy tool version
+ ///
+ internal static string GetToolVersion()
+ {
+ var assembly = typeof(App).GetTypeInfo().Assembly;
+ var version = assembly.GetCustomAttribute()?.Version;
+ if (version is null)
+ {
+ return string.Empty;
+ }
+
+ var versionParts = version.Split('.');
+ if (versionParts.Length == 4)
+ {
+ // The revision part of the version number is intentionally set to 0 since package versioning on
+ // NuGet follows semantic versioning consisting only of Major.Minor.Patch versions.
+ versionParts[3] = "0";
+ }
+
+ return string.Join(".", versionParts);
+ }
+}
diff --git a/src/AWS.Deploy.CLI/Utilities/TypeRegistrar.cs b/src/AWS.Deploy.CLI/Utilities/TypeRegistrar.cs
new file mode 100644
index 000000000..3a3578ad3
--- /dev/null
+++ b/src/AWS.Deploy.CLI/Utilities/TypeRegistrar.cs
@@ -0,0 +1,62 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli;
+
+namespace AWS.Deploy.CLI.Utilities;
+
+///
+/// Provides functionality to register types and instances with an for dependency injection.
+///
+public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar
+{
+ private IServiceProvider? _provider;
+
+ ///
+ /// Event fired after the service provider is built.
+ ///
+ public event Action? ServiceProviderBuilt;
+
+ ///
+ public ITypeResolver Build()
+ {
+ if (_provider == null)
+ {
+ _provider = services.BuildServiceProvider();
+ ServiceProviderBuilt?.Invoke(_provider);
+ }
+
+ return new TypeResolver(_provider);
+ }
+
+ ///
+ public void Register(Type service, Type implementation)
+ {
+ services.AddSingleton(service, implementation);
+ _provider = null;
+ }
+
+ ///
+ public void RegisterInstance(Type service, object implementation)
+ {
+ services.AddSingleton(service, implementation);
+ _provider = null;
+ }
+
+ ///
+ public void RegisterLazy(Type service, Func