diff --git a/.chglog/config.yml b/.chglog/config.yml index b262810aa..c40e8eb34 100644 --- a/.chglog/config.yml +++ b/.chglog/config.yml @@ -17,7 +17,20 @@ options: - build - ci - chore + sort_by: Subject commit_groups: + sort_by: Custom + title_order: + - feat + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore title_maps: feat: Features fix: Bug Fixes diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 07f9c89ad..90e467c33 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -15,6 +15,8 @@ using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.CLI.Commands.CommandHandlerInput; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.DeploymentManifest; namespace AWS.Deploy.CLI.Commands { @@ -32,6 +34,8 @@ public class CommandFactory : ICommandFactory private static readonly Option _optionDiagnosticLogging = new(new []{"-d", "--diagnostics"}, "Enable diagnostic output."); private static readonly Option _optionApply = new("--apply", "Path to the deployment settings file to be applied."); private static readonly Option _optionDisableInteractive = new(new []{"-s", "--silent" }, "Disable interactivity to deploy without any prompts for user input."); + private 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 readonly IToolInteractiveService _toolInteractiveService; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; @@ -49,6 +53,7 @@ public class CommandFactory : ICommandFactory private readonly IDeployedApplicationQueryer _deployedApplicationQueryer; private readonly ITypeHintCommandFactory _typeHintCommandFactory; private readonly IConsoleUtilities _consoleUtilities; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; public CommandFactory( IToolInteractiveService toolInteractiveService, @@ -66,7 +71,8 @@ public CommandFactory( ITemplateMetadataReader templateMetadataReader, IDeployedApplicationQueryer deployedApplicationQueryer, ITypeHintCommandFactory typeHintCommandFactory, - IConsoleUtilities consoleUtilities) + IConsoleUtilities consoleUtilities, + IDeploymentManifestEngine deploymentManifestEngine) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -84,6 +90,7 @@ public CommandFactory( _deployedApplicationQueryer = deployedApplicationQueryer; _typeHintCommandFactory = typeHintCommandFactory; _consoleUtilities = consoleUtilities; + _deploymentManifestEngine = deploymentManifestEngine; } public Command BuildRootCommand() @@ -98,6 +105,7 @@ public Command BuildRootCommand() rootCommand.Add(BuildDeployCommand()); rootCommand.Add(BuildListCommand()); rootCommand.Add(BuildDeleteCommand()); + rootCommand.Add(BuildDeploymentProjectCommand()); rootCommand.Add(BuildServerModeCommand()); return rootCommand; @@ -307,6 +315,81 @@ private Command BuildListCommand() 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") + { + _optionOutputDirectory, + _optionDiagnosticLogging, + _optionProjectPath, + _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 DirectoryManager().GetDirectoryInfo(projectDefinition.ProjectPath).FullName; + + var generateDeploymentProject = new GenerateDeploymentProjectCommand( + _toolInteractiveService, + _consoleUtilities, + _cdkProjectHandler, + _commandLineWrapper, + new DirectoryManager(), + new FileManager(), + session, + _deploymentManifestEngine, + 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; + } + }); + + deploymentProjectCommand.Add(generateDeploymentProjectCommand); + return deploymentProjectCommand; + } + private Command BuildServerModeCommand() { var serverModeCommand = new Command( diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs new file mode 100644 index 000000000..cb0736298 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.CLI.Commands.CommandHandlerInput +{ + /// + /// This class maps the command line options for the "deployment-project generate" command to the appropriate C# properties. + /// + public class GenerateDeploymentProjectCommandHandlerInput + { + public string? ProjectPath { get; set; } + public bool Diagnostics { get; set; } + public string? Output { get; set; } + public string? ProjectDisplayName { get; set; } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 26d6f2f11..9978cb565 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -174,13 +174,11 @@ public async Task ConfigureDeployment(CloudApplication cloudApplication, Orchest // Apply the user entered project name to the recommendation so that any default settings based on project name are applied. selectedRecommendation.OverrideProjectName(cloudApplication.Name); - var deploymentBundleDefinition = orchestrator.GetDeploymentBundleDefinition(selectedRecommendation); - - var configurableOptionSettings = selectedRecommendation.Recipe.OptionSettings.Union(deploymentBundleDefinition.Parameters); + var configurableOptionSettings = selectedRecommendation.GetConfigurableOptionSettingItems(); if (userDeploymentSettings != null) { - ConfigureDeploymentFromConfigFile(selectedRecommendation, deploymentBundleDefinition, userDeploymentSettings); + ConfigureDeploymentFromConfigFile(selectedRecommendation, userDeploymentSettings); } if (!_toolInteractiveService.DisableInteractive) @@ -215,7 +213,7 @@ private async Task GetSelectedRecommendationFromPreviousDeployme " does not match the original recipe used to deploy the application stack."); } - selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); + selectedRecommendation = selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); var header = $"Loading {deployedApplication.Name} settings:"; @@ -250,23 +248,16 @@ private async Task GetSelectedRecommendationFromPreviousDeployme /// This method is used to set the values for Option Setting Items when a deployment is being performed using a user specifed config file. /// /// The selected recommendation settings used for deployment - /// The container for the deployment bundle used by an application. /// The deserialized object from the user provided config file. - private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, DeploymentBundleDefinition deploymentBundleDefinition, UserDeploymentSettings userDeploymentSettings) + private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, UserDeploymentSettings userDeploymentSettings) { foreach (var entry in userDeploymentSettings.LeafOptionSettingItems) { var optionSettingJsonPath = entry.Key; var optionSettingValue = entry.Value; - var isPartOfDeploymentBundle = true; - var optionSetting = deploymentBundleDefinition.Parameters.FirstOrDefault(x => x.Id.Equals(optionSettingJsonPath)); - if (optionSetting == null) - { - optionSetting = recommendation.GetOptionSetting(optionSettingJsonPath); - isPartOfDeploymentBundle = false; - } - + var optionSetting = recommendation.GetOptionSetting(optionSettingJsonPath); + if (!recommendation.IsExistingCloudApplication || optionSetting.Updatable) { object settingValue; @@ -291,11 +282,10 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, De { throw new InvalidOverrideValueException($"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); } - + optionSetting.SetValueOverride(settingValue); - if (isPartOfDeploymentBundle) - SetDeploymentBundleOptionSetting(recommendation, optionSetting.Id, settingValue); + SetDeploymentBundleOptionSetting(recommendation, optionSetting.Id, settingValue); } } @@ -341,7 +331,7 @@ private void SetDeploymentBundleOptionSetting(Recommendation recommendation, str new DotnetPublishSelfContainedBuildCommand(_consoleUtilities).OverrideValue(recommendation, (bool)settingValue); break; default: - throw new OptionSettingItemDoesNotExistException($"The Option Setting Item { optionSettingId } does not exist."); + return; } } @@ -485,7 +475,7 @@ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendat errorMessage += "Specify a valid Docker execution directory as part of the deployment settings file and try again."; throw new DockerBuildFailedException(errorMessage); } - + _toolInteractiveService.WriteLine(string.Empty); var answer = _consoleUtilities.AskYesNoQuestion("Do you want to go back and modify the current configuration?", "true"); if (answer == YesNo.Yes) diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs new file mode 100644 index 000000000..413c90124 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -0,0 +1,238 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +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; + +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 + { + 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 string _targetApplicationFullPath; + + public GenerateDeploymentProjectCommand( + IToolInteractiveService toolInteractiveService, + IConsoleUtilities consoleUtilities, + ICdkProjectHandler cdkProjectHandler, + ICommandLineWrapper commandLineWrapper, + IDirectoryManager directoryManager, + IFileManager fileManager, + OrchestratorSession session, + IDeploymentManifestEngine deploymentManifestEngine, + string targetApplicationFullPath) + { + _toolInteractiveService = toolInteractiveService; + _consoleUtilities = consoleUtilities; + _cdkProjectHandler = cdkProjectHandler; + _commandLineWrapper = commandLineWrapper; + _directoryManager = directoryManager; + _fileManager = fileManager; + _session = session; + _deploymentManifestEngine = deploymentManifestEngine; + _targetApplicationFullPath = targetApplicationFullPath; + } + + /// + /// 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, new[] { RecipeLocator.FindRecipeDefinitionsPath() }); + var recommendations = await GenerateDeploymentRecommendations(orchestrator); + var selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations); + + if (string.IsNullOrEmpty(saveCdkDirectoryPath)) + saveCdkDirectoryPath = GenerateDefaultSaveDirectoryPath(); + + var newDirectoryCreated = CreateSaveCdkDirectory(saveCdkDirectoryPath); + + var (isValid, errorMessage) = ValidateSaveCdkDirectory(saveCdkDirectoryPath); + if (!isValid) + { + if (newDirectoryCreated) + _directoryManager.Delete(saveCdkDirectoryPath); + errorMessage = $"Failed to generate deployment project.{Environment.NewLine}{errorMessage}"; + throw new 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; + } + } + + await _cdkProjectHandler.CreateCdkProjectForDeployment(selectedRecommendation, _session, saveCdkDirectoryPath); + await GenerateDeploymentRecipeSnapShot(selectedRecommendation, saveCdkDirectoryPath, projectDisplayName); + + var saveCdkDirectoryFullPath = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).FullName; + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteLine($"The CDK deployment project is saved at: {saveCdkDirectoryFullPath}"); + + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, _targetApplicationFullPath); + } + + /// + /// This method generates the appropriate recommendations for the target deployment project. + /// + /// + /// A list of + private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator) + { + var recommendations = await orchestrator.GenerateDeploymentRecommendations(forDeployment: false); + if (recommendations.Count == 0) + { + throw new FailedToGenerateAnyRecommendations("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\MyWebAppCDK If the save directory already exists, then a suffix number is attached. + /// + /// The defaukt save directory path. + private string GenerateDefaultSaveDirectoryPath() + { + var applicatonDirectoryFullPath = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath).Parent.FullName; + var saveCdkDirectoryFullPath = applicatonDirectoryFullPath + "CDK"; + + var suffixNumber = 0; + while (_directoryManager.Exists(saveCdkDirectoryFullPath)) + saveCdkDirectoryFullPath = applicatonDirectoryFullPath + $"CDK{++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) + { + var newDirectoryCreated = false; + if (!_directoryManager.Exists(saveCdkDirectoryPath)) + { + _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 containaing a boolean that indicates if the directory is valid and a corresponding string error message. + private Tuple ValidateSaveCdkDirectory(string saveCdkDirectoryPath) + { + var errorMessage = string.Empty; + var isValid = true; + var targetApplicationDirectoryFullPath = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath).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; + + 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."; + + isValid = false; + } + + 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. + private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendation, string saveCdkDirectoryPath, string projectDisplayName) + { + var targetApplicationDirectoryName = _directoryManager.GetDirectoryInfo(_targetApplicationFullPath).Parent.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); + + 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; + + var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, Formatting.Indented); + await _fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody); + } + } +} diff --git a/src/AWS.Deploy.CLI/Exceptions.cs b/src/AWS.Deploy.CLI/Exceptions.cs index 957bb2f84..c63e3468c 100644 --- a/src/AWS.Deploy.CLI/Exceptions.cs +++ b/src/AWS.Deploy.CLI/Exceptions.cs @@ -79,4 +79,13 @@ public class FailedToFindCompatibleRecipeException : Exception { public FailedToFindCompatibleRecipeException(string message, Exception? innerException = null) : base(message, innerException) { } } + + /// + /// Throw if the directory specified to save the CDK deployment project is invalid. + /// + [AWSDeploymentExpectedException] + public class InvalidSaveDirectoryForCdkProject : Exception + { + public InvalidSaveDirectoryForCdkProject(string message, Exception? innerException = null) : base(message, innerException) { } + } } diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 8cb7fc742..43f61c0dd 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -5,6 +5,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration; @@ -48,6 +49,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveServiceImpl), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ITypeHintCommandFactory), typeof(TypeHintCommandFactory), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IZipFileManager), typeof(ZipFileManager), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentManifestEngine), typeof(DeploymentManifestEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 7f07d8042..6ea582a36 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -150,9 +150,7 @@ public IActionResult GetConfigSettings(string sessionId) var orchestrator = CreateOrchestrator(state); - var deploymentBundleDefinition = orchestrator.GetDeploymentBundleDefinition(state.SelectedRecommendation); - - var configurableOptionSettings = state.SelectedRecommendation.Recipe.OptionSettings.Union(deploymentBundleDefinition.Parameters); + var configurableOptionSettings = state.SelectedRecommendation.GetConfigurableOptionSettingItems(); var output = new GetOptionSettingsOutput(); output.OptionSettings = ListOptionSettingSummary(state.SelectedRecommendation, configurableOptionSettings); @@ -220,7 +218,7 @@ public IActionResult ApplyConfigSettings(string sessionId, [FromBody] ApplyConfi return Ok(output); } - + /// /// Gets the list of existing deployments that are compatible with the session's project. /// @@ -302,7 +300,7 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody } var existingCloudApplicationMetadata = await templateMetadataReader.LoadCloudApplicationMetadata(input.ExistingDeploymentName); - state.SelectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); + state.SelectedRecommendation = state.SelectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); state.ApplicationDetails.Name = input.ExistingDeploymentName; state.ApplicationDetails.RecipeId = existingDeployment.RecipeId; diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs new file mode 100644 index 000000000..eaa6aaac0 --- /dev/null +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using AWS.Deploy.Common.IO; +using Newtonsoft.Json; + +namespace AWS.Deploy.Common.DeploymentManifest +{ + public interface IDeploymentManifestEngine + { + Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, string targetApplicationFullPath); + } + + /// + /// This class contains the helper methods to update the deployment manifest file + /// that keeps track of the save CDK deployment projects. + /// + public class DeploymentManifestEngine : IDeploymentManifestEngine + { + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + + private const string DEPLOYMENT_MANIFEST_FILE_NAME = "aws-deployments.json"; + + public DeploymentManifestEngine(IDirectoryManager directoryManager, IFileManager fileManager) + { + _directoryManager = directoryManager; + _fileManager = fileManager; + } + + /// + /// This method updates the deployment manifest json file by adding the directory path at which the CDK deployment project is saved. + /// If the manifest file does not exists then a new file is generated. + /// The absolute path to the directory at which the CDK deployment project is saved + /// The absolute path to the target application csproj or fsproj file. + /// Thrown if an error occured while trying to update the deployment manifest file. + /// + /// + public async Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, string targetApplicationFullPath) + { + try + { + var deploymentManifestFilePath = GetDeploymentManifestFilePath(targetApplicationFullPath); + var saveCdkDirectoryRelativePath = _directoryManager.GetRelativePath(targetApplicationFullPath, saveCdkDirectoryFullPath); + + DeploymentManifestModel deploymentManifestModel; + + if (_fileManager.Exists(deploymentManifestFilePath)) + { + deploymentManifestModel = await ReadManifestFile(deploymentManifestFilePath); + deploymentManifestModel.DeploymentManifestEntries.Add(new DeploymentManifestEntry(saveCdkDirectoryRelativePath)); + } + else + { + var deploymentManifestEntries = new List { new DeploymentManifestEntry(saveCdkDirectoryRelativePath) }; + deploymentManifestModel = new DeploymentManifestModel(deploymentManifestEntries); + } + + var manifestFileJsonString = SerializeManifestModel(deploymentManifestModel); + await _fileManager.WriteAllTextAsync(deploymentManifestFilePath, manifestFileJsonString); + } + catch (Exception ex) + { + throw new FailedToUpdateDeploymentManifestFileException($"Failed to update the deployment manifest file " + + $"for the deployment project stored at '{saveCdkDirectoryFullPath}'", ex); + } + + } + + /// + /// This method parses the deployment-manifest file into a + /// + /// The path to the deployment-manifest file + /// An instance of + private async Task ReadManifestFile(string filePath) + { + var manifestFilejsonString = await _fileManager.ReadAllTextAsync(filePath); + return JsonConvert.DeserializeObject(manifestFilejsonString); + } + + /// + /// This method parses the into a string + /// + /// + /// A formatted string representation of + private string SerializeManifestModel(DeploymentManifestModel deploymentManifestModel) + { + return JsonConvert.SerializeObject(deploymentManifestModel, Formatting.Indented); + } + + /// + /// This method returns the path at which the deployment-manifest file will be stored. + /// The absolute path to the target application csproj or fsproj file + /// + /// The path to the deployment-manifest file. + private string GetDeploymentManifestFilePath(string targetApplicationFullPath) + { + var projectDirectoryFullPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; + var deploymentManifestFileFullPath = Path.Combine(projectDirectoryFullPath, DEPLOYMENT_MANIFEST_FILE_NAME); + return deploymentManifestFileFullPath; + } + } +} diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEntry.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEntry.cs new file mode 100644 index 000000000..28276082f --- /dev/null +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEntry.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Newtonsoft.Json; + +namespace AWS.Deploy.Common.DeploymentManifest +{ + /// + /// This class supports serialization and de-serialization of the deployment-manifest file. + /// + public class DeploymentManifestEntry + { + [JsonProperty("Path")] + public string SaveCdkDirectoryRelativePath { get; set; } + + public DeploymentManifestEntry(string saveCdkDirectoryRelativePath) + { + SaveCdkDirectoryRelativePath = saveCdkDirectoryRelativePath; + } + } +} diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs new file mode 100644 index 000000000..030876ff1 --- /dev/null +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestModel.cs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace AWS.Deploy.Common.DeploymentManifest +{ + /// + /// This class supports serialization and de-serialization of the deployment-manifest file. + /// + public class DeploymentManifestModel + { + [JsonProperty("deployment-projects")] + public List DeploymentManifestEntries { get; set; } + + public DeploymentManifestModel(List deploymentManifestEntries) + { + DeploymentManifestEntries = deploymentManifestEntries; + } + } +} diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 0c2f951a8..6e3eda4c1 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -150,7 +150,7 @@ public class InvalidProjectPathException : Exception { public InvalidProjectPathException(string message, Exception? innerException = null) : base(message, innerException) { } } - + /// Throw if an invalid is used. /// [AWSDeploymentExpectedException] @@ -159,6 +159,23 @@ public class InvalidUserDeploymentSettingsException : Exception public InvalidUserDeploymentSettingsException(string message, Exception? innerException = null) : base(message, innerException) { } } + /// + /// Exception is thrown if we cannot retrieve deployment bundle definitions + /// + [AWSDeploymentExpectedException] + public class NoDeploymentBundleDefinitionsFoundException : Exception + { + public NoDeploymentBundleDefinitionsFoundException(string message, Exception? innerException = null) : base(message, innerException) { } + } + + /// Exception thrown if a failure occured while trying to update the deployment manifest file. + /// + [AWSDeploymentExpectedException] + public class FailedToUpdateDeploymentManifestFileException : Exception + { + public FailedToUpdateDeploymentManifestFileException(string message, Exception? innerException = null) : base(message, innerException) { } + } + /// /// Indicates a specific strongly typed Exception can be anticipated. /// Whoever throws this error should also present the user with helpful information diff --git a/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs b/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs new file mode 100644 index 000000000..11763794f --- /dev/null +++ b/src/AWS.Deploy.Common/Extensions/GenericExtensions.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using Newtonsoft.Json; + +namespace AWS.Deploy.Common.Extensions +{ + public static class GenericExtensions + { + public static T DeepCopy(this T obj) + { + var serializedObject = JsonConvert.SerializeObject(obj); + return JsonConvert.DeserializeObject(serializedObject); + } + } +} diff --git a/src/AWS.Deploy.Common/IO/DirectoryManager.cs b/src/AWS.Deploy.Common/IO/DirectoryManager.cs index c2572a31f..498978656 100644 --- a/src/AWS.Deploy.Common/IO/DirectoryManager.cs +++ b/src/AWS.Deploy.Common/IO/DirectoryManager.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using System.IO; namespace AWS.Deploy.Common.IO @@ -8,16 +9,41 @@ namespace AWS.Deploy.Common.IO public interface IDirectoryManager { DirectoryInfo CreateDirectory(string path); + DirectoryInfo GetDirectoryInfo(string path); bool Exists(string path); - string[] GetFiles(string projectPath, string? searchPattern = null); + string[] GetFiles(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly); + string[] GetDirectories(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly); + bool IsEmpty(string path); + bool ExistsInsideDirectory(string parentDirectoryPath, string childPath); + void Delete(string path, bool recursive = false); + string GetRelativePath(string referenceFullPath, string targetFullPath); } public class DirectoryManager : IDirectoryManager { public DirectoryInfo CreateDirectory(string path) => Directory.CreateDirectory(path); + + public DirectoryInfo GetDirectoryInfo(string path) => new DirectoryInfo(path); public bool Exists(string path) => Directory.Exists(path); - public string[] GetFiles(string path, string? searchPattern = null) => Directory.GetFiles(path, searchPattern ?? "*"); + public string[] GetFiles(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) + => Directory.GetFiles(path, searchPattern ?? "*", searchOption); + + public string[] GetDirectories(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) + => Directory.GetDirectories(path, searchPattern ?? "*", searchOption); + + public bool IsEmpty(string path) => GetFiles(path).Length == 0 && GetDirectories(path).Length == 0; + + public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) + { + var parentDirectoryFullPath = GetDirectoryInfo(parentDirectoryPath).FullName; + var childFullPath = GetDirectoryInfo(childPath).FullName; + return childFullPath.Contains(parentDirectoryFullPath + Path.DirectorySeparatorChar, StringComparison.InvariantCulture); + } + + public void Delete(string path, bool recursive = false) => Directory.Delete(path, recursive); + + public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); } } diff --git a/src/AWS.Deploy.Common/ProjectDefinitionParser.cs b/src/AWS.Deploy.Common/ProjectDefinitionParser.cs index a090d8954..2d57257f5 100644 --- a/src/AWS.Deploy.Common/ProjectDefinitionParser.cs +++ b/src/AWS.Deploy.Common/ProjectDefinitionParser.cs @@ -33,10 +33,18 @@ public ProjectDefinitionParser(IFileManager fileManager, IDirectoryManager direc _directoryManager = directoryManager; } + /// + /// This method parses the target application project and sets the + /// appropriate metadata as part of the + /// + /// The project path can be an absolute or a relative path to the + /// target application project directory or the application project file. + /// public async Task Parse(string projectPath) { if (_directoryManager.Exists(projectPath)) { + projectPath = _directoryManager.GetDirectoryInfo(projectPath).FullName; var files = _directoryManager.GetFiles(projectPath, "*.csproj"); if (files.Length == 1) { diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 5a89305fe..e8f18c003 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -112,7 +112,7 @@ public void SetValueOverride(object valueOverride) if (AllowedValues != null && AllowedValues.Count > 0 && valueOverride != null && !AllowedValues.Contains(valueOverride.ToString() ?? "")) - throw new InvalidOverrideValueException($"Invalid value for option setting item: {Id}"); + throw new InvalidOverrideValueException($"Invalid value for option setting item '{Name}'"); if (valueOverride is bool || valueOverride is int || valueOverride is long) { diff --git a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs index dfdf4ba99..9b63b3dcb 100644 --- a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs +++ b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs @@ -62,12 +62,12 @@ public class RecipeDefinition /// /// The location of the CDK project template relative from the recipe definition file. /// - public string CdkProjectTemplate { get; set; } + public string? CdkProjectTemplate { get; set; } /// /// The ID of the CDK project template for the template generator. /// - public string CdkProjectTemplateId { get; set; } + public string? CdkProjectTemplateId { get; set; } /// /// The rules used by the recommendation engine to determine if the recipe definition is compatible with the project. diff --git a/src/AWS.Deploy.Common/Recipes/Validation/IDeployToolValidationContext.cs b/src/AWS.Deploy.Common/Recipes/Validation/IDeployToolValidationContext.cs index 9a6e13bc7..9bd5125b8 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/IDeployToolValidationContext.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/IDeployToolValidationContext.cs @@ -16,6 +16,6 @@ namespace AWS.Deploy.Common.Recipes.Validation public interface IDeployToolValidationContext { ProjectDefinition ProjectDefinition { get; } - string AWSRegion { get; } + string? AWSRegion { get; } } } diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index c0b0ae358..8ebd89738 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.Common @@ -28,16 +29,20 @@ public class Recommendation : IUserInputOption public DeploymentBundle DeploymentBundle { get; } + private readonly List DeploymentBundleSettings = new (); + private readonly Dictionary _replacementTokens = new(); - public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefinition, int computedPriority, Dictionary additionalReplacements) + public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefinition, List deploymentBundleSettings, int computedPriority, Dictionary additionalReplacements) { + additionalReplacements ??= new Dictionary(); Recipe = recipe; ComputedPriority = computedPriority; ProjectDefinition = projectDefinition; DeploymentBundle = new DeploymentBundle(); + DeploymentBundleSettings = deploymentBundleSettings; _replacementTokens[REPLACE_TOKEN_PROJECT_NAME] = Path.GetFileNameWithoutExtension(projectDefinition.ProjectPath); @@ -57,19 +62,20 @@ public void OverrideProjectName(string name) _replacementTokens[REPLACE_TOKEN_PROJECT_NAME] = name; } - public void ApplyPreviousSettings(IDictionary? previousSettings) + public Recommendation ApplyPreviousSettings(IDictionary previousSettings) { - if (previousSettings == null) - return; + var recommendation = this.DeepCopy(); + + ApplyPreviousSettings(recommendation, previousSettings); - ApplyPreviousSettings(Recipe.OptionSettings, previousSettings); + return recommendation; } - private void ApplyPreviousSettings(IEnumerable optionSettings, IDictionary previousSettings) + private void ApplyPreviousSettings(Recommendation recommendation, IDictionary previousSettings) { - IsExistingCloudApplication = true; + recommendation.IsExistingCloudApplication = true; - foreach (var optionSetting in optionSettings) + foreach (var optionSetting in recommendation.Recipe.OptionSettings) { if (previousSettings.TryGetValue(optionSetting.Id, out var value)) { @@ -78,6 +84,14 @@ private void ApplyPreviousSettings(IEnumerable optionSettings } } + public IEnumerable GetConfigurableOptionSettingItems() + { + if (DeploymentBundleSettings == null) + return Recipe.OptionSettings; + + return Recipe.OptionSettings.Union(DeploymentBundleSettings); + } + /// /// Interactively traverses given json path and returns target option setting. /// Throws exception if there is no that matches /> @@ -98,7 +112,7 @@ public OptionSettingItem GetOptionSetting(string? jsonPath) foreach (var id in ids) { - var optionSettings = optionSetting?.ChildOptionSettings ?? Recipe.OptionSettings; + var optionSettings = optionSetting?.ChildOptionSettings ?? GetConfigurableOptionSettingItems(); optionSetting = optionSettings.FirstOrDefault(os => os.Id.Equals(id)); if (optionSetting == null) { diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 1cf7deb27..eb6d05bca 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -1,8 +1,10 @@ +using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.Recipes.CDK.Common; @@ -12,6 +14,7 @@ namespace AWS.Deploy.Orchestration public interface ICdkProjectHandler { public Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation); + public Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveDirectoryPath = null); } public class CdkProjectHandler : ICdkProjectHandler @@ -19,12 +22,14 @@ public class CdkProjectHandler : ICdkProjectHandler private readonly IOrchestratorInteractiveService _interactiveService; private readonly ICommandLineWrapper _commandLineWrapper; private readonly CdkAppSettingsSerializer _appSettingsBuilder; + private readonly IDirectoryManager _directoryManager; public CdkProjectHandler(IOrchestratorInteractiveService interactiveService, ICommandLineWrapper commandLineWrapper) { _interactiveService = interactiveService; _commandLineWrapper = commandLineWrapper; _appSettingsBuilder = new CdkAppSettingsSerializer(); + _directoryManager = new DirectoryManager(); } public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplication cloudApplication, Recommendation recommendation) @@ -61,18 +66,32 @@ await _commandLineWrapper.Run( $"npx cdk deploy --require-approval never -c {Con needAwsCredentials: true); } - private async Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session) + public async Task CreateCdkProjectForDeployment(Recommendation recommendation, OrchestratorSession session, string? saveCdkDirectoryPath = null) { - var tempDirectoryPath = + string? assemblyName; + if (string.IsNullOrEmpty(saveCdkDirectoryPath)) + { + saveCdkDirectoryPath = Path.Combine( Constants.CDK.ProjectsDirectory, Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); - Directory.CreateDirectory(tempDirectoryPath); + + assemblyName = recommendation.ProjectDefinition.AssemblyName; + } + else + { + assemblyName = _directoryManager.GetDirectoryInfo(saveCdkDirectoryPath).Name; + } + + if (string.IsNullOrEmpty(assemblyName)) + throw new ArgumentNullException("The assembly name for the CDK deployment project cannot be null"); + + _directoryManager.CreateDirectory(saveCdkDirectoryPath); var templateEngine = new TemplateEngine(); - await templateEngine.GenerateCDKProjectFromTemplate(recommendation, session, tempDirectoryPath); + await templateEngine.GenerateCDKProjectFromTemplate(recommendation, session, saveCdkDirectoryPath, assemblyName); - return tempDirectoryPath; + return saveCdkDirectoryPath; } } } diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index cc9bcc7da..b9d4564e8 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -1,3 +1,4 @@ + using System; using AWS.Deploy.Common; @@ -84,15 +85,6 @@ public class DockerPushFailedException : Exception public DockerPushFailedException(string message, Exception? innerException = null) : base(message, innerException) { } } - /// - /// Exception is thrown if we cannot retrieve deployment bundle definitions - /// - [AWSDeploymentExpectedException] - public class NoDeploymentBundleDefinitionsFoundException : Exception - { - public NoDeploymentBundleDefinitionsFoundException(string message, Exception? innerException = null) : base(message, innerException) { } - } - /// /// Exception is thrown if we cannot retrieve recipe definitions /// diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index f721f04bf..a457fa5f8 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -24,15 +24,15 @@ public class Orchestrator { private const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; - private readonly ICdkProjectHandler _cdkProjectHandler; - private readonly ICDKManager _cdkManager; - private readonly IOrchestratorInteractiveService _interactiveService; - private readonly IAWSResourceQueryer _awsResourceQueryer; - private readonly IDeploymentBundleHandler _deploymentBundleHandler; - private readonly IDockerEngine _dockerEngine; - private readonly IList _recipeDefinitionPaths; + private readonly ICdkProjectHandler? _cdkProjectHandler; + private readonly ICDKManager? _cdkManager; + private readonly IOrchestratorInteractiveService? _interactiveService; + private readonly IAWSResourceQueryer? _awsResourceQueryer; + private readonly IDeploymentBundleHandler? _deploymentBundleHandler; + private readonly IDockerEngine? _dockerEngine; + private readonly IList? _recipeDefinitionPaths; - private readonly OrchestratorSession _session; + private readonly OrchestratorSession? _session; public Orchestrator( OrchestratorSession session, @@ -54,9 +54,24 @@ public Orchestrator( _recipeDefinitionPaths = recipeDefinitionPaths; } - public async Task> GenerateDeploymentRecommendations() + public Orchestrator(OrchestratorSession session, IList recipeDefinitionPaths) { + _session = session; + _recipeDefinitionPaths = recipeDefinitionPaths; + } + + public async Task> GenerateDeploymentRecommendations(bool forDeployment = true) + { + if (_recipeDefinitionPaths == null) + throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); + if (_session == null) + throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); + var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths, _session); + + if (!forDeployment) + return await engine.ComputeRecommendations(); + var additionalReplacements = await GetReplacements(); return await engine.ComputeRecommendations(additionalReplacements); } @@ -65,6 +80,9 @@ public async Task> GetReplacements() { var replacements = new Dictionary(); + if (_awsResourceQueryer == null) + throw new InvalidOperationException($"{nameof(_awsResourceQueryer)} is null as part of the Orchestrator object"); + var latestPlatform = await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(); replacements[REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN] = latestPlatform.PlatformArn; @@ -73,6 +91,15 @@ public async Task> GetReplacements() public async Task DeployRecommendation(CloudApplication cloudApplication, Recommendation recommendation) { + if (_interactiveService == null) + throw new InvalidOperationException($"{nameof(_interactiveService)} is null as part of the orchestartor object"); + if (_cdkManager == null) + throw new InvalidOperationException($"{nameof(_cdkManager)} is null as part of the orchestartor object"); + if (_cdkProjectHandler == null) + throw new InvalidOperationException($"{nameof(_cdkProjectHandler)} is null as part of the orchestartor object"); + if (_session == null) + throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); + _interactiveService.LogMessageLine(string.Empty); _interactiveService.LogMessageLine($"Initiating deployment: {recommendation.Name}"); @@ -93,37 +120,15 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm } } - public DeploymentBundleDefinition GetDeploymentBundleDefinition(Recommendation recommendation) - { - var deploymentBundleDefinitionsPath = DeploymentBundleDefinitionLocator.FindDeploymentBundleDefinitionPath(); - - try - { - foreach (var deploymentBundleFile in Directory.GetFiles(deploymentBundleDefinitionsPath, "*.deploymentbundle", SearchOption.TopDirectoryOnly)) - { - try - { - var content = File.ReadAllText(deploymentBundleFile); - var definition = JsonConvert.DeserializeObject(content); - if (definition.Type.Equals(recommendation.Recipe.DeploymentBundle)) - return definition; - } - catch (Exception e) - { - throw new Exception($"Failed to Deserialize Deployment Bundle [{deploymentBundleFile}]: {e.Message}", e); - } - } - } - catch(IOException) - { - throw new NoDeploymentBundleDefinitionsFoundException("Failed to find a deployment bundle definition"); - } - - throw new NoDeploymentBundleDefinitionsFoundException("Failed to find a deployment bundle definition"); - } - public async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) { + if (_interactiveService == null) + throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); + if (_dockerEngine == null) + throw new InvalidOperationException($"{nameof(_dockerEngine)} is null as part of the orchestartor object"); + if (_deploymentBundleHandler == null) + throw new InvalidOperationException($"{nameof(_deploymentBundleHandler)} is null as part of the orchestartor object"); + if (!recommendation.ProjectDefinition.HasDockerFile) { _interactiveService.LogMessageLine("Generating Dockerfile..."); @@ -159,6 +164,11 @@ public async Task CreateContainerDeploymentBundle(CloudApplication cloudAp public async Task CreateDotnetPublishDeploymentBundle(Recommendation recommendation) { + if (_deploymentBundleHandler == null) + throw new InvalidOperationException($"{nameof(_deploymentBundleHandler)} is null as part of the orchestartor object"); + if (_interactiveService == null) + throw new InvalidOperationException($"{nameof(_interactiveService)} is null as part of the orchestartor object"); + try { await _deploymentBundleHandler.CreateDotnetPublishZip(recommendation); diff --git a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs index bf0830a30..6bd780ffc 100644 --- a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs +++ b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs @@ -16,8 +16,8 @@ public class OrchestratorSession : IDeployToolValidationContext { public ProjectDefinition ProjectDefinition { get; set; } public string? AWSProfileName { get; set; } - public AWSCredentials AWSCredentials { get; set; } - public string AWSRegion { get; set; } + public AWSCredentials? AWSCredentials { get; set; } + public string? AWSRegion { get; set; } /// /// Calculating the current can take several seconds /// and is not needed immediately so it is run as a background Task. @@ -25,7 +25,7 @@ public class OrchestratorSession : IDeployToolValidationContext /// It's safe to repeatedly await this property; evaluation will only be done once. /// public Task? SystemCapabilities { get; set; } - public string AWSAccountId { get; set; } + public string? AWSAccountId { get; set; } public OrchestratorSession( ProjectDefinition projectDefinition, @@ -38,5 +38,10 @@ public OrchestratorSession( AWSRegion = awsRegion; AWSAccountId = awsAccountId; } + + public OrchestratorSession(ProjectDefinition projectDefinition) + { + ProjectDefinition = projectDefinition; + } } } diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index f0809b24f..9e15f914f 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Recipes; using Newtonsoft.Json; namespace AWS.Deploy.Orchestration.RecommendationEngine @@ -64,13 +65,43 @@ public async Task> ComputeRecommendations(Dictionary recommendation.ComputedPriority).ToList(); return recommendations; } + public List GetDeploymentBundleSettings(DeploymentBundleTypes deploymentBundleTypes) + { + var deploymentBundleDefinitionsPath = DeploymentBundleDefinitionLocator.FindDeploymentBundleDefinitionPath(); + + try + { + foreach (var deploymentBundleFile in Directory.GetFiles(deploymentBundleDefinitionsPath, "*.deploymentbundle", SearchOption.TopDirectoryOnly)) + { + try + { + var content = File.ReadAllText(deploymentBundleFile); + var definition = JsonConvert.DeserializeObject(content); + if (definition.Type.Equals(deploymentBundleTypes)) + return definition.Parameters; + } + catch (Exception e) + { + throw new Exception($"Failed to Deserialize Deployment Bundle [{deploymentBundleFile}]: {e.Message}", e); + } + } + } + catch(IOException) + { + throw new NoDeploymentBundleDefinitionsFoundException("Failed to find a deployment bundle definition"); + } + + throw new NoDeploymentBundleDefinitionsFoundException("Failed to find a deployment bundle definition"); + } + public async Task EvaluateRules(IList rules) { // If there are no rules the recipe must be invalid so don't include it. diff --git a/src/AWS.Deploy.Orchestration/TemplateEngine.cs b/src/AWS.Deploy.Orchestration/TemplateEngine.cs index e4f87dc6f..b7bb128cc 100644 --- a/src/AWS.Deploy.Orchestration/TemplateEngine.cs +++ b/src/AWS.Deploy.Orchestration/TemplateEngine.cs @@ -31,8 +31,17 @@ public TemplateEngine() _bootstrapper = new Bootstrapper(CreateHost(), null, virtualizeConfiguration: true); } - public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, OrchestratorSession session, string outputDirectory) + public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, OrchestratorSession session, string outputDirectory, string assemblyName) { + if (string.IsNullOrEmpty(recommendation.Recipe.CdkProjectTemplate)) + { + throw new InvalidOperationException($"{nameof(recommendation.Recipe.CdkProjectTemplate)} cannot be null or an empty string"); + } + if (string.IsNullOrEmpty(recommendation.Recipe.CdkProjectTemplateId)) + { + throw new InvalidOperationException($"{nameof(recommendation.Recipe.CdkProjectTemplateId)} cannot be null or an empty string"); + } + //The location of the base template that will be installed into the templating engine var cdkProjectTemplateDirectory = Path.Combine( Path.GetDirectoryName(recommendation.Recipe.RecipePath) ?? @@ -56,9 +65,6 @@ public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, throw new Exception($"Failed to find a Template for [{recommendation.Recipe.CdkProjectTemplateId}]"); var templateParameters = new Dictionary { - { "AWSAccountID" , session.AWSAccountId }, - { "AWSRegion" , session.AWSRegion }, - // CDK Template projects can parameterize the version number of the AWS.Deploy.Recipes.CDK.Common package. This avoid // projects having to be modified every time the package version is bumped. { "AWSDeployRecipesCDKCommonVersion", FileVersionInfo.GetVersionInfo(typeof(Constants.CloudFormationIdentifier).Assembly.Location).ProductVersion @@ -74,7 +80,7 @@ public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, try { //Generate the CDK project using the installed template into the output directory - await _bootstrapper.CreateAsync(template, recommendation.ProjectDefinition.AssemblyName, outputDirectory, templateParameters, false, ""); + await _bootstrapper.CreateAsync(template, assemblyName, outputDirectory, templateParameters, false, ""); } catch { diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs index bbfb1fde5..136718dfd 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeConfiguration.cs @@ -59,12 +59,12 @@ public class RecipeConfiguration /// /// The Region used during deployment. /// - public string AWSRegion { get; set; } + public string? AWSRegion { get; set; } /// /// The account ID used during deployment. /// - public string AWSAccountId { get; set; } + public string? AWSAccountId { get; set; } /// A parameterless constructor is needed for /// or the classes will fail to initialize. @@ -77,7 +77,7 @@ public RecipeConfiguration() #nullable restore warnings public RecipeConfiguration(string stackName, string projectPath, string recipeId, string recipeVersion, - string awsAccountId, string awsRegion, T settings) + string? awsAccountId, string? awsRegion, T settings) { StackName = stackName; ProjectPath = projectPath; diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj index c6c2192c9..55fdf8543 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj @@ -4,6 +4,9 @@ Exe netcoreapp3.1 Major + true + 9 + enable AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj index 93197d8ff..64ac5c694 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkLinux/AspNetAppElasticBeanstalkLinux.csproj @@ -4,6 +4,9 @@ Exe netcoreapp3.1 Major + true + 9 + enable AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj index cb3906e87..8f64be81a 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj @@ -4,6 +4,9 @@ Exe netcoreapp3.1 Major + true + 9 + enable AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj index c6c2192c9..55fdf8543 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateScheduleTask/ConsoleAppECSFargateScheduleTask.csproj @@ -4,6 +4,9 @@ Exe netcoreapp3.1 Major + true + 9 + enable AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj index c6c2192c9..55fdf8543 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/ConsoleAppECSFargateService/ConsoleAppECSFargateService.csproj @@ -4,6 +4,9 @@ Exe netcoreapp3.1 Major + true + 9 + enable AWSDeployRecipesCDKCommonVersion diff --git a/src/AWS.Deploy.ServerMode.Client/Exceptions.cs b/src/AWS.Deploy.ServerMode.Client/Exceptions.cs index bb81482af..5ed799591 100644 --- a/src/AWS.Deploy.ServerMode.Client/Exceptions.cs +++ b/src/AWS.Deploy.ServerMode.Client/Exceptions.cs @@ -24,4 +24,14 @@ public PortUnavailableException(string message) : base(message) { } } + + /// + /// Exception is thrown when there is a mismatch between the deploy tool and the server mode client library certificate author. + /// + public class InvalidAssemblyReferenceException : Exception + { + public InvalidAssemblyReferenceException(string message) : base(message) + { + } + } } diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 6f10d6963..704253b7f 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -66,6 +66,7 @@ public class ServerModeSession : IServerModeSession, IDisposable private readonly CommandLineWrapper _commandLineWrapper; private readonly HttpClientHandler _httpClientHandler; private readonly TimeSpan _serverTimeout; + private readonly string _deployToolPath; private string? _baseUrl; private Aes? _aes; @@ -83,12 +84,13 @@ private string HealthUrl } } - public ServerModeSession(int startPort = 10000, int endPort = 10100, bool diagnosticLoggingEnabled = false) + public ServerModeSession(int startPort = 10000, int endPort = 10100, string deployToolPath = "", bool diagnosticLoggingEnabled = false) : this(new CommandLineWrapper(diagnosticLoggingEnabled), new HttpClientHandler(), TimeSpan.FromSeconds(60), startPort, - endPort) + endPort, + deployToolPath) { } @@ -96,17 +98,28 @@ public ServerModeSession(CommandLineWrapper commandLineWrapper, HttpClientHandler httpClientHandler, TimeSpan serverTimeout, int startPort = 10000, - int endPort = 10100) + int endPort = 10100, + string deployToolPath = "") { _startPort = startPort; _endPort = endPort; _commandLineWrapper = commandLineWrapper; _httpClientHandler = httpClientHandler; _serverTimeout = serverTimeout; + _deployToolPath = deployToolPath; } public async Task Start(CancellationToken cancellationToken) { + var deployToolRoot = "dotnet aws"; + if (!string.IsNullOrEmpty(_deployToolPath)) + { + if (!File.Exists(_deployToolPath)) + throw new InvalidAssemblyReferenceException("The specified assembly location is invalid."); + + deployToolRoot = $"dotnet {_deployToolPath}"; + } + var currentProcessId = Process.GetCurrentProcess().Id; for (var port = _startPort; port <= _endPort; port++) @@ -122,7 +135,7 @@ public async Task Start(CancellationToken cancellationToken) var keyInfoStdin = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(keyInfo))); - var command = $"dotnet aws server-mode --port {port} --parent-pid {currentProcessId} --encryption-keyinfo-stdin"; + var command = $"{deployToolRoot} server-mode --port {port} --parent-pid {currentProcessId} --encryption-keyinfo-stdin"; var startServerTask = _commandLineWrapper.Run(command, keyInfoStdin); _baseUrl = $"http://localhost:{port}"; diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj b/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj index a4d8aede4..df4e13f86 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj +++ b/test/AWS.Deploy.CLI.Common.UnitTests/AWS.Deploy.CLI.Common.UnitTests.csproj @@ -38,4 +38,14 @@ + + + testapps\%(RecursiveDir)/%(FileName)%(Extension) + PreserveNewest + + + + + + diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs new file mode 100644 index 000000000..68c6b9fbc --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; +using Newtonsoft.Json; +using Xunit; +using Should; + +namespace AWS.Deploy.CLI.Common.UnitTests.DeploymentManifestFile +{ + public class DeploymentManifestFileTests : IDisposable + { + private readonly IFileManager _fileManager; + private readonly IDirectoryManager _directoryManager; + private readonly string _targetApplicationFullPath; + private readonly string _targetApplicationDirectoryFullPath; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + + private bool _isDisposed; + + public DeploymentManifestFileTests() + { + _fileManager = new FileManager(); + _directoryManager = new DirectoryManager(); + var targetApplicationPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + _targetApplicationFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).FullName; + _targetApplicationDirectoryFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).Parent.FullName; + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + } + + [Fact] + public async Task Create() + { + // Arrange + var saveCdkDirectoryFullPath = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp"); + var saveCdkDirectoryFullPath2 = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp2"); + var saveCdkDirectoryFullPath3 = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp3"); + + var saveCdkDirectoryRelativePath = Path.GetRelativePath(_targetApplicationFullPath, saveCdkDirectoryFullPath); + var saveCdkDirectoryRelativePath2 = Path.GetRelativePath(_targetApplicationFullPath, saveCdkDirectoryFullPath2); + var saveCdkDirectoryRelativePath3 = Path.GetRelativePath(_targetApplicationFullPath, saveCdkDirectoryFullPath3); + + var deploymentManifestFilePath = Path.Combine(_targetApplicationDirectoryFullPath, "aws-deployments.json"); + + // Act + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, _targetApplicationFullPath); + + // Assert + Assert.True(_fileManager.Exists(deploymentManifestFilePath)); + var deploymentProjectPaths = await GetDeploymentManifestEntries(deploymentManifestFilePath); + Assert.Single(deploymentProjectPaths); + deploymentProjectPaths.ShouldContain(saveCdkDirectoryRelativePath); + + // Update deployment-manifest file + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath2, _targetApplicationFullPath); + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath3, _targetApplicationFullPath); + + // Assert + Assert.True(_fileManager.Exists(deploymentManifestFilePath)); + deploymentProjectPaths = await GetDeploymentManifestEntries(deploymentManifestFilePath); + Assert.Equal(3, deploymentProjectPaths.Count); + deploymentProjectPaths.ShouldContain(saveCdkDirectoryRelativePath); + deploymentProjectPaths.ShouldContain(saveCdkDirectoryRelativePath2); + deploymentProjectPaths.ShouldContain(saveCdkDirectoryRelativePath3); + + // cleanup + File.Delete(deploymentManifestFilePath); + Assert.False(_fileManager.Exists(deploymentManifestFilePath)); + } + + private async Task> GetDeploymentManifestEntries(string deploymentManifestFilePath) + { + var deploymentProjectPaths = new List(); + var manifestFilejsonString = await _fileManager.ReadAllTextAsync(deploymentManifestFilePath); + var deploymentManifestModel = JsonConvert.DeserializeObject(manifestFilejsonString); + + foreach (var entry in deploymentManifestModel.DeploymentManifestEntries) + { + deploymentProjectPaths.Add(entry.SaveCdkDirectoryRelativePath); + } + + return deploymentProjectPaths; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + var deploymentManifestFilePath = Path.Combine(_targetApplicationDirectoryFullPath, "aws-deployments.json"); + if (_fileManager.Exists(deploymentManifestFilePath)) + { + File.Delete(deploymentManifestFilePath); + } + } + + _isDisposed = true; + } + + ~DeploymentManifestFileTests() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs index a4c6b3a17..e8f46657d 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs @@ -18,12 +18,30 @@ public DirectoryInfo CreateDirectory(string path) return new DirectoryInfo(path); } + public void Delete(string path, bool recursive = false) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + public bool Exists(string path) { return CreatedDirectories.Contains(path); } - public string[] GetFiles(string projectPath, string searchPattern = null) => + public string[] GetDirectories(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public DirectoryInfo GetDirectoryInfo(string path) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public string[] GetFiles(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public bool IsEmpty(string path) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + + public string GetRelativePath(string referenceFullPath, string targetFullPath) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs new file mode 100644 index 000000000..ee68c6836 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs @@ -0,0 +1,179 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using AWS.Deploy.CLI.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Services; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject +{ + public class SaveCdkDeploymentProjectTests : IDisposable + { + private readonly App _app; + private readonly InMemoryInteractiveService _interactiveService; + private readonly string _targetApplicationProjectPath; + private readonly string _deploymentManifestFilePath; + + private bool _isDisposed; + private string _saveDirectoryPath; + + public SaveCdkDeploymentProjectTests() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _app = serviceProvider.GetService(); + Assert.NotNull(_app); + + _interactiveService = serviceProvider.GetService(); + Assert.NotNull(_interactiveService); + + _targetApplicationProjectPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); + _deploymentManifestFilePath = Path.Combine("testapps", "WebAppWithDockerFile", "aws-deployments.json"); + } + + [Fact] + public async Task DefaultSaveDirectory() + { + // Arrange input for saving the CDK deployment project + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + + // Generate and save the CDK deployment project + var deployArgs = new[] { "deployment-project", "generate", "--project-path", _targetApplicationProjectPath}; + await _app.Run(deployArgs); + + // Verify project is saved + _saveDirectoryPath = Path.Combine("testapps", "WebAppWithDockerFileCDK"); + var directoryInfo = new DirectoryInfo(_saveDirectoryPath); + var stdOut = _interactiveService.StdOutReader.ReadAllLines(); + var successMessage = $"The CDK deployment project is saved at: {directoryInfo.FullName}"; + + Assert.Contains(stdOut, (line) => successMessage.Equals(line)); + Assert.True(Directory.Exists(_saveDirectoryPath)); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "WebAppWithDockerFileCDK.csproj"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "AppStack.cs"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "Program.cs"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "cdk.json"))); + Assert.True(Directory.EnumerateFiles(_saveDirectoryPath, "*.recipe").Any()); + + // Verify deployment-manifest file is generated + Assert.True(File.Exists(_deploymentManifestFilePath)); + + // Delete generated artifacts + Directory.Delete(_saveDirectoryPath, true); + File.Delete(_deploymentManifestFilePath); + + // Verify artifacts are deleted + Assert.False(Directory.Exists(_saveDirectoryPath)); + Assert.False(File.Exists(_deploymentManifestFilePath)); + } + + [Fact] + public async Task CustomSaveDirectory() + { + // Arrange input for saving the CDK deployment project + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + + // Generate and save the CDK deployment project + _saveDirectoryPath = Path.Combine("DeploymentProjects", "MyCdkApp"); + var deployArgs = new[] { "deployment-project", "generate", "--project-path", _targetApplicationProjectPath, "--output", _saveDirectoryPath }; + var returnCode = await _app.Run(deployArgs); + + // Verify project is saved + var directoryInfo = new DirectoryInfo(_saveDirectoryPath); + var stdOut = _interactiveService.StdOutReader.ReadAllLines(); + var successMessage = $"The CDK deployment project is saved at: {directoryInfo.FullName}"; + + Assert.Equal(CommandReturnCodes.SUCCESS, returnCode); + Assert.Contains(stdOut, (line) => successMessage.Equals(line)); + Assert.True(Directory.Exists(_saveDirectoryPath)); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "MyCdkApp.csproj"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "AppStack.cs"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "Program.cs"))); + Assert.True(File.Exists(Path.Combine(_saveDirectoryPath, "cdk.json"))); + Assert.True(Directory.EnumerateFiles(_saveDirectoryPath, "*.recipe").Any()); + + // Verify deployment-manifest file is generated + Assert.True(File.Exists(_deploymentManifestFilePath)); + + // Delete generated artifacts + Directory.Delete(_saveDirectoryPath, true); + File.Delete(_deploymentManifestFilePath); + + // Verify artifacts are deleted + Assert.False(Directory.Exists(_saveDirectoryPath)); + Assert.False(File.Exists(_deploymentManifestFilePath)); + } + + [Fact] + public async Task InvalidSaveCdkDirectoryInsideProjectDirectory() + { + // Arrange input for saving the CDK deployment project + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + + // Generate and save the CDK deployment project + _saveDirectoryPath = Path.Combine("testapps", "WebAppWithDockerFile", "MyCdkApp"); + var deployArgs = new[] { "deployment-project", "generate", "--project-path", _targetApplicationProjectPath, "--output", _saveDirectoryPath }; + var returnCode = await _app.Run(deployArgs); + + Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); + Assert.False(Directory.Exists(_saveDirectoryPath)); + } + + [Fact] + public async Task InvalidNonEmptySaveCdkDirectory() + { + // Arrange input for saving the CDK deployment project + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + + // create a non-empty directory inside which we intend to save the CDK deployment project. + Directory.CreateDirectory(Path.Combine("DeploymentProjects", "MyCdkApp")); + + // Generate and save the CDK deployment project + _saveDirectoryPath = "DeploymentProjects"; + var deployArgs = new[] { "deployment-project", "generate", "--project-path", _targetApplicationProjectPath, "--output", _saveDirectoryPath }; + var returnCode = await _app.Run(deployArgs); + + Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); + + Directory.Delete("DeploymentProjects", true); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + if (Directory.Exists(_saveDirectoryPath)) + Directory.Delete(_saveDirectoryPath, true); + + if (File.Exists(_deploymentManifestFilePath)) + File.Delete(_deploymentManifestFilePath); + } + + _isDisposed = true; + } + + ~SaveCdkDeploymentProjectTests() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index 6370e3629..d1f54d916 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -67,7 +67,7 @@ public async Task ApplyApplicationIAMRolePreviousSettings(bool createNew, string var settings = JsonConvert.DeserializeObject>(serializedSettings); - beanstalkRecommendation.ApplyPreviousSettings(settings); + beanstalkRecommendation = beanstalkRecommendation.ApplyPreviousSettings(settings); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); var typeHintResponse = beanstalkRecommendation.GetOptionSettingValue(applicationIAMRoleOptionSetting); @@ -101,7 +101,7 @@ public async Task ApplyVpcPreviousSettings(bool isDefault, bool createNew, strin var settings = JsonConvert.DeserializeObject>(serializedSettings); - fargateRecommendation.ApplyPreviousSettings(settings); + fargateRecommendation = fargateRecommendation.ApplyPreviousSettings(settings); var vpcOptionSetting = fargateRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("Vpc")); diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 98ea1cff0..3a4a01a66 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -58,7 +58,7 @@ public async Task BuildDockerImage_DockerExecutionDirectoryNotSet() var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty); var result = await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation); @@ -77,7 +77,7 @@ public async Task BuildDockerImage_DockerExecutionDirectorySet() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); recommendation.DeploymentBundle.DockerExecutionDirectory = projectPath; @@ -97,7 +97,7 @@ public async Task PushDockerImage_RepositoryNameCheck() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty); await _deploymentBundleHandler.PushDockerImageToECR(cloudApplication, recommendation, "ConsoleAppTask:latest"); @@ -110,7 +110,7 @@ public async Task CreateDotnetPublishZip_NotSelfContained() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = false; recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = "Release"; @@ -133,7 +133,7 @@ public async Task CreateDotnetPublishZip_SelfContained() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = true; recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = "Release"; diff --git a/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs b/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs new file mode 100644 index 000000000..a1f34a8eb --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/ProjectDefinitionParserTest.cs @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; +using Xunit; +using Should; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class ProjectDefinitionParserTest + { + [Theory] + [InlineData("WebAppWithDockerFile", "WebAppWithDockerFile.csproj")] + [InlineData("WebAppNoDockerFile", "WebAppNoDockerFile.csproj")] + [InlineData("ConsoleAppTask", "ConsoleAppTask.csproj")] + [InlineData("ConsoleAppService", "ConsoleAppService.csproj")] + [InlineData("MessageProcessingApp", "MessageProcessingApp.csproj")] + [InlineData("ContosoUniversityBackendService", "ContosoUniversityBackendService.csproj")] + [InlineData("ContosoUniversityWeb", "ContosoUniversity.csproj")] + [InlineData("BlazorWasm31", "BlazorWasm31.csproj")] + [InlineData("BlazorWasm50", "BlazorWasm50.csproj")] + public async Task ParseProjectDefinitionWithRelativeProjectPath(string projectName, string csprojName) + { + //Arrange + var currrentWorkingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var projectDirectoryPath = SystemIOUtilities.ResolvePath(projectName); + var absoluteProjectDirectoryPath = new DirectoryInfo(projectDirectoryPath).FullName; + var absoluteProjectPath = Path.Combine(absoluteProjectDirectoryPath, csprojName); + var relativeProjectDirectoryPath = Path.GetRelativePath(currrentWorkingDirectory, absoluteProjectDirectoryPath); + + // Act + var projectDefinition = await new ProjectDefinitionParser(new FileManager(), new DirectoryManager()).Parse(relativeProjectDirectoryPath); + + // Assert + projectDefinition.ShouldNotBeNull(); + Assert.Equal(absoluteProjectPath, projectDefinition.ProjectPath); + } + + [Theory] + [InlineData("WebAppWithDockerFile", "WebAppWithDockerFile.csproj")] + [InlineData("WebAppNoDockerFile", "WebAppNoDockerFile.csproj")] + [InlineData("ConsoleAppTask", "ConsoleAppTask.csproj")] + [InlineData("ConsoleAppService", "ConsoleAppService.csproj")] + [InlineData("MessageProcessingApp", "MessageProcessingApp.csproj")] + [InlineData("ContosoUniversityBackendService", "ContosoUniversityBackendService.csproj")] + [InlineData("ContosoUniversityWeb", "ContosoUniversity.csproj")] + [InlineData("BlazorWasm31", "BlazorWasm31.csproj")] + [InlineData("BlazorWasm50", "BlazorWasm50.csproj")] + public async Task ParseProjectDefinitionWithAbsoluteProjectPath(string projectName, string csprojName) + { + //Arrange + var projectDirectoryPath = SystemIOUtilities.ResolvePath(projectName); + var absoluteProjectDirectoryPath = new DirectoryInfo(projectDirectoryPath).FullName; + var absoluteProjectPath = Path.Combine(absoluteProjectDirectoryPath, csprojName); + + // Act + var projectDefinition = await new ProjectDefinitionParser(new FileManager(), new DirectoryManager()).Parse(absoluteProjectDirectoryPath); + + // Assert + projectDefinition.ShouldNotBeNull(); + Assert.Equal(absoluteProjectPath, projectDefinition.ProjectPath); + } + } +} + diff --git a/version.json b/version.json index 866f49c19..89dfef907 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.12", + "version": "0.13", "publicReleaseRefSpec": [ ".*" ],