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 func) + { + services.AddSingleton(service, _ => func()); + _provider = null; + } + + /// + /// Retrieve the + /// + public IServiceProvider GetServiceProvider() + { + return _provider ??= services.BuildServiceProvider(); + } +} diff --git a/src/AWS.Deploy.CLI/Utilities/TypeResolver.cs b/src/AWS.Deploy.CLI/Utilities/TypeResolver.cs new file mode 100644 index 000000000..ec77142ce --- /dev/null +++ b/src/AWS.Deploy.CLI/Utilities/TypeResolver.cs @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System; +using Spectre.Console.Cli; + +namespace AWS.Deploy.CLI.Utilities; + +/// +/// Provides functionality to resolve types from an and manages the disposal of the provider if required. +/// +public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver, IDisposable +{ + private readonly IServiceProvider _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + + /// + public object? Resolve(Type? type) + { + if (type == null) + { + return null; + } + + return _provider.GetService(type); + } + + /// + public void Dispose() + { + if (_provider is IDisposable disposable) + { + disposable.Dispose(); + } + } +} diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index ee8b2f704..065cf8dfe 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -22,5 +22,7 @@ public static class CLI public const string CLI_APP_NAME = "AWS .NET Deployment Tool"; public const string WORKSPACE_ENV_VARIABLE = "AWS_DOTNET_DEPLOYTOOL_WORKSPACE"; + + public const string TOOL_NAME = "dotnet-aws"; } } diff --git a/src/AWS.Deploy.DocGenerator/Program.cs b/src/AWS.Deploy.DocGenerator/Program.cs index 06881a377..b5af31785 100644 --- a/src/AWS.Deploy.DocGenerator/Program.cs +++ b/src/AWS.Deploy.DocGenerator/Program.cs @@ -7,6 +7,7 @@ using System.Threading; using AWS.Deploy.ServerMode.Client; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.ServerMode.Client.Utilities; namespace AWS.Deploy.DocGenerator @@ -24,9 +25,14 @@ public static async Task Main(string[] args) { var interactiveService = serviceProvider.GetRequiredService(); var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(interactiveService, 4152, null, true); - - _ = serverCommand.ExecuteAsync(new CancellationTokenSource().Token); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = 4152, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(interactiveService); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, new CancellationTokenSource()); var baseUrl = $"http://localhost:{4152}/"; diff --git a/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs b/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs index d6b967b7b..bc2ccab6f 100644 --- a/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs +++ b/src/AWS.Deploy.ServerMode.ClientGenerator/Program.cs @@ -6,67 +6,65 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using AWS.Deploy.CLI.Commands.Settings; -namespace AWS.Deploy.ServerMode.ClientGenerator +// Start up the server mode to make the swagger.json file available. +var portNumber = 5678; +var serverCommandSettings = new ServerModeCommandSettings { - class Program - { - static async Task Main(string[] args) - { - // Start up the server mode to make the swagger.json file available. - var portNumber = 5678; - var serverCommand = new ServerModeCommand(new ConsoleInteractiveServiceImpl(), portNumber, null, true); - var cancelSource = new CancellationTokenSource(); - _ = serverCommand.ExecuteAsync(cancelSource.Token); - try - { - // Wait till server mode is started. - await Task.Delay(3000); + Port = portNumber, + ParentPid = null, + UnsecureMode = true +}; +var serverCommand = new ServerModeCommand(new ConsoleInteractiveServiceImpl()); +var cancelSource = new CancellationTokenSource(); +_ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); +try +{ + // Wait till server mode is started. + await Task.Delay(3000); - // Grab the swagger.json from the running instances of server mode - var document = await OpenApiDocument.FromUrlAsync($"http://localhost:{portNumber}/swagger/v1/swagger.json"); + // Grab the swagger.json from the running instances of server mode + var document = await OpenApiDocument.FromUrlAsync($"http://localhost:{portNumber}/swagger/v1/swagger.json"); - var settings = new CSharpClientGeneratorSettings - { - ClassName = "RestAPIClient", - GenerateClientInterfaces = true, - CSharpGeneratorSettings = - { - Namespace = "AWS.Deploy.ServerMode.Client", - }, - HttpClientType = "ServerModeHttpClient" - }; + var settings = new CSharpClientGeneratorSettings + { + ClassName = "RestAPIClient", + GenerateClientInterfaces = true, + CSharpGeneratorSettings = + { + Namespace = "AWS.Deploy.ServerMode.Client", + }, + HttpClientType = "ServerModeHttpClient" + }; - var generator = new CSharpClientGenerator(document, settings); - var code = generator.GenerateFile(); + var generator = new CSharpClientGenerator(document, settings); + var code = generator.GenerateFile(); - // Save the generated client to the AWS.Deploy.ServerMode.Client project - var fullPath = DetermineFullFilePath("RestAPI.cs"); - File.WriteAllText(fullPath, code); - } - finally - { - // terminate running server mode. - cancelSource.Cancel(); - } - } + // Save the generated client to the AWS.Deploy.ServerMode.Client project + var fullPath = DetermineFullFilePath("RestAPI.cs"); + File.WriteAllText(fullPath, code); +} +finally +{ + // terminate running server mode. + cancelSource.Cancel(); +} - static string DetermineFullFilePath(string codeFile) - { - var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); +static string DetermineFullFilePath(string codeFile) +{ + var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); - while (!string.Equals(dir?.Name, "src")) - { - if (dir == null) - break; + while (!string.Equals(dir?.Name, "src")) + { + if (dir == null) + break; - dir = dir.Parent; - } + dir = dir.Parent; + } - if (dir == null) - throw new Exception("Could not determine file path of current directory."); + if (dir == null) + throw new Exception("Could not determine file path of current directory."); - return Path.Combine(dir.FullName, "AWS.Deploy.ServerMode.Client", codeFile); - } - } + return Path.Combine(dir.FullName, "AWS.Deploy.ServerMode.Client", codeFile); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs index 75947133b..28d8cf818 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/CLITests.cs @@ -6,60 +6,65 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.CLI.IntegrationTests.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Services; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests { [Collection(nameof(TestContextFixture))] - public class CLITests + public class CLITests(TestContextFixture fixture) { - private readonly TestContextFixture _fixture; - - public CLITests(TestContextFixture fixture) - { - _fixture = fixture; - } - [Fact] public async Task DeployToExistingBeanstalkEnvironment() { - var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _fixture.App.Run(deployArgs)); + var projectPath = fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; + InMemoryInteractiveService interactiveService = null; + Assert.Equal(CommandReturnCodes.SUCCESS, await fixture.ServiceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); - var environmentDescription = await _fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(_fixture.EnvironmentName); + var environmentDescription = await fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(fixture.EnvironmentName); // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + await fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; - var deployStdOutput = _fixture.InteractiveService.StdOutReader.ReadAllLines(); + var successMessagePrefix = $"The Elastic Beanstalk Environment {fixture.EnvironmentName} has been successfully updated"; + var deployStdOutput = interactiveService.StdOutReader.ReadAllLines(); var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); Assert.False(string.IsNullOrEmpty(successMessage)); var expectedVersionLabel = successMessage.Split(" ").Last(); - Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + Assert.True(await fixture.EBHelper.VerifyEnvironmentVersionLabel(fixture.EnvironmentName, expectedVersionLabel)); } [Fact] public async Task DeployToExistingBeanstalkEnvironmentSelfContained() { - var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2", "--apply", "Existing-ElasticBeanStalkConfigFile-Linux-SelfContained.json" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _fixture.App.Run(deployArgs)); + var projectPath = fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2", "--apply", "Existing-ElasticBeanStalkConfigFile-Linux-SelfContained.json" }; + InMemoryInteractiveService interactiveService = null; + Assert.Equal(CommandReturnCodes.SUCCESS, await fixture.ServiceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); - var environmentDescription = await _fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(_fixture.EnvironmentName); + var environmentDescription = await fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(fixture.EnvironmentName); // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + await fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; - var deployStdOutput = _fixture.InteractiveService.StdOutReader.ReadAllLines(); + var successMessagePrefix = $"The Elastic Beanstalk Environment {fixture.EnvironmentName} has been successfully updated"; + var deployStdOutput = interactiveService.StdOutReader.ReadAllLines(); var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); Assert.False(string.IsNullOrEmpty(successMessage)); var expectedVersionLabel = successMessage.Split(" ").Last(); - Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + Assert.True(await fixture.EBHelper.VerifyEnvironmentVersionLabel(fixture.EnvironmentName, expectedVersionLabel)); } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs index 62edfef86..d21805a47 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs @@ -6,62 +6,39 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.CLI.IntegrationTests.Extensions; -using AWS.Deploy.Common; -using AWS.Deploy.Common.IO; -using AWS.Deploy.Common.Recipes; -using AWS.Deploy.Orchestration.ServiceHandlers; -using Moq; +using AWS.Deploy.CLI.IntegrationTests.Services; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment { [Collection(nameof(WindowsTestContextFixture))] - public class CLITests + public class CLITests(WindowsTestContextFixture fixture) { - private readonly WindowsTestContextFixture _fixture; - private readonly AWSElasticBeanstalkHandler _awsElasticBeanstalkHandler; - private readonly Mock _optionSettingHandler; - private readonly ProjectDefinitionParser _projectDefinitionParser; - private readonly RecipeDefinition _recipeDefinition; - - public CLITests(WindowsTestContextFixture fixture) - { - _projectDefinitionParser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); - _fixture = fixture; - _optionSettingHandler = new Mock(); - _awsElasticBeanstalkHandler = new AWSElasticBeanstalkHandler(null, null, null, _optionSettingHandler.Object); - _recipeDefinition = new Mock( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()).Object; - } - [Fact] public async Task DeployToExistingBeanstalkEnvironment() { - var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _fixture.App.Run(deployArgs)); + var projectPath = fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; + InMemoryInteractiveService interactiveService = null; + Assert.Equal(CommandReturnCodes.SUCCESS, await fixture.ServiceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); - var environmentDescription = await _fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(_fixture.EnvironmentName); + var environmentDescription = await fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(fixture.EnvironmentName); // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + await fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; - var deployStdOutput = _fixture.InteractiveService.StdOutReader.ReadAllLines(); + var successMessagePrefix = $"The Elastic Beanstalk Environment {fixture.EnvironmentName} has been successfully updated"; + var deployStdOutput = interactiveService.StdOutReader.ReadAllLines(); var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); Assert.False(string.IsNullOrEmpty(successMessage)); var expectedVersionLabel = successMessage.Split(" ").Last(); - Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + Assert.True(await fixture.EBHelper.VerifyEnvironmentVersionLabel(fixture.EnvironmentName, expectedVersionLabel)); } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs index d215dce83..4c7e9ef96 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs @@ -7,8 +7,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Amazon.Runtime; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.IntegrationTests.Utilities; using AWS.Deploy.ServerMode.Client; using AWS.Deploy.ServerMode.Client.Utilities; @@ -17,27 +17,27 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment { [Collection(nameof(WindowsTestContextFixture))] - public class ServerModeTests + public class ServerModeTests(WindowsTestContextFixture fixture) { - private readonly WindowsTestContextFixture _fixture; private const string BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkWindowsEnvironment"; - public ServerModeTests(WindowsTestContextFixture fixture) - { - _fixture = fixture; - } - [Fact] public async Task DeployToExistingWindowsBeanstalkEnvironment() { - var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var projectPath = fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4031; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_fixture.ToolInteractiveService, portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(fixture.ToolInteractiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + var serverTask = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -55,13 +55,13 @@ public async Task DeployToExistingWindowsBeanstalkEnvironment() Assert.NotNull(sessionId); var existingDeployments = await restClient.GetExistingDeploymentsAsync(sessionId); - var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(_fixture.EnvironmentName, x.Name)); + var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(fixture.EnvironmentName, x.Name)); - Assert.Equal(_fixture.EnvironmentName, existingDeployment.Name); + Assert.Equal(fixture.EnvironmentName, existingDeployment.Name); Assert.Equal(BEANSTALK_ENVIRONMENT_RECIPE_ID, existingDeployment.RecipeId); Assert.Null(existingDeployment.BaseRecipeId); Assert.False(existingDeployment.IsPersistedDeploymentProject); - Assert.Equal(_fixture.EnvironmentId, existingDeployment.ExistingDeploymentId); + Assert.Equal(fixture.EnvironmentId, existingDeployment.ExistingDeploymentId); Assert.Equal(DeploymentTypes.BeanstalkEnvironment, existingDeployment.DeploymentType); var signalRClient = new DeploymentCommunicationClient(baseUrl); @@ -72,7 +72,7 @@ public async Task DeployToExistingWindowsBeanstalkEnvironment() await restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput { - ExistingDeploymentId = _fixture.EnvironmentId + ExistingDeploymentId = fixture.EnvironmentId }); await restClient.StartDeploymentAsync(sessionId); @@ -80,13 +80,13 @@ public async Task DeployToExistingWindowsBeanstalkEnvironment() await restClient.WaitForDeployment(sessionId); Assert.True(logOutput.Length > 0); - var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; + var successMessagePrefix = $"The Elastic Beanstalk Environment {fixture.EnvironmentName} has been successfully updated"; var deployStdOutput = logOutput.ToString().Split(Environment.NewLine); var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); Assert.False(string.IsNullOrEmpty(successMessage)); var expectedVersionLabel = successMessage.Split(" ").Last(); - Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + Assert.True(await fixture.EBHelper.VerifyEnvironmentVersionLabel(fixture.EnvironmentName, expectedVersionLabel)); } finally { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs index 905f43765..def4805ec 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs @@ -30,7 +30,6 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.E /// public class WindowsTestContextFixture : IAsyncLifetime { - public readonly App App; public readonly HttpHelper HttpHelper; public readonly IAWSResourceQueryer AWSResourceQueryer; public readonly TestAppManager TestAppManager; @@ -42,6 +41,7 @@ public class WindowsTestContextFixture : IAsyncLifetime public readonly InMemoryInteractiveService InteractiveService; public readonly ElasticBeanstalkHelper EBHelper; public readonly IAMHelper IAMHelper; + public readonly IServiceCollection ServiceCollection; public readonly string ApplicationName; public readonly string EnvironmentName; @@ -51,22 +51,18 @@ public class WindowsTestContextFixture : IAsyncLifetime public WindowsTestContextFixture() { - var serviceCollection = new ServiceCollection(); + ServiceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); + ServiceCollection.AddCustomServices(); + ServiceCollection.AddTestServices(); + var serviceProvider = ServiceCollection.BuildServiceProvider(); var awsClientFactory = serviceProvider.GetService(); awsClientFactory.ConfigureAWSOptions((options) => { options.Region = Amazon.RegionEndpoint.USWest2; }); - App = serviceProvider.GetService(); - Assert.NotNull(App); - InteractiveService = serviceProvider.GetService(); Assert.NotNull(InteractiveService); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs index 4bd214164..d469cab1d 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs @@ -7,10 +7,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Amazon.Runtime; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.IntegrationTests.Utilities; -using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; using AWS.Deploy.ServerMode.Client.Utilities; using Xunit; @@ -18,27 +17,27 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests { [Collection(nameof(TestContextFixture))] - public class ServerModeTests + public class ServerModeTests(TestContextFixture fixture) { - private readonly TestContextFixture _fixture; private const string BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment"; - public ServerModeTests(TestContextFixture fixture) - { - _fixture = fixture; - } - [Fact] public async Task DeployToExistingBeanstalkEnvironment() { - var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var projectPath = fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4031; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_fixture.ToolInteractiveService, portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(fixture.ToolInteractiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + var serverTask = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -56,13 +55,13 @@ public async Task DeployToExistingBeanstalkEnvironment() Assert.NotNull(sessionId); var existingDeployments = await restClient.GetExistingDeploymentsAsync(sessionId); - var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(_fixture.EnvironmentName, x.Name)); + var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(fixture.EnvironmentName, x.Name)); - Assert.Equal(_fixture.EnvironmentName, existingDeployment.Name); + Assert.Equal(fixture.EnvironmentName, existingDeployment.Name); Assert.Equal(BEANSTALK_ENVIRONMENT_RECIPE_ID, existingDeployment.RecipeId); Assert.Null(existingDeployment.BaseRecipeId); Assert.False(existingDeployment.IsPersistedDeploymentProject); - Assert.Equal(_fixture.EnvironmentId, existingDeployment.ExistingDeploymentId); + Assert.Equal(fixture.EnvironmentId, existingDeployment.ExistingDeploymentId); Assert.Equal(DeploymentTypes.BeanstalkEnvironment, existingDeployment.DeploymentType); var signalRClient = new DeploymentCommunicationClient(baseUrl); @@ -73,7 +72,7 @@ public async Task DeployToExistingBeanstalkEnvironment() await restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput { - ExistingDeploymentId = _fixture.EnvironmentId + ExistingDeploymentId = fixture.EnvironmentId }); await restClient.StartDeploymentAsync(sessionId); @@ -81,13 +80,13 @@ public async Task DeployToExistingBeanstalkEnvironment() await restClient.WaitForDeployment(sessionId); Assert.True(logOutput.Length > 0); - var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; + var successMessagePrefix = $"The Elastic Beanstalk Environment {fixture.EnvironmentName} has been successfully updated"; var deployStdOutput = logOutput.ToString().Split(Environment.NewLine); var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); Assert.False(string.IsNullOrEmpty(successMessage)); var expectedVersionLabel = successMessage.Split(" ").Last(); - Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + Assert.True(await fixture.EBHelper.VerifyEnvironmentVersionLabel(fixture.EnvironmentName, expectedVersionLabel)); } finally { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs index 60354ab6c..d2d12f178 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs @@ -15,7 +15,6 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; -using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -28,7 +27,6 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests /// public class TestContextFixture : IAsyncLifetime { - public readonly App App; public readonly HttpHelper HttpHelper; public readonly IAWSResourceQueryer AWSResourceQueryer; public readonly TestAppManager TestAppManager; @@ -39,6 +37,7 @@ public class TestContextFixture : IAsyncLifetime public readonly InMemoryInteractiveService InteractiveService; public readonly ElasticBeanstalkHelper EBHelper; public readonly IAMHelper IAMHelper; + public readonly IServiceCollection ServiceCollection; public readonly string ApplicationName; public readonly string EnvironmentName; @@ -48,22 +47,18 @@ public class TestContextFixture : IAsyncLifetime public TestContextFixture() { - var serviceCollection = new ServiceCollection(); + ServiceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); + ServiceCollection.AddCustomServices(); + ServiceCollection.AddTestServices(); + var serviceProvider = ServiceCollection.BuildServiceProvider(); var awsClientFactory = serviceProvider.GetService(); awsClientFactory.ConfigureAWSOptions((options) => { options.Region = Amazon.RegionEndpoint.USWest2; }); - App = serviceProvider.GetService(); - Assert.NotNull(App); - InteractiveService = serviceProvider.GetService(); Assert.NotNull(InteractiveService); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs index 8e0fcc620..53fea9b7c 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BlazorWasmTests.cs @@ -13,37 +13,24 @@ using AWS.Deploy.CLI.IntegrationTests.Services; using Microsoft.Extensions.DependencyInjection; using Xunit; -using static System.Net.WebRequestMethods; namespace AWS.Deploy.CLI.IntegrationTests { public class BlazorWasmTests : IDisposable { - private readonly HttpHelper _httpHelper; + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; private readonly CloudFrontHelper _cloudFrontHelper; - private readonly InMemoryInteractiveService _interactiveService; - private readonly App _app; private string _stackName; private bool _isDisposed; private readonly TestAppManager _testAppManager; public BlazorWasmTests() { - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); - - _httpHelper = new HttpHelper(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -57,70 +44,118 @@ public BlazorWasmTests() public async Task DefaultConfigurations(params string[] components) { _stackName = $"{components[1]}{Guid.NewGuid().ToString().Split('-').Last()}"; + string applicationUrl = null; + string distributionId = null; + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + // Example URL string: BlazorWasm6068e7a879d5ee.EndpointURL = http://blazorwasm6068e7a879d5ee-blazorhostc7106839-a2585dcq9xve.s3-website-us-west-2.amazonaws.com/ + applicationUrl = deployStdOut.First(line => line.Contains("https://") && line.Contains("cloudfront.net/")) + .Split("=")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // The initial state of logging should be false, the test will test enabling logging during redeployment. + distributionId = await _cloudFormationHelper.GetResourceId(_stackName, "RecipeCloudFrontDistribution2BE25932"); + var distribution = await _cloudFrontHelper.GetDistribution(distributionId); + Assert.Equal(Amazon.CloudFront.PriceClass.PriceClass_All, distribution.DistributionConfig.PriceClass); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy - var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - - // Example URL string: BlazorWasm6068e7a879d5ee.EndpointURL = http://blazorwasm6068e7a879d5ee-blazorhostc7106839-a2585dcq9xve.s3-website-us-west-2.amazonaws.com/ - var applicationUrl = deployStdOut.First(line => line.Contains("https://") && line.Contains("cloudfront.net/")) - .Split("=")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // The initial state of logging should be false, the test will test enabling logging during redeployment. - var distributionId = await _cloudFormationHelper.GetResourceId(_stackName, "RecipeCloudFrontDistribution2BE25932"); - var distribution = await _cloudFrontHelper.GetDistribution(distributionId); - Assert.Equal(Amazon.CloudFront.PriceClass.PriceClass_All, distribution.DistributionConfig.PriceClass); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); - - // Setup for redeployment turning on access logging via settings file. - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - var applyLoggingSettingsFile = Path.Combine(Directory.GetParent(_testAppManager.GetProjectPath(Path.Combine(components))).FullName, "apply-settings.json"); - deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics", "--apply", applyLoggingSettingsFile }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - distribution = await _cloudFrontHelper.GetDistribution(distributionId); - Assert.Equal(Amazon.CloudFront.PriceClass.PriceClass_100, distribution.DistributionConfig.PriceClass); - - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + var applyLoggingSettingsFile = Path.Combine(Directory.GetParent(_testAppManager.GetProjectPath(Path.Combine(components))).FullName, "apply-settings.json"); + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics", "--apply", applyLoggingSettingsFile }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Setup for redeployment turning on access logging via settings file. + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + var distribution = await _cloudFrontHelper.GetDistribution(distributionId); + Assert.Equal(Amazon.CloudFront.PriceClass.PriceClass_100, distribution.DistributionConfig.PriceClass); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -140,8 +175,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs index e5e6401d9..e7e7a1ae5 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ECSFargateDeploymentTest.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using Amazon.CloudFormation; using Amazon.ECS; -using Amazon.ECS.Model; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; @@ -21,11 +20,9 @@ namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment { public class ECSFargateDeploymentTest : IDisposable { - private readonly HttpHelper _httpHelper; + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; private readonly ECSHelper _ecsHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; private string _clusterName; @@ -36,75 +33,99 @@ public ECSFargateDeploymentTest() var ecsClient = new AmazonECSClient(); _ecsHelper = new ECSHelper(ecsClient); - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); - _httpHelper = new HttpHelper(_interactiveService); - _testAppManager = new TestAppManager(); } [Fact] public async Task PerformDeployment() { - var stackNamePlaceholder = "{StackName}"; - _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; - _clusterName = $"{_stackName}-cluster"; - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ECSFargateConfigFile.json"); - ConfigFileHelper.ApplyReplacementTokens(new Dictionary { { stackNamePlaceholder, _stackName } }, configFilePath); - - // Deploy - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var cluster = await _ecsHelper.GetCluster(_clusterName); - Assert.Equal("ACTIVE", cluster.Status); - Assert.Equal(cluster.ClusterName, _clusterName); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); - - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + InMemoryInteractiveService interactiveService = null; + try + { + var stackNamePlaceholder = "{StackName}"; + _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; + _clusterName = $"{_stackName}-cluster"; + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ECSFargateConfigFile.json"); + ConfigFileHelper.ApplyReplacementTokens(new Dictionary { { stackNamePlaceholder, _stackName } }, configFilePath); + + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var cluster = await _ecsHelper.GetCluster(_clusterName); + Assert.Equal("ACTIVE", cluster.Status); + Assert.Equal(cluster.ClusterName, _clusterName); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + interactiveService.StdInWriter.Write("y"); // Confirm delete + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -124,8 +145,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs index c796fc4c2..331911af5 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConfigFileDeployment/ElasticBeanStalkDeploymentTest.cs @@ -20,31 +20,18 @@ namespace AWS.Deploy.CLI.IntegrationTests.ConfigFileDeployment { public class ElasticBeanStalkDeploymentTest : IDisposable { - private readonly HttpHelper _httpHelper; + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; private readonly TestAppManager _testAppManager; - private readonly IServiceProvider _serviceProvider; public ElasticBeanStalkDeploymentTest() { - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - _serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = _serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = _serviceProvider.GetService(); - Assert.NotNull(_interactiveService); - - _httpHelper = new HttpHelper(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -70,49 +57,89 @@ public async Task PerformDeployment() {"ApplicationIAMRole.CreateNew", true }, {"XRayTracingSupportEnabled", true } }; - await ConfigFileHelper.CreateConfigFile(_serviceProvider, stackNamePlaceholder, "AspNetAppElasticBeanstalkLinux", optionSettings, projectPath, configFilePath, SaveSettingsType.Modified); + await ConfigFileHelper.CreateConfigFile(_serviceCollection.BuildServiceProvider(), stackNamePlaceholder, "AspNetAppElasticBeanstalkLinux", optionSettings, projectPath, configFilePath, SaveSettingsType.Modified); Assert.True(await ConfigFileHelper.VerifyConfigFileContents(expectedConfigFilePath, configFilePath)); // Deploy _stackName = $"WebAppNoDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; ConfigFileHelper.ApplyReplacementTokens(new Dictionary { {stackNamePlaceholder, _stackName } }, configFilePath); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - // Example: Endpoint: http://52.36.216.238/ - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + InMemoryInteractiveService interactiveService = null; + try + { + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--apply", configFilePath, "--silent", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + // Example: Endpoint: http://52.36.216.238/ + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - _interactiveService.ReadStdOutStartToEnd(); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + interactiveService.StdInWriter.Write("y"); // Confirm delete + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -132,8 +159,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs index dd53b1a9d..d4ccd974b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ConsoleAppTests.cs @@ -20,11 +20,10 @@ namespace AWS.Deploy.CLI.IntegrationTests { public class ConsoleAppTests : IDisposable { + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; private readonly ECSHelper _ecsHelper; private readonly CloudWatchLogsHelper _cloudWatchLogsHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; private readonly TestAppManager _testAppManager; @@ -37,18 +36,10 @@ public ConsoleAppTests() var cloudWatchLogsClient = new AmazonCloudWatchLogsClient(); _cloudWatchLogsHelper = new CloudWatchLogsHelper(cloudWatchLogsClient); - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -63,59 +54,105 @@ public async Task DefaultConfigurations(params string[] components) { _stackName = $"{components[1]}{Guid.NewGuid().ToString().Split('-').Last()}"; - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy - var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal("ACTIVE", cluster.Status); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); - // Verify CloudWatch logs - var logGroup = await _ecsHelper.GetLogGroup(_stackName); - var logMessages = await _cloudWatchLogsHelper.GetLogMessages(logGroup); - Assert.Contains("Hello World!", logMessages); + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + var cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal("ACTIVE", cluster.Status); - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; + // Verify CloudWatch logs + var logGroup = await _ecsHelper.GetLogGroup(_stackName); + var logMessages = await _cloudWatchLogsHelper.GetLogMessages(logGroup); + Assert.Contains("Hello World!", logMessages); - // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); - // Arrange input for re-deployment - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - // Perform re-deployment - deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // Perform re-deployment + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for re-deployment + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } [Theory] @@ -124,61 +161,106 @@ public async Task FargateArmDeployment(params string[] components) { _stackName = $"{components[1]}Arm{Guid.NewGuid().ToString().Split('-').Last()}"; - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteLineAsync("7"); // Select "Environment Architecture" - await _interactiveService.StdInWriter.WriteLineAsync("2"); // Select "Arm64" - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy - var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal("ACTIVE", cluster.Status); - - // Verify CloudWatch logs - var logGroup = await _ecsHelper.GetLogGroup(_stackName); - var logMessages = await _cloudWatchLogsHelper.GetLogMessages(logGroup); - Assert.Contains("Hello World!", logMessages); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); - - // Arrange input for re-deployment - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - // Perform re-deployment - deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.WriteLine("7"); // Select "Environment Architecture" + interactiveService.StdInWriter.WriteLine("2"); // Select "Arm64" + interactiveService.StdInWriter.Write(Environment.NewLine); // Confirm selection and deploy + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal("ACTIVE", cluster.Status); + + // Verify CloudWatch logs + var logGroup = await _ecsHelper.GetLogGroup(_stackName); + var logMessages = await _cloudWatchLogsHelper.GetLogMessages(logGroup); + Assert.Contains("Hello World!", logMessages); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // Perform re-deployment + var deployArgs = new[] { "deploy", "--project-path", _testAppManager.GetProjectPath(Path.Combine(components)), "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for re-deployment + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -198,8 +280,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Extensions/TestServiceCollectionExtension.cs b/test/AWS.Deploy.CLI.IntegrationTests/Extensions/TestServiceCollectionExtension.cs index 09d3b58eb..c1e08a71a 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Extensions/TestServiceCollectionExtension.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Extensions/TestServiceCollectionExtension.cs @@ -1,17 +1,11 @@ // 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 System; +using System.Threading.Tasks; using AWS.Deploy.CLI.IntegrationTests.Services; using AWS.Deploy.CLI.Utilities; -using AWS.Deploy.Common; -using AWS.Deploy.Common.Extensions; -using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration; -using AWS.Deploy.Orchestration.CDK; -using AWS.Deploy.Orchestration.Data; -using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; namespace AWS.Deploy.CLI.IntegrationTests.Extensions @@ -28,5 +22,19 @@ public static void AddTestServices(this IServiceCollection serviceCollection) serviceCollection.AddSingleton(serviceProvider => serviceProvider.GetService()); serviceCollection.AddSingleton(serviceProvider => serviceProvider.GetService()); } + + public static async Task RunDeployToolAsync(this IServiceCollection serviceCollection, + string[] args, + Action onProviderBuilt = null) + { + var registrar = new TypeRegistrar(serviceCollection); + + if (onProviderBuilt != null) + registrar.ServiceProviderBuilt += onProviderBuilt; + + var app = App.ConfigureServices(registrar); + return await App.RunAsync(args, app, registrar); + } + } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/Utilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/Utilities.cs index 35d65913a..45db6e68e 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/Utilities.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/Utilities.cs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Collections.Generic; using System.IO; -using System.Text; using AWS.Deploy.Orchestration.Utilities; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace AWS.Deploy.CLI.IntegrationTests.Helpers { @@ -17,7 +14,7 @@ public static class Utilities /// This method sets a custom workspace which will be used by the deploy tool to create and run the CDK project and any temporary files during the deployment. /// It also adds a nuget.config file that references a private nuget-cache. This cache holds the latest (in-development/unreleased) version of AWS.Deploy.Recipes.CDK.Common.nupkg file /// - public static void OverrideDefaultWorkspace(ServiceProvider serviceProvider, string customWorkspace) + public static void OverrideDefaultWorkspace(IServiceProvider serviceProvider, string customWorkspace) { var environmentVariableManager = serviceProvider.GetRequiredService(); environmentVariableManager.SetEnvironmentVariable("AWS_DOTNET_DEPLOYTOOL_WORKSPACE", customWorkspace); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs index 1e299dfcb..0aa01121f 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RedeploymentTests.cs @@ -19,30 +19,18 @@ namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { public class RedeploymentTests : IDisposable { - private readonly HttpHelper _httpHelper; private readonly CloudFormationHelper _cloudFormationHelper; private readonly ECSHelper _ecsHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; + private readonly IServiceCollection _serviceCollection; private bool _isDisposed; private string _stackName; public RedeploymentTests() { - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); - - _httpHelper = new HttpHelper(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -54,90 +42,130 @@ public RedeploymentTests() [Fact] public async Task AttemptWorkFlow() { - _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; - var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); - var projectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); - var compatibleDeploymentProjectPath = Path.Combine(tempDirectoryPath, "DeploymentProjects", "CompatibleCdkApp"); - var incompatibleDeploymentProjectPath = Path.Combine(tempDirectoryPath, "DeploymentProjects", "IncompatibleCdkApp"); - - // perform inital deployment using ECS Fargate recipe - await PerformInitialDeployment(projectPath); - - // Create a compatible CDK deployment project using the ECS recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(projectPath, "Custom ECS Fargate Recipe", "1", compatibleDeploymentProjectPath, underSourceControl: false); - - // Create an incompatible CDK deployment project using the App runner recipe - await Utilities.CreateCDKDeploymentProjectWithRecipeName(projectPath, "Custom App Runner Recipe", "2", incompatibleDeploymentProjectPath, underSourceControl: false); - - // attempt re-deployment using incompatible CDK project - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", incompatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; - var returnCode = await _app.Run(deployArgs); - Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); - - // attempt re-deployment using compatible CDK project - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", compatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; - returnCode = await _app.Run(deployArgs); - Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); - Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal(TaskDefinitionStatus.ACTIVE, cluster.Status); - - // Delete stack - await DeleteStack(); + InMemoryInteractiveService interactiveService = null; + try + { + _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; + var tempDirectoryPath = new TestAppManager().GetProjectPath(string.Empty); + var projectPath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); + var compatibleDeploymentProjectPath = Path.Combine(tempDirectoryPath, "DeploymentProjects", "CompatibleCdkApp"); + var incompatibleDeploymentProjectPath = Path.Combine(tempDirectoryPath, "DeploymentProjects", "IncompatibleCdkApp"); + + // perform inital deployment using ECS Fargate recipe + await PerformInitialDeployment(projectPath); + + // Create a compatible CDK deployment project using the ECS recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(projectPath, "Custom ECS Fargate Recipe", "1", compatibleDeploymentProjectPath, underSourceControl: false); + + // Create an incompatible CDK deployment project using the App runner recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(projectPath, "Custom App Runner Recipe", "2", incompatibleDeploymentProjectPath, underSourceControl: false); + + // attempt re-deployment using incompatible CDK project + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", incompatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; + var returnCode = await _serviceCollection.RunDeployToolAsync(deployArgs); + Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); + + deployArgs = new[] { "deploy", "--project-path", projectPath, "--deployment-project", compatibleDeploymentProjectPath, "--application-name", _stackName, "--diagnostics" }; + returnCode = await _serviceCollection.RunDeployToolAsync(deployArgs, provider => + { + interactiveService = provider.GetRequiredService(); + + // attempt re-deployment using compatible CDK project + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + }); + Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + var cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal(TaskDefinitionStatus.ACTIVE, cluster.Status); + + // Delete stack + await DeleteStack(); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } + + private async Task PerformInitialDeployment(string projectPath) { - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select ECS Fargate recommendation - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + var returnCode = await _serviceCollection.RunDeployToolAsync(deployArgs, provider => + { + interactiveService = provider.GetRequiredService(); - // Deploy - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - var returnCode = await _app.Run(deployArgs); - Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); + // Arrange input for deploy + interactiveService.StdInWriter.WriteLine("1"); // Select ECS Fargate recommendation + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + }); + Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal(TaskDefinitionStatus.ACTIVE, cluster.Status); + var cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal(TaskDefinitionStatus.ACTIVE, cluster.Status); - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - returnCode = await _app.Run(listArgs); - Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + returnCode = await _serviceCollection.RunDeployToolAsync(listArgs, provider => + { + interactiveService = provider.GetRequiredService(); + }); + Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } private async Task DeleteStack() { - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + InMemoryInteractiveService interactiveService = null; + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + var returnCode = await _serviceCollection.RunDeployToolAsync(deleteArgs, provider => + { + interactiveService = provider.GetRequiredService(); - // Delete - var returnCode = await _app.Run(deleteArgs); - Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Confirm delete + interactiveService.StdInWriter.Flush(); + }); + Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -157,8 +185,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs index 47362996a..af3473531 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs @@ -13,6 +13,9 @@ using Xunit; using Should; using System.Reflection; +using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Utilities; +using Spectre.Console.Cli; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -20,13 +23,7 @@ public static class Utilities { public static async Task CreateCDKDeploymentProject(string targetApplicationPath, string saveDirectoryPath = null, bool isValid = true) { - var (app, interactiveService) = GetAppServiceProvider(); - Assert.NotNull(app); - Assert.NotNull(interactiveService); - - // Arrange input for saving the CDK deployment project - await interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await interactiveService.StdInWriter.FlushAsync(); + var serviceCollection = GetAppServiceCollection(); string[] deployArgs; // default save directory @@ -34,19 +31,27 @@ public static async Task CreateCDKDeploymentProject(string targetApplicationPath { saveDirectoryPath = targetApplicationPath + ".Deployment"; deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--diagnostics" }; - } + } else { deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--output", saveDirectoryPath, "--diagnostics" }; } - - var returnCode = await app.Run(deployArgs); + InMemoryInteractiveService interactiveService = null; + var returnCode = await serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for saving the CDK deployment project + interactiveService.StdInWriter.WriteLine(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.Flush(); + }); // Verify project is saved var stdOut = interactiveService.StdOutReader.ReadAllLines(); var successMessage = $"Saving AWS CDK deployment project to: {saveDirectoryPath}"; - + if (!isValid) { returnCode.ShouldEqual(CommandReturnCodes.USER_ERROR); @@ -61,18 +66,7 @@ public static async Task CreateCDKDeploymentProject(string targetApplicationPath public static async Task CreateCDKDeploymentProjectWithRecipeName(string targetApplicationPath, string recipeName, string option, string saveDirectoryPath = null, bool isValid = true, bool underSourceControl = true) { - var (app, interactiveService) = GetAppServiceProvider(); - Assert.NotNull(app); - Assert.NotNull(interactiveService); - - // Arrange input for saving the CDK deployment project - await interactiveService.StdInWriter.WriteLineAsync(option); // select recipe to save the CDK deployment project - if (!underSourceControl) - { - await interactiveService.StdInWriter.WriteAsync("y"); // proceed to save without source control. - } - await interactiveService.StdInWriter.FlushAsync(); - + var serviceCollection = GetAppServiceCollection(); string[] deployArgs; // default save directory @@ -86,8 +80,20 @@ public static async Task CreateCDKDeploymentProjectWithRecipeName(string targetA deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--output", saveDirectoryPath, "--project-display-name", recipeName, "--diagnostics" }; } - - var returnCode = await app.Run(deployArgs); + InMemoryInteractiveService interactiveService = null; + var returnCode = await serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for saving the CDK deployment project + interactiveService.StdInWriter.WriteLine(option); // select recipe to save the CDK deployment project + if (!underSourceControl) + { + interactiveService.StdInWriter.Write("y"); // proceed to save without source control. + } + interactiveService.StdInWriter.Flush(); + }); // Verify project is saved var stdOut = interactiveService.StdOutReader.ReadAllLines(); @@ -105,18 +111,14 @@ public static async Task CreateCDKDeploymentProjectWithRecipeName(string targetA VerifyCreatedArtifacts(targetApplicationPath, saveDirectoryPath); } - private static (App app, InMemoryInteractiveService interactiveService) GetAppServiceProvider() + private static IServiceCollection GetAppServiceCollection() { var serviceCollection = new ServiceCollection(); serviceCollection.AddCustomServices(); serviceCollection.AddTestServices(); - var serviceProvider = serviceCollection.BuildServiceProvider(); - - var app = serviceProvider.GetService(); - var interactiveService = serviceProvider.GetService(); - return (app, interactiveService); + return serviceCollection; } private static void VerifyCreatedArtifacts(string targetApplicationPath, string saveDirectoryPath) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs index d83bd19f9..5110bae29 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Amazon.Runtime; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; @@ -53,10 +54,16 @@ public async Task DependentOptionSettingsGetInvalidated() var portNumber = 4022; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -144,10 +151,16 @@ public async Task SettingInvalidValue() var portNumber = 4022; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index 9f6f77b15..309764ff9 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Amazon.CloudFormation; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; @@ -71,10 +72,16 @@ public async Task GetAndApplyAppRunnerSettings_RecipeValidatorsAreRun() var portNumber = 4026; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -117,10 +124,16 @@ public async Task GetAndApplyAppRunnerSettings_FailedUpdatesReturnSettingId() var portNumber = 4027; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -161,10 +174,16 @@ public async Task ContainerPortSettings_PortWarning() var portNumber = 4025; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -199,10 +218,16 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() var portNumber = 4021; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -279,10 +304,16 @@ public async Task GetAppRunnerConfigSettings_TypeHintData() var portNumber = 4002; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -323,10 +354,16 @@ public async Task GetConfigSettingResources_VpcConnectorOptions() var portNumber = 4023; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -338,7 +375,7 @@ public async Task GetConfigSettingResources_VpcConnectorOptions() await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppAppRunner", _stackName); - // Assert that the Subnets and SecurityGroups options are returning columns + // Assert that the Subnets and SecurityGroups options are returning columns var subnets = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.Subnets"); Assert.Collection(subnets.Columns, column => Assert.NotNull(column), // Subnet Id @@ -382,10 +419,16 @@ public async Task GetAndApplyECSFargateSettings_LoadBalancerSchemeConfig(string // for this recipe takes longer than the default timeout httpClient.Timeout = new TimeSpan(0, 0, 120); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService()); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index 4b9037e19..28d6f3b31 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -13,19 +13,16 @@ using System.Threading.Tasks; using Amazon.CloudFormation; using Amazon.ECS; -using Amazon.Runtime; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.Common.UnitTests.IO; -using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; using AWS.Deploy.CLI.IntegrationTests.Services; using AWS.Deploy.CLI.IntegrationTests.Utilities; using AWS.Deploy.CLI.ServerMode; -using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; using AWS.Deploy.ServerMode.Client.Utilities; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Xunit; @@ -35,7 +32,6 @@ public class ServerModeTests : IDisposable { private bool _isDisposed; private string _stackName; - private readonly IServiceProvider _serviceProvider; private readonly CloudFormationHelper _cloudFormationHelper; private readonly string _awsRegion; @@ -49,13 +45,6 @@ public ServerModeTests() var cloudFormationClient = new AmazonCloudFormationClient(Amazon.RegionEndpoint.USWest2); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - _serviceProvider = serviceCollection.BuildServiceProvider(); - _awsRegion = "us-west-2"; _testAppManager = new TestAppManager(); @@ -70,10 +59,16 @@ public ServerModeTests() public async Task ConfirmLocalhostOnly() { var portNumber = 4900; - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - _ = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials)); @@ -101,10 +96,16 @@ public async Task GetRecommendations() var portNumber = 4000; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); @@ -158,10 +159,16 @@ public async Task GetRecommendationsWithEncryptedCredentials() await _interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await _interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(_interactiveService, portNumber, null, false); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = false + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); @@ -199,10 +206,16 @@ public async Task AppRunnerRedeployment_VisibleOnRedeploymentSettings() var portNumber = 4950; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -281,10 +294,16 @@ public async Task WebFargateDeploymentNoConfigChanges() var portNumber = 4011; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -410,10 +429,16 @@ public async Task RecommendationsForNewDeployments_DoesNotIncludeExistingBeansta var portNumber = 4002; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { @@ -447,11 +472,17 @@ public async Task RecommendationsForNewDeployments_DoesNotIncludeExistingBeansta public async Task ShutdownViaRestClient() { var portNumber = 4003; - var cancelSource = new CancellationTokenSource(); using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); + var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { @@ -483,10 +514,16 @@ public async Task InvalidStackName_ThrowsException(string invalidStackName) var portNumber = 4012; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var baseUrl = $"http://localhost:{portNumber}/"; @@ -528,10 +565,16 @@ public async Task CheckCategories() var portNumber = 4200; using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeUtilities.ResolveDefaultCredentials); - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = true + }; + var serverCommand = new ServerModeCommand(_interactiveService); var cancelSource = new CancellationTokenSource(); - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + _ = serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/VersionTest.cs b/test/AWS.Deploy.CLI.IntegrationTests/VersionTest.cs index 55368056c..b73139556 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/VersionTest.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/VersionTest.cs @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -13,23 +14,14 @@ namespace AWS.Deploy.CLI.IntegrationTests { public class VersionTest { - private readonly InMemoryInteractiveService _interactiveService; - private readonly App _app; + private readonly IServiceCollection _serviceCollection; public VersionTest() { - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); + _serviceCollection = new ServiceCollection(); - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); } [Theory] @@ -37,8 +29,13 @@ public VersionTest() [InlineData("-v")] public async Task VerifyVersionOutput(string arg) { - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(new[] { arg })); - var stdOut = _interactiveService.StdOutReader.ReadAllLines(); + InMemoryInteractiveService interactiveService = null; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync([arg], + provider => + { + interactiveService = provider.GetRequiredService(); + })); + var stdOut = interactiveService.StdOutReader.ReadAllLines(); var versionNumber = stdOut.First(line => line.StartsWith("Version")) .Split(":")[1] diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index b5283fbb0..26e8be0d4 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -22,10 +21,8 @@ namespace AWS.Deploy.CLI.IntegrationTests { public class WebAppNoDockerFileTests : IDisposable { - private readonly HttpHelper _httpHelper; + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; private readonly TestAppManager _testAppManager; @@ -33,34 +30,24 @@ public class WebAppNoDockerFileTests : IDisposable public WebAppNoDockerFileTests() { - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); - foreach (var item in serviceCollection) + foreach (var item in _serviceCollection) { if (item.ServiceType == typeof(IEnvironmentVariableManager)) { - serviceCollection.Remove(item); + _serviceCollection.Remove(item); break; } } - serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentVariableManager), typeof(TestEnvironmentVariableManager), ServiceLifetime.Singleton)); - - var serviceProvider = serviceCollection.BuildServiceProvider(); + _serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentVariableManager), typeof(TestEnvironmentVariableManager), ServiceLifetime.Singleton)); _customWorkspace = Path.Combine(Path.GetTempPath(), $"deploy-tool-workspace{Guid.NewGuid().ToString().Split('-').Last()}"); - Helpers.Utilities.OverrideDefaultWorkspace(serviceProvider, _customWorkspace); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); - - _httpHelper = new HttpHelper(_interactiveService); + Helpers.Utilities.OverrideDefaultWorkspace(_serviceCollection.BuildServiceProvider(), _customWorkspace); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -77,48 +64,70 @@ public async Task EBDefaultConfigurations(string configFile, bool linux) { _stackName = $"BeanstalkTest-{Guid.NewGuid().ToString().Split('-').Last()}"; - // Deploy - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", configFile }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - - // Example: Endpoint: http://52.36.216.238/ - var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); - var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":") + 1).Trim(); - Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", configFile }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + // Example: Endpoint: http://52.36.216.238/ + var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); + var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":") + 1).Trim(); + Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); + + if(!linux) + { + // "extra-path" is the IISAppPath set in the config file. + applicationUrl = new Uri(new Uri(applicationUrl), "extra-path").AbsoluteUri; + } - if(!linux) + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally { - // "extra-path" is the IISAppPath set in the config file. - applicationUrl = new Uri(new Uri(applicationUrl), "extra-path").AbsoluteUri; + interactiveService?.ReadStdOutStartToEnd(); } - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs)); ; - - // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete + // Delete // Use --silent flag to delete without user prompts var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" }; - - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs)); ; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs)); ; // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); @@ -128,72 +137,105 @@ public async Task EBDefaultConfigurations(string configFile, bool linux) public async Task BeanstalkArmDeployment() { _stackName = $"BeanstalkArm{Guid.NewGuid().ToString().Split('-').Last()}"; + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppArmDeployment", "WebAppArmDeployment.csproj")); - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteLineAsync("8"); // Select "Environment Architecture" - await _interactiveService.StdInWriter.WriteLineAsync("2"); // Select "Arm64" - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy - await _interactiveService.StdInWriter.FlushAsync(); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.WriteLine("8"); // Select "Environment Architecture" + interactiveService.StdInWriter.WriteLine("2"); // Select "Arm64" + interactiveService.StdInWriter.Write(Environment.NewLine); // Confirm selection and deploy + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + // Example: Endpoint: http://52.36.216.238/ + var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); + var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":", StringComparison.Ordinal) + 1).Trim(); + Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Deploy - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppArmDeployment", "WebAppArmDeployment.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - - // Example: Endpoint: http://52.36.216.238/ - var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); - var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":", StringComparison.Ordinal) + 1).Trim(); - Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs)); ; - - // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); - - // Try switching from ARM to X86_64 on redeployment - await _interactiveService.StdInWriter.WriteLineAsync("3"); // Select "Environment Architecture" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "X86_64" - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy - await _interactiveService.StdInWriter.WriteLineAsync("more"); // Select "Environment Architecture" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "EC2 Instance Type" - await _interactiveService.StdInWriter.WriteLineAsync("y"); // Select "Free tier" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "x86_64" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "CPU Cores" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "Instance Memory" - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select "Instance Type" - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy - await _interactiveService.StdInWriter.FlushAsync(); - - // Perform re-deployment - deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - Assert.Contains(deployStdOut, x => x.Contains("Please select an Instance Type that supports the currently selected Environment Architecture.")); - - // Arrange input for delete - // Use --silent flag to delete without user prompts - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } + + try + { + // Perform re-deployment + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Try switching from ARM to X86_64 on redeployment + interactiveService.StdInWriter.WriteLine("3"); // Select "Environment Architecture" + interactiveService.StdInWriter.WriteLine("1"); // Select "X86_64" + interactiveService.StdInWriter.Write(Environment.NewLine); // Confirm selection and deploy + interactiveService.StdInWriter.WriteLine("more"); // Select "Environment Architecture" + interactiveService.StdInWriter.WriteLine("1"); // Select "EC2 Instance Type" + interactiveService.StdInWriter.WriteLine("y"); // Select "Free tier" + interactiveService.StdInWriter.WriteLine("1"); // Select "x86_64" + interactiveService.StdInWriter.WriteLine("1"); // Select "CPU Cores" + interactiveService.StdInWriter.WriteLine("1"); // Select "Instance Memory" + interactiveService.StdInWriter.WriteLine("1"); // Select "Instance Type" + interactiveService.StdInWriter.Write(Environment.NewLine); // Confirm selection and deploy + interactiveService.StdInWriter.Flush(); + })); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + Assert.Contains(deployStdOut, x => x.Contains("Please select an Instance Type that supports the currently selected Environment Architecture.")); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs)); ; + // Use --silent flag to delete without user prompts + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs)); ; // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); @@ -203,42 +245,64 @@ public async Task BeanstalkArmDeployment() public async Task DeployRetiredDotnetVersion() { _stackName = $"RetiredDotnetBeanstalk{Guid.NewGuid().ToString().Split('-').Last()}"; - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebApiNET6", "WebApiNET6.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", "ElasticBeanStalkConfigFile.json" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - // Example: Endpoint: http://52.36.216.238/ - var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); - var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":", StringComparison.Ordinal) + 1).Trim(); - Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs)); ; + InMemoryInteractiveService interactiveService = null; + try + { + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", "ElasticBeanStalkConfigFile.json" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + // Example: Endpoint: http://52.36.216.238/ + var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint")); + var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":", StringComparison.Ordinal) + 1).Trim(); + Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute)); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify stack exists in list of deployments - var listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete + // Delete // Use --silent flag to delete without user prompts var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" }; - - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs)); ; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs)); ; // Verify application is deleted Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); @@ -261,8 +325,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index 73037ecb6..21c4a0f91 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs @@ -22,11 +22,9 @@ namespace AWS.Deploy.CLI.IntegrationTests { public class WebAppWithDockerFileTests : IDisposable { - private readonly HttpHelper _httpHelper; + private readonly IServiceCollection _serviceCollection; private readonly CloudFormationHelper _cloudFormationHelper; private readonly ECSHelper _ecsHelper; - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private bool _isDisposed; private string _stackName; private readonly TestAppManager _testAppManager; @@ -36,20 +34,10 @@ public WebAppWithDockerFileTests() var ecsClient = new AmazonECSClient(); _ecsHelper = new ECSHelper(ecsClient); - var serviceCollection = new ServiceCollection(); + _serviceCollection = new ServiceCollection(); - serviceCollection.AddCustomServices(); - serviceCollection.AddTestServices(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - _app = serviceProvider.GetService(); - Assert.NotNull(_app); - - _interactiveService = serviceProvider.GetService(); - Assert.NotNull(_interactiveService); - - _httpHelper = new HttpHelper(_interactiveService); + _serviceCollection.AddCustomServices(); + _serviceCollection.AddTestServices(); var cloudFormationClient = new AmazonCloudFormationClient(); _cloudFormationHelper = new CloudFormationHelper(cloudFormationClient); @@ -62,102 +50,161 @@ public async Task ApplySettingsWithoutDeploying() { _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteLineAsync("more"); // Select 'more' - await _interactiveService.StdInWriter.WriteLineAsync("13"); // Select 'Environment Architecture' - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select 'X86_64' - await _interactiveService.StdInWriter.WriteLineAsync("13"); // Select 'Environment Architecture' again for Code Coverage - await _interactiveService.StdInWriter.WriteLineAsync("1"); // Select 'X86_64' - await _interactiveService.StdInWriter.WriteLineAsync("8"); // Select 'Task CPU' - await _interactiveService.StdInWriter.WriteLineAsync("2"); // Select '512 (.5 vCPU)' - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - - await _app.Run(deployArgs); - - var consoleOutput = _interactiveService.StdOutReader.ReadAllLines(); - - // Assert 'Environment Architecture' is set to 'X86_64' - var environmentArchitecture = consoleOutput.LastOrDefault(x => x.StartsWith("13. Environment Architecture:")); - Assert.NotNull(environmentArchitecture); - var environmentArchitectureSplit = environmentArchitecture.Split(':').ToList().Select(x => x.Trim()).ToList(); - Assert.Equal(2, environmentArchitectureSplit.Count); - Assert.Equal("X86_64", environmentArchitectureSplit[1]); - - // Assert 'Task CPU' is set to '512' - var taskCpu = consoleOutput.LastOrDefault(x => x.StartsWith("8 . Task CPU:")); - Assert.NotNull(taskCpu); - var taskCpuSplit = taskCpu.Split(':').ToList().Select(x => x.Trim()).ToList(); - Assert.Equal(2, taskCpuSplit.Count); - Assert.Equal("512", taskCpuSplit[1]); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + + await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.WriteLine("more"); // Select 'more' + interactiveService.StdInWriter.WriteLine("13"); // Select 'Environment Architecture' + interactiveService.StdInWriter.WriteLine("1"); // Select 'X86_64' + interactiveService.StdInWriter.WriteLine("13"); // Select 'Environment Architecture' again for Code Coverage + interactiveService.StdInWriter.WriteLine("1"); // Select 'X86_64' + interactiveService.StdInWriter.WriteLine("8"); // Select 'Task CPU' + interactiveService.StdInWriter.WriteLine("2"); // Select '512 (.5 vCPU)' + interactiveService.StdInWriter.Flush(); + }); + + var consoleOutput = interactiveService.StdOutReader.ReadAllLines(); + + // Assert 'Environment Architecture' is set to 'X86_64' + var environmentArchitecture = consoleOutput.LastOrDefault(x => x.StartsWith("13. Environment Architecture:")); + Assert.NotNull(environmentArchitecture); + var environmentArchitectureSplit = environmentArchitecture.Split(':').ToList().Select(x => x.Trim()).ToList(); + Assert.Equal(2, environmentArchitectureSplit.Count); + Assert.Equal("X86_64", environmentArchitectureSplit[1]); + + // Assert 'Task CPU' is set to '512' + var taskCpu = consoleOutput.LastOrDefault(x => x.StartsWith("8 . Task CPU:")); + Assert.NotNull(taskCpu); + var taskCpuSplit = taskCpu.Split(':').ToList().Select(x => x.Trim()).ToList(); + Assert.Equal(2, taskCpuSplit.Count); + Assert.Equal("512", taskCpuSplit[1]); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } [Fact] public async Task DefaultConfigurations() { _stackName = $"WebAppWithDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; - - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal("ACTIVE", cluster.Status); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); - - // Arrange input for re-deployment - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - // Perform re-deployment - deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - Assert.Equal("ACTIVE", cluster.Status); + Cluster cluster = null; + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal("ACTIVE", cluster.Status); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // Perform re-deployment + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + Assert.Equal(StackStatus.UPDATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + Assert.Equal("ACTIVE", cluster.Status); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } [Fact] @@ -165,39 +212,52 @@ public async Task CustomContainerPortConfigurations() { var stackNamePlaceholder = "{StackName}"; _stackName = $"WebAppNoDockerFile{Guid.NewGuid().ToString().Split('-').Last()}"; - - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default option settings - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ECSFargateCustomPortConfigFile.json"); - ConfigFileHelper.ApplyReplacementTokens(new Dictionary { { stackNamePlaceholder, _stackName } }, configFilePath); - - // Deploy - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--apply", configFilePath, "--silent" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var cluster = await _ecsHelper.GetCluster(_stackName); - Assert.Equal("ACTIVE", cluster.Status); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); - var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); - Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "ECSFargateCustomPortConfigFile.json"); + ConfigFileHelper.ApplyReplacementTokens(new Dictionary { { stackNamePlaceholder, _stackName } }, configFilePath); + + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--apply", configFilePath, "--silent" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var cluster = await _ecsHelper.GetCluster(_stackName); + Assert.Equal("ACTIVE", cluster.Status); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: ")); + var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim(); + Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist."); + + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } [Fact] @@ -209,48 +269,83 @@ public async Task AppRunnerDeployment() var configFilePath = Path.Combine(Directory.GetParent(projectPath).FullName, "AppRunnerConfigFile.json"); ConfigFileHelper.ApplyReplacementTokens(new Dictionary { { stackNamePlaceholder, _stackName } }, configFilePath); - // Deploy - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--apply", configFilePath, "--silent" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.RecipeEndpointURL")) - .Split("=")[1] - .Trim(); - - Assert.Contains("awsapprunner", applicationUrl); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // Ensure environemnt variables specified in AppRunnerConfigFile.json are set for the service. - var checkEnvironmentVariableUrl = applicationUrl + "envvar/TEST_Key1"; - using var httpClient = new HttpClient(); - var envVarValue = await httpClient.GetStringAsync(checkEnvironmentVariableUrl); - Assert.Equal("Value1", envVarValue); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); - - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--apply", configFilePath, "--silent" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var applicationUrl = deployStdOut.First(line => line.StartsWith($"{_stackName}.RecipeEndpointURL")) + .Split("=")[1] + .Trim(); + + Assert.Contains("awsapprunner", applicationUrl); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // Ensure environemnt variables specified in AppRunnerConfigFile.json are set for the service. + var checkEnvironmentVariableUrl = applicationUrl + "envvar/TEST_Key1"; + using var httpClient = new HttpClient(); + var envVarValue = await httpClient.GetStringAsync(checkEnvironmentVariableUrl); + Assert.Equal("Value1", envVarValue); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } [Fact] @@ -258,48 +353,83 @@ public async Task FargateArmDeployment() { _stackName = $"FargateArmDeployment{Guid.NewGuid().ToString().Split('-').Last()}"; - // Arrange input for deploy - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation - await _interactiveService.StdInWriter.WriteLineAsync("8"); // Select "Environment Architecture" - await _interactiveService.StdInWriter.WriteLineAsync("2"); // Select "Arm64" - await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy - await _interactiveService.StdInWriter.FlushAsync(); - - // Deploy - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppArmWithDocker", "WebAppArmWithDocker.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); - - // Verify application is deployed and running - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); - - var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); - - var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) - .Split(" ")[1] - .Trim(); - - // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout - await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); - - // list - var listArgs = new[] { "list-deployments", "--diagnostics" }; - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; - - // Verify stack exists in list of deployments - var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); - Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); - - // Arrange input for delete - await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete - await _interactiveService.StdInWriter.FlushAsync(); - var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + InMemoryInteractiveService interactiveService = null; + try + { + // Deploy + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppArmWithDocker", "WebAppArmWithDocker.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deployArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for deploy + interactiveService.StdInWriter.Write(Environment.NewLine); // Select default recommendation + interactiveService.StdInWriter.WriteLine("8"); // Select "Environment Architecture" + interactiveService.StdInWriter.WriteLine("2"); // Select "Arm64" + interactiveService.StdInWriter.Write(Environment.NewLine); // Confirm selection and deploy + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = interactiveService.StdOutReader.ReadAllLines(); + + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + var httpHelper = new HttpHelper(interactiveService); + await httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Delete - Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + try + { + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(listArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + })); + + // Verify stack exists in list of deployments + var listDeployStdOut = interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } - // Verify application is deleted - Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); + try + { + // Delete + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _serviceCollection.RunDeployToolAsync(deleteArgs, + provider => + { + interactiveService = provider.GetRequiredService(); + + // Arrange input for delete + interactiveService.StdInWriter.Write("y"); // Select default option settings + interactiveService.StdInWriter.Flush(); + })); + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); + } + finally + { + interactiveService?.ReadStdOutStartToEnd(); + } } public void Dispose() @@ -319,8 +449,6 @@ protected virtual void Dispose(bool disposing) { _cloudFormationHelper.DeleteStack(_stackName).GetAwaiter().GetResult(); } - - _interactiveService.ReadStdOutStartToEnd(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.UnitTests/AWS.Deploy.CLI.UnitTests.csproj b/test/AWS.Deploy.CLI.UnitTests/AWS.Deploy.CLI.UnitTests.csproj index 0a8c4a550..6b88b5ebe 100644 --- a/test/AWS.Deploy.CLI.UnitTests/AWS.Deploy.CLI.UnitTests.csproj +++ b/test/AWS.Deploy.CLI.UnitTests/AWS.Deploy.CLI.UnitTests.csproj @@ -5,7 +5,6 @@ false AWS.Deploy.CLI.UnitTests AWS.Deploy.CLI.UnitTests - 9.0 diff --git a/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs b/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs deleted file mode 100644 index 35af3992c..000000000 --- a/test/AWS.Deploy.CLI.UnitTests/Commands/CommandFactoryTest.cs +++ /dev/null @@ -1,576 +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.Linq; -using Amazon.Runtime; -using System.Threading.Tasks; -using AWS.Deploy.CLI.Commands; -using AWS.Deploy.CLI.Commands.CommandHandlerInput; -using AWS.Deploy.CLI.Commands.TypeHints; -using AWS.Deploy.CLI.Utilities; -using AWS.Deploy.Common; -using AWS.Deploy.Common.Data; -using AWS.Deploy.Common.DeploymentManifest; -using AWS.Deploy.Common.IO; -using AWS.Deploy.Common.Recipes; -using AWS.Deploy.Common.Recipes.Validation; -using AWS.Deploy.Orchestration; -using AWS.Deploy.Orchestration.CDK; -using AWS.Deploy.Orchestration.DisplayedResources; -using AWS.Deploy.Orchestration.LocalUserSettings; -using AWS.Deploy.Orchestration.ServiceHandlers; -using AWS.Deploy.Orchestration.Utilities; -using Moq; -using Xunit; -using Amazon.SecurityToken.Model; -using System.Collections.Generic; -using Amazon.Extensions.NETCore.Setup; - -namespace AWS.Deploy.CLI.UnitTests -{ - public class CommandFactoryTests - { - private readonly Mock _mockServiceProvider; - private readonly Mock _mockToolInteractiveService; - private readonly Mock _mockOrchestratorInteractiveService; - private readonly Mock _mockCdkManager; - private readonly Mock _mockSystemCapabilityEvaluator; - private readonly Mock _mockCloudApplicationNameGenerator; - private readonly Mock _mockAwsUtilities; - private readonly Mock _mockAwsClientFactory; - private readonly Mock _mockAwsResourceQueryer; - private readonly Mock _mockProjectParserUtility; - private readonly Mock _mockCommandLineWrapper; - private readonly Mock _mockCdkProjectHandler; - private readonly Mock _mockDeploymentBundleHandler; - private readonly Mock _mockCloudFormationTemplateReader; - private readonly Mock _mockDeployedApplicationQueryer; - private readonly Mock _mockTypeHintCommandFactory; - private readonly Mock _mockDisplayedResourceHandler; - private readonly Mock _mockConsoleUtilities; - private readonly Mock _mockDirectoryManager; - private readonly Mock _mockFileManager; - private readonly Mock _mockDeploymentManifestEngine; - private readonly Mock _mockLocalUserSettingsEngine; - private readonly Mock _mockCdkVersionDetector; - private readonly Mock _mockAwsServiceHandler; - private readonly Mock _mockOptionSettingHandler; - private readonly Mock _mockValidatorFactory; - private readonly Mock _mockRecipeHandler; - private readonly Mock _mockDeployToolWorkspaceMetadata; - private readonly Mock _mockDeploymentSettingsHandler; - - public CommandFactoryTests() - { - _mockServiceProvider = new Mock(); - _mockToolInteractiveService = new Mock(); - _mockOrchestratorInteractiveService = new Mock(); - _mockCdkManager = new Mock(); - _mockSystemCapabilityEvaluator = new Mock(); - _mockCloudApplicationNameGenerator = new Mock(); - _mockAwsUtilities = new Mock(); - _mockAwsClientFactory = new Mock(); - _mockAwsResourceQueryer = new Mock(); - _mockProjectParserUtility = new Mock(); - _mockCommandLineWrapper = new Mock(); - _mockCdkProjectHandler = new Mock(); - _mockDeploymentBundleHandler = new Mock(); - _mockCloudFormationTemplateReader = new Mock(); - _mockDeployedApplicationQueryer = new Mock(); - _mockTypeHintCommandFactory = new Mock(); - _mockDisplayedResourceHandler = new Mock(); - _mockConsoleUtilities = new Mock(); - _mockDirectoryManager = new Mock(); - _mockFileManager = new Mock(); - _mockDeploymentManifestEngine = new Mock(); - _mockLocalUserSettingsEngine = new Mock(); - _mockCdkVersionDetector = new Mock(); - _mockAwsServiceHandler = new Mock(); - _mockOptionSettingHandler = new Mock(); - _mockValidatorFactory = new Mock(); - _mockRecipeHandler = new Mock(); - _mockDeployToolWorkspaceMetadata = new Mock(); - _mockDeploymentSettingsHandler = new Mock(); - } - - private CommandFactory CreateCommandFactory() - { - return new CommandFactory( - _mockServiceProvider.Object, - _mockToolInteractiveService.Object, - _mockOrchestratorInteractiveService.Object, - _mockCdkManager.Object, - _mockSystemCapabilityEvaluator.Object, - _mockCloudApplicationNameGenerator.Object, - _mockAwsUtilities.Object, - _mockAwsClientFactory.Object, - _mockAwsResourceQueryer.Object, - _mockProjectParserUtility.Object, - _mockCommandLineWrapper.Object, - _mockCdkProjectHandler.Object, - _mockDeploymentBundleHandler.Object, - _mockCloudFormationTemplateReader.Object, - _mockDeployedApplicationQueryer.Object, - _mockTypeHintCommandFactory.Object, - _mockDisplayedResourceHandler.Object, - _mockConsoleUtilities.Object, - _mockDirectoryManager.Object, - _mockFileManager.Object, - _mockDeploymentManifestEngine.Object, - _mockLocalUserSettingsEngine.Object, - _mockCdkVersionDetector.Object, - _mockAwsServiceHandler.Object, - _mockOptionSettingHandler.Object, - _mockValidatorFactory.Object, - _mockRecipeHandler.Object, - _mockDeployToolWorkspaceMetadata.Object, - _mockDeploymentSettingsHandler.Object - ); - } - - [Fact] - public void BuildRootCommand_ReturnsRootCommandWithExpectedSubcommands() - { - // Arrange - var commandFactory = CreateCommandFactory(); - - // Act - var rootCommand = commandFactory.BuildRootCommand(); - - // Assert - Assert.NotNull(rootCommand); - Assert.Equal("dotnet-aws", rootCommand.Name); - Assert.Contains(rootCommand.Options, o => o.Name == "version"); - Assert.Contains(rootCommand.Children, c => c.Name == "deploy"); - Assert.Contains(rootCommand.Children, c => c.Name == "list-deployments"); - Assert.Contains(rootCommand.Children, c => c.Name == "delete-deployment"); - Assert.Contains(rootCommand.Children, c => c.Name == "deployment-project"); - Assert.Contains(rootCommand.Children, c => c.Name == "server-mode"); - } - - [Fact] - public void BuildRootCommand_DeployCommandHasExpectedOptions() - { - // Arrange - var commandFactory = CreateCommandFactory(); - - // Act - var rootCommand = commandFactory.BuildRootCommand(); - var deployCommand = rootCommand.Children.First(c => c.Name == "deploy") as Command; - - // Assert - Assert.NotNull(deployCommand); - Assert.Contains(deployCommand.Options, o => o.Name == "profile"); - Assert.Contains(deployCommand.Options, o => o.Name == "region"); - Assert.Contains(deployCommand.Options, o => o.Name == "project-path"); - Assert.Contains(deployCommand.Options, o => o.Name == "application-name"); - Assert.Contains(deployCommand.Options, o => o.Name == "apply"); - Assert.Contains(deployCommand.Options, o => o.Name == "diagnostics"); - Assert.Contains(deployCommand.Options, o => o.Name == "silent"); - Assert.Contains(deployCommand.Options, o => o.Name == "deployment-project"); - Assert.Contains(deployCommand.Options, o => o.Name == "save-settings"); - Assert.Contains(deployCommand.Options, o => o.Name == "save-all-settings"); - } - - // Add more tests for other commands and their options... - - [Fact] - public void BuildRootCommand_ServerModeCommandHasExpectedOptions() - { - // Arrange - var commandFactory = CreateCommandFactory(); - - // Act - var rootCommand = commandFactory.BuildRootCommand(); - var serverModeCommand = rootCommand.Children.First(c => c.Name == "server-mode") as Command; - - // Assert - Assert.NotNull(serverModeCommand); - Assert.Contains(serverModeCommand.Options, o => o.Name == "port"); - Assert.Contains(serverModeCommand.Options, o => o.Name == "parent-pid"); - Assert.Contains(serverModeCommand.Options, o => o.Name == "unsecure-mode"); - Assert.Contains(serverModeCommand.Options, o => o.Name == "diagnostics"); - } - - [Fact] - public async Task DeployCommand_UsesRegionFromCLIWhenProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromCLI = "us-west-2"; - var regionFromProfile = "us-east-1"; - var testProjectPath = "/path/to/project"; - - // Mock ProjectDefinition - var mockProjectDefinition = new ProjectDefinition( - new System.Xml.XmlDocument(), - testProjectPath, - testProjectPath, - "123"); - - _mockProjectParserUtility - .Setup(x => x.Parse(It.IsAny())) - .ReturnsAsync(mockProjectDefinition); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns(regionFromCLI); - - // Create a mock DeploymentSettings - var mockDeploymentSettings = new DeploymentSettings - { - AWSProfile = "deployment-profile", - AWSRegion = "deployment-region" - }; - - _mockDeploymentSettingsHandler - .Setup(x => x.ReadSettings(It.IsAny())) - .ReturnsAsync(mockDeploymentSettings); - - _mockAwsResourceQueryer - .Setup(x => x.GetCallerIdentity(It.IsAny())) - .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); - - - // Act - var result = await InvokeDeployCommandHandler(new DeployCommandHandlerInput - { - Profile = testProfile, - Region = regionFromCLI, - Apply = "some-settings-file.json", - ProjectPath = testProjectPath - }); - - // Assert - _mockProjectParserUtility.Verify(x => x.Parse(testProjectPath), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); - } - - [Fact] - public async Task DeployCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromProfile = "us-east-1"; - var testProjectPath = "/path/to/project"; - - // Mock ProjectDefinition - var mockProjectDefinition = new ProjectDefinition( - new System.Xml.XmlDocument(), - testProjectPath, - testProjectPath, - "123"); - - _mockProjectParserUtility - .Setup(x => x.Parse(It.IsAny())) - .ReturnsAsync(mockProjectDefinition); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns((string r, string f) => r); - - // Create a mock DeploymentSettings - var mockDeploymentSettings = new DeploymentSettings - { - AWSProfile = "deployment-profile", - AWSRegion = null // Ensure this is null to test our scenario - }; - - _mockDeploymentSettingsHandler - .Setup(x => x.ReadSettings(It.IsAny())) - .ReturnsAsync(mockDeploymentSettings); - - _mockAwsResourceQueryer - .Setup(x => x.GetCallerIdentity(It.IsAny())) - .ReturnsAsync(new GetCallerIdentityResponse { Account = "123456789012" }); - - // Act - var result = await InvokeDeployCommandHandler(new DeployCommandHandlerInput - { - Profile = testProfile, - Region = null, // Not providing a region via CLI - Apply = "some-settings-file.json", - ProjectPath = testProjectPath - }); - - // Assert - _mockProjectParserUtility.Verify(x => x.Parse(testProjectPath), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); - - // Verify that the region from the profile is used - _mockAwsResourceQueryer.Verify(x => x.GetCallerIdentity(regionFromProfile), Times.Once); - } - - [Fact] - public void BuildRootCommand_DeleteCommandHasExpectedOptions() - { - // Arrange - var commandFactory = CreateCommandFactory(); - - // Act - var rootCommand = commandFactory.BuildRootCommand(); - var deleteCommand = rootCommand.Children.First(c => c.Name == "delete-deployment") as Command; - - // Assert - Assert.NotNull(deleteCommand); - Assert.Contains(deleteCommand.Options, o => o.Name == "profile"); - Assert.Contains(deleteCommand.Options, o => o.Name == "region"); - Assert.Contains(deleteCommand.Options, o => o.Name == "project-path"); - Assert.Contains(deleteCommand.Options, o => o.Name == "diagnostics"); - Assert.Contains(deleteCommand.Options, o => o.Name == "silent"); - - // Verify that the delete command has a deployment-name argument - Assert.Contains(deleteCommand.Arguments, a => a.Name == "deployment-name"); - } - - [Fact] - public void BuildRootCommand_ListCommandHasExpectedOptions() - { - // Arrange - var commandFactory = CreateCommandFactory(); - - // Act - var rootCommand = commandFactory.BuildRootCommand(); - var listCommand = rootCommand.Children.First(c => c.Name == "list-deployments") as Command; - - // Assert - Assert.NotNull(listCommand); - Assert.Contains(listCommand.Options, o => o.Name == "profile"); - Assert.Contains(listCommand.Options, o => o.Name == "region"); - Assert.Contains(listCommand.Options, o => o.Name == "diagnostics"); - } - - [Fact] - public async Task DeleteCommand_UsesRegionFromCLIWhenProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromCLI = "us-west-2"; - var regionFromProfile = "us-east-1"; - var deploymentName = "test-deployment"; - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns(regionFromCLI); - - _mockAwsClientFactory - .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) - .Callback>(action => - { - var options = new AWSOptions(); - action(options); - Assert.Equal(regionFromCLI, options.Region.SystemName); - }); - - // Act - var result = await InvokeDeleteCommandHandler(new DeleteCommandHandlerInput - { - Profile = testProfile, - Region = regionFromCLI, - DeploymentName = deploymentName - }); - - // Assert - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); - _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); - } - - [Fact] - public async Task DeleteCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromProfile = "us-east-1"; - var deploymentName = "test-deployment"; - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns((string r, string f) => f); - - _mockAwsClientFactory - .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) - .Callback>(action => - { - var options = new AWSOptions(); - action(options); - Assert.Equal(regionFromProfile, options.Region.SystemName); - }); - - // Act - var result = await InvokeDeleteCommandHandler(new DeleteCommandHandlerInput - { - Profile = testProfile, - Region = null, // Not providing a region via CLI - DeploymentName = deploymentName - }); - - // Assert - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); - _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); - } - - [Fact] - public async Task ListCommand_UsesRegionFromCLIWhenProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromCLI = "us-west-2"; - var regionFromProfile = "us-east-1"; - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns(regionFromCLI); - - _mockAwsClientFactory - .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) - .Callback>(action => - { - var options = new AWSOptions(); - action(options); - Assert.Equal(regionFromCLI, options.Region.SystemName); - }); - - // Act - var result = await InvokeListCommandHandler(new ListCommandHandlerInput - { - Profile = testProfile, - Region = regionFromCLI - }); - - // Assert - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromCLI, null), Times.Once); - _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); - } - - [Fact] - public async Task ListCommand_UsesRegionFromProfileWhenCLIRegionNotProvided() - { - // Arrange - var commandFactory = CreateCommandFactory(); - var mockCredentials = new Mock(); - var testProfile = "test-profile"; - var regionFromProfile = "us-east-1"; - - _mockAwsUtilities - .Setup(x => x.ResolveAWSCredentials(It.IsAny())) - .Returns(Task.FromResult((Tuple.Create(mockCredentials.Object, regionFromProfile)))); - - _mockAwsUtilities - .Setup(x => x.ResolveAWSRegion(It.IsAny(), It.IsAny())) - .Returns((string r, string f) => f); - - _mockAwsClientFactory - .Setup(x => x.ConfigureAWSOptions(It.IsAny>())) - .Callback>(action => - { - var options = new AWSOptions(); - action(options); - Assert.Equal(regionFromProfile, options.Region.SystemName); - }); - - // Act - var result = await InvokeListCommandHandler(new ListCommandHandlerInput - { - Profile = testProfile, - Region = null // Not providing a region via CLI - }); - - // Assert - _mockAwsUtilities.Verify(x => x.ResolveAWSCredentials(testProfile), Times.Once); - _mockAwsUtilities.Verify(x => x.ResolveAWSRegion(regionFromProfile, null), Times.Once); - _mockAwsClientFactory.Verify(x => x.ConfigureAWSOptions(It.IsAny>()), Times.Once); - } - - private async Task InvokeDeployCommandHandler(DeployCommandHandlerInput input) - { - var args = new List { "deploy" }; - - if (!string.IsNullOrEmpty(input.Profile)) - args.AddRange(new[] { "--profile", input.Profile }); - - if (!string.IsNullOrEmpty(input.Region)) - args.AddRange(new[] { "--region", input.Region }); - - if (!string.IsNullOrEmpty(input.Apply)) - args.AddRange(new[] { "--apply", input.Apply }); - - if (!string.IsNullOrEmpty(input.ProjectPath)) - args.AddRange(new[] { "--project-path", input.ProjectPath }); - - var rootCommand = CreateCommandFactory().BuildRootCommand(); - return await rootCommand.InvokeAsync(args.ToArray()); - } - - - private async Task InvokeDeleteCommandHandler(DeleteCommandHandlerInput input) - { - var args = new List { "delete-deployment" }; - - if (!string.IsNullOrEmpty(input.Profile)) - args.AddRange(new[] { "--profile", input.Profile }); - - if (!string.IsNullOrEmpty(input.Region)) - args.AddRange(new[] { "--region", input.Region }); - - if (!string.IsNullOrEmpty(input.DeploymentName)) - args.Add(input.DeploymentName); - - var rootCommand = CreateCommandFactory().BuildRootCommand(); - return await rootCommand.InvokeAsync(args.ToArray()); - } - - private async Task InvokeListCommandHandler(ListCommandHandlerInput input) - { - var args = new List { "list-deployments" }; - - if (!string.IsNullOrEmpty(input.Profile)) - args.AddRange(new[] { "--profile", input.Profile }); - - if (!string.IsNullOrEmpty(input.Region)) - args.AddRange(new[] { "--region", input.Region }); - - var rootCommand = CreateCommandFactory().BuildRootCommand(); - return await rootCommand.InvokeAsync(args.ToArray()); - } - - - } -} diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs index 9dabbdc63..0c4f41831 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeAuthTests.cs @@ -20,6 +20,7 @@ using AWS.Deploy.CLI.UnitTests.Utilities; using System.Threading; using System.Globalization; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.CLI.IntegrationTests.Services; namespace AWS.Deploy.CLI.UnitTests @@ -211,14 +212,20 @@ public async Task AuthMissingEncryptionInfoVersion() await interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, false); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = false + }; + var serverCommand = new ServerModeCommand(interactiveService); var cancelSource = new CancellationTokenSource(); Exception actualException = null; try { - await serverCommand.ExecuteAsync(cancelSource.Token); + await serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); } catch(InvalidEncryptionKeyInfoException e) { @@ -254,12 +261,18 @@ public async Task AuthEncryptionWithInvalidVersion() await interactiveService.StdInWriter.WriteAsync(keyInfoStdin); await interactiveService.StdInWriter.FlushAsync(); - var serverCommand = new ServerModeCommand(interactiveService, portNumber, null, false); + var serverCommandSettings = new ServerModeCommandSettings + { + Port = portNumber, + ParentPid = null, + UnsecureMode = false + }; + var serverCommand = new ServerModeCommand(interactiveService); var cancelSource = new CancellationTokenSource(); Exception actualException = null; try { - await serverCommand.ExecuteAsync(cancelSource.Token); + await serverCommand.ExecuteAsync(null!, serverCommandSettings, cancelSource); } catch (InvalidEncryptionKeyInfoException e) { diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index 965649cbc..1f73e5076 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -23,6 +23,7 @@ using AWS.Deploy.Common.Recipes; using DeploymentTypes = AWS.Deploy.CLI.ServerMode.Models.DeploymentTypes; using System; +using AWS.Deploy.CLI.Commands.Settings; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Recipes; @@ -47,11 +48,23 @@ public void EnsureMinimumSystemTextJsonVersion() [Fact] public async Task TcpPortIsInUseTest() { - var serverModeCommand1 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, true); - var serverModeCommand2 = new ServerModeCommand(new TestToolInteractiveServiceImpl(), 1234, null, true); - - var serverModeTask1 = serverModeCommand1.ExecuteAsync(); - var serverModeTask2 = serverModeCommand2.ExecuteAsync(); + var serverCommandSettings1 = new ServerModeCommandSettings + { + Port = 1234, + ParentPid = null, + UnsecureMode = true + }; + var serverModeCommand1 = new ServerModeCommand(new TestToolInteractiveServiceImpl()); + var serverCommandSettings2 = new ServerModeCommandSettings + { + Port = 1234, + ParentPid = null, + UnsecureMode = true + }; + var serverModeCommand2 = new ServerModeCommand(new TestToolInteractiveServiceImpl()); + + var serverModeTask1 = serverModeCommand1.ExecuteAsync(null!, serverCommandSettings1); + var serverModeTask2 = serverModeCommand2.ExecuteAsync(null!, serverCommandSettings2); await Task.WhenAny(serverModeTask1, serverModeTask2); diff --git a/test/AWS.Deploy.DockerImageUploader/App.cs b/test/AWS.Deploy.DockerImageUploader/App.cs index 4f454534e..68c2368a6 100644 --- a/test/AWS.Deploy.DockerImageUploader/App.cs +++ b/test/AWS.Deploy.DockerImageUploader/App.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using System.IO; using System.Linq; +using AWS.Deploy.CLI.Utilities; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Docker; @@ -26,7 +27,6 @@ public class App private readonly IDirectoryManager _directoryManager; private readonly IProjectDefinitionParser _projectDefinitionParser; private readonly IRecipeHandler _recipeHandler; - private readonly CLI.App _deployToolCli; private readonly List _testApps = new() { "WebApiNET6", "ConsoleAppTask" }; @@ -36,7 +36,6 @@ public App(IServiceProvider serviceProvider) _fileManager = serviceProvider.GetRequiredService(); _directoryManager = serviceProvider.GetRequiredService(); _recipeHandler = serviceProvider.GetRequiredService(); - _deployToolCli = serviceProvider.GetRequiredService(); } /// @@ -64,7 +63,13 @@ private async Task CreateImageAndPushToECR(string projectPath) var configFilePath = Path.Combine(projectPath, "DockerImageUploaderConfigFile.json"); var deployArgs = new[] { "deploy", "--project-path", projectPath, "--diagnostics", "--apply", configFilePath, "--silent" }; - await _deployToolCli.Run(deployArgs); + var serviceCollection = new ServiceCollection(); + + var registrar = new TypeRegistrar(serviceCollection); + + var app = CLI.App.ConfigureServices(registrar); + + await CLI.App.RunAsync(deployArgs, app, registrar); } private string ResolvePath(string projectName) diff --git a/test/AWS.Deploy.DockerImageUploader/Program.cs b/test/AWS.Deploy.DockerImageUploader/Program.cs index 695792c57..f23af7df2 100644 --- a/test/AWS.Deploy.DockerImageUploader/Program.cs +++ b/test/AWS.Deploy.DockerImageUploader/Program.cs @@ -5,6 +5,7 @@ using AWS.Deploy.CLI.Extensions; using System.Threading.Tasks; using System; +using AWS.Deploy.CLI.Utilities; namespace AWS.Deploy.DockerImageUploader {