From ca9198ece989639211de7bdf344e1d1225aac501 Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Wed, 4 Aug 2021 16:38:43 +0000 Subject: [PATCH 1/3] build: version bump to 0.15 --- version.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/version.json b/version.json index ecaec4479..567425880 100644 --- a/version.json +++ b/version.json @@ -1,13 +1,13 @@ -{ - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.14", - "publicReleaseRefSpec": [ - ".*" - ], - "cloudBuild": { - "setAllVariables": true, - "buildNumber": { - "enabled": true - } - } -} +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.15", + "publicReleaseRefSpec": [ + ".*" + ], + "cloudBuild": { + "setAllVariables": true, + "buildNumber": { + "enabled": true + } + } +} From b34a10786d58322de38662b8b2980fe825ebd44e Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Thu, 5 Aug 2021 13:53:41 -0400 Subject: [PATCH 2/3] feat: List saved CDK projects as deployment options. --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 34 ++- .../DeployCommandHandlerInput.cs | 1 + ...ateDeploymentProjectCommandHandlerInput.cs | 4 +- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 140 ++++++--- .../GenerateDeploymentProjectCommand.cs | 17 +- .../CustomServiceCollectionExtension.cs | 1 + .../Controllers/DeploymentController.cs | 4 +- .../DeploymentManifestEngine.cs | 41 ++- src/AWS.Deploy.Common/IO/DirectoryManager.cs | 2 + .../Recipes/OptionSettingItem.cs | 3 + .../Recipes/RecipeDefinition.cs | 9 + .../SerializeRecipeContractResolver.cs | 30 ++ .../CdkProjectHandler.cs | 18 +- .../CustomRecipeLocator.cs | 185 ++++++++++++ src/AWS.Deploy.Orchestration/Orchestrator.cs | 47 +++- .../RecommendationEngine.cs | 9 +- .../TemplateEngine.cs | 6 - .../DeploymentManifestFileTests.cs | 50 +++- .../IO/TestDirectoryManager.cs | 12 +- .../CustomRecipeLocatorTests.cs | 127 +++++++++ .../RecommendationTests.cs | 265 ++++++++++++++++++ .../SaveCdkDeploymentProjectTests.cs | 138 ++------- .../SaveCdkDeploymentProject/Utilities.cs | 139 +++++++++ .../Utilities/TestToolAWSResourceQueryer.cs | 48 ++++ .../Utilities/TestToolAWSResourceQueryer.cs | 6 +- 25 files changed, 1151 insertions(+), 185 deletions(-) create mode 100644 src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs create mode 100644 src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index dcfc6e53b..de5f6e392 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -5,6 +5,7 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.IO; +using System.Collections.Generic; using Amazon; using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Utilities; @@ -37,6 +38,7 @@ public class CommandFactory : ICommandFactory 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 static readonly Option _optionDeploymentProject = new(new[] { "--deployment-project" }, "The absolute or relative path of the CDK project that will be used for deployment"); private readonly IToolInteractiveService _toolInteractiveService; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; @@ -56,6 +58,7 @@ public class CommandFactory : ICommandFactory private readonly IDisplayedResourcesHandler _displayedResourceHandler; private readonly IConsoleUtilities _consoleUtilities; private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly ICustomRecipeLocator _customRecipeLocator; public CommandFactory( IToolInteractiveService toolInteractiveService, @@ -75,7 +78,8 @@ public CommandFactory( ITypeHintCommandFactory typeHintCommandFactory, IDisplayedResourcesHandler displayedResourceHandler, IConsoleUtilities consoleUtilities, - IDeploymentManifestEngine deploymentManifestEngine) + IDeploymentManifestEngine deploymentManifestEngine, + ICustomRecipeLocator customRecipeLocator) { _toolInteractiveService = toolInteractiveService; _orchestratorInteractiveService = orchestratorInteractiveService; @@ -95,6 +99,7 @@ public CommandFactory( _displayedResourceHandler = displayedResourceHandler; _consoleUtilities = consoleUtilities; _deploymentManifestEngine = deploymentManifestEngine; + _customRecipeLocator = customRecipeLocator; } public Command BuildRootCommand() @@ -127,7 +132,8 @@ private Command BuildDeployCommand() _optionStackName, _optionApply, _optionDiagnosticLogging, - _optionDisableInteractive + _optionDisableInteractive, + _optionDeploymentProject }; deployCommand.Handler = CommandHandler.Create(async (DeployCommandHandlerInput input) => @@ -150,7 +156,7 @@ private Command BuildDeployCommand() var systemCapabilities = _systemCapabilityEvaluator.Evaluate(); var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? ""); - + var callerIdentity = await _awsResourceQueryer.GetCallerIdentity(); var session = new OrchestratorSession( @@ -179,9 +185,17 @@ private Command BuildDeployCommand() _displayedResourceHandler, _cloudApplicationNameGenerator, _consoleUtilities, + _customRecipeLocator, session); - await deploy.ExecuteAsync(input.StackName ?? "", userDeploymentSettings); + var deploymentProjectPath = input.DeploymentProject ?? string.Empty; + if (!string.IsNullOrEmpty(deploymentProjectPath)) + { + var targetApplicationDirectoryPath = new DirectoryInfo(projectDefinition.ProjectPath).Parent!.FullName; + deploymentProjectPath = Path.GetFullPath(deploymentProjectPath, targetApplicationDirectoryPath); + } + + await deploy.ExecuteAsync(input.StackName ?? "", deploymentProjectPath, userDeploymentSettings); return CommandReturnCodes.SUCCESS; } @@ -346,12 +360,18 @@ private Command BuildDeploymentProjectCommand() _toolInteractiveService.Diagnostics = input.Diagnostics; var projectDefinition = await _projectParserUtility.Parse(input.ProjectPath ?? ""); - var saveDirectory = input.Output ?? ""; - var projectDisplayName = input.ProjectDisplayName ?? ""; + var saveDirectory = input.Output; + var projectDisplayName = input.ProjectDisplayName; OrchestratorSession session = new OrchestratorSession(projectDefinition); - var targetApplicationFullPath = new DirectoryManager().GetDirectoryInfo(projectDefinition.ProjectPath).FullName; + 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, diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs index 6a1557a91..7e65326cd 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs @@ -17,5 +17,6 @@ public class DeployCommandHandlerInput public string? Apply { get; set; } public bool Diagnostics { get; set; } public bool Silent { get; set; } + public string? DeploymentProject { get; set; } } } diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs index cb0736298..712601fcc 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/GenerateDeploymentProjectCommandHandlerInput.cs @@ -10,7 +10,7 @@ public class GenerateDeploymentProjectCommandHandlerInput { public string? ProjectPath { get; set; } public bool Diagnostics { get; set; } - public string? Output { get; set; } - public string? ProjectDisplayName { get; set; } + public string Output { get; set; } = string.Empty; + public string ProjectDisplayName { get; set; } = string.Empty; } } diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index ef8b79c7e..5121bd3eb 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -35,8 +35,9 @@ public class DeployCommand private readonly ITypeHintCommandFactory _typeHintCommandFactory; private readonly IDisplayedResourcesHandler _displayedResourcesHandler; private readonly ICloudApplicationNameGenerator _cloudApplicationNameGenerator; - private readonly IConsoleUtilities _consoleUtilities; + private readonly ICustomRecipeLocator _customRecipeLocator; + private readonly OrchestratorSession _session; public DeployCommand( @@ -53,6 +54,7 @@ public DeployCommand( IDisplayedResourcesHandler displayedResourcesHandler, ICloudApplicationNameGenerator cloudApplicationNameGenerator, IConsoleUtilities consoleUtilities, + ICustomRecipeLocator customRecipeLocator, OrchestratorSession session) { _toolInteractiveService = toolInteractiveService; @@ -69,11 +71,12 @@ public DeployCommand( _consoleUtilities = consoleUtilities; _session = session; _cdkManager = cdkManager; + _customRecipeLocator = customRecipeLocator; } - public async Task ExecuteAsync(string stackName, UserDeploymentSettings? userDeploymentSettings = null) + public async Task ExecuteAsync(string stackName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) { - var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(stackName, userDeploymentSettings); + var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(stackName, userDeploymentSettings, deploymentProjectPath); // Verify Docker installation and minimum NodeJS version. await EvaluateSystemCapabilities(_session, selectedRecommendation); @@ -117,8 +120,9 @@ private void DisplayOutputResources(List displayedResourc /// /// The stack name provided via the --stack-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 stackName, UserDeploymentSettings? userDeploymentSettings) + public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string stackName, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) { var orchestrator = new Orchestrator( _session, @@ -128,32 +132,71 @@ private void DisplayOutputResources(List displayedResourc _awsResourceQueryer, _deploymentBundleHandler, _dockerEngine, - new[] { RecipeLocator.FindRecipeDefinitionsPath() }); + _customRecipeLocator, + new List { RecipeLocator.FindRecipeDefinitionsPath() }); // Determine what recommendations are possible for the project. - var recommendations = await GenerateDeploymentRecommendations(orchestrator); + var recommendations = await GenerateDeploymentRecommendations(orchestrator, deploymentProjectPath); + + // Get all existing applications that were previously deployed using our deploy tool. + var allDeployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(); - // Look to see if there are any existing deployed applications using any of the compatible recommendations. - var deployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(recommendations); + // Filter compatible applications that can be re-deployed using the current set of recommendations. + var compatibleApplications = GetCompatibleApplications(allDeployedApplications, recommendations); // Get Cloudformation stack name. - var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, deployedApplications); + var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, compatibleApplications); // Find existing application with the same CloudFormation stack name. - var deployedApplication = deployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName)); + var deployedApplication = allDeployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName)); 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.StackName.Equals(deployedApplication.StackName, StringComparison.Ordinal))) + { + var errorMessage = $"{deployedApplication.StackName} already exists as a Cloudformation stack but a compatible recommendation to perform a redeployment was no found"; + throw new FailedToFindCompatibleRecipeException(errorMessage); + } + // preset settings for deployment based on last deployment. selectedRecommendation = await GetSelectedRecommendationFromPreviousDeployment(recommendations, deployedApplication, userDeploymentSettings); + } else - selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); - + { + if (!string.IsNullOrEmpty(deploymentProjectPath)) + { + selectedRecommendation = recommendations.First(); + } + else + { + selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); + } + } + var cloudApplication = new CloudApplication(cloudApplicationName, selectedRecommendation.Recipe.Id); return (orchestrator, selectedRecommendation, cloudApplication); } + /// + /// Filters the applications that can be re-deployed using the current set of available recommendations. + /// + /// + /// + /// A list of that are compatible for a re-deployment + private List GetCompatibleApplications(List allDeployedApplications, List recommendations) + { + var compatibleApplications = new List(); + foreach (var app in allDeployedApplications) + { + if (recommendations.Any(rec => string.Equals(rec.Recipe.Id, app.RecipeId, StringComparison.Ordinal))) + compatibleApplications.Add(app); + } + return compatibleApplications; + } + /// /// Checks if the system meets all the necessary requirements for deployment. /// @@ -210,12 +253,26 @@ public async Task ConfigureDeployment(CloudApplication cloudApplication, Orchest } } - private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator) + private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator, string deploymentProjectPath) { - var recommendations = await orchestrator.GenerateDeploymentRecommendations(); - if (recommendations.Count == 0) + List recommendations; + if (!string.IsNullOrEmpty(deploymentProjectPath)) { - throw new FailedToGenerateAnyRecommendations("The project you are trying to deploy is currently not supported."); + 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(errorMessage); + } + } + else + { + recommendations = await orchestrator.GenerateDeploymentRecommendations(); + if (!recommendations.Any()) + { + var errorMessage = "There are no compatible deployment recommendations for this application."; + throw new FailedToGenerateAnyRecommendations(errorMessage); + } } return recommendations; } @@ -223,19 +280,25 @@ private async Task> GenerateDeploymentRecommendations(Orche private async Task GetSelectedRecommendationFromPreviousDeployment(List recommendations, CloudApplication deployedApplication, UserDeploymentSettings? userDeploymentSettings) { var existingCloudApplicationMetadata = await _templateMetadataReader.LoadCloudApplicationMetadata(deployedApplication.Name); - + var deploymentSettingRecipeId = userDeploymentSettings?.RecipeId; var selectedRecommendation = recommendations.FirstOrDefault(x => string.Equals(x.Recipe.Id, deployedApplication.RecipeId, StringComparison.InvariantCultureIgnoreCase)); if (selectedRecommendation == null) - throw new FailedToFindCompatibleRecipeException("A compatible recipe was not found for the deployed application."); - - if (userDeploymentSettings != null && !string.IsNullOrEmpty(userDeploymentSettings.RecipeId)) { - if (!string.Equals(userDeploymentSettings.RecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase)) - throw new InvalidUserDeploymentSettingsException("The recipe ID specified as part of the deployment configuration file" + - " does not match the original recipe used to deploy the application stack."); + var errorMessage = $"{deployedApplication.StackName} already exists as a Cloudformation stack but the recommendation used to deploy to the stack was not found."; + throw new FailedToFindCompatibleRecipeException(errorMessage); } - + if (!string.IsNullOrEmpty(deploymentSettingRecipeId) && !string.Equals(deploymentSettingRecipeId, selectedRecommendation.Recipe.Id, StringComparison.InvariantCultureIgnoreCase)) + { + var errorMessage = $"The existing stack {deployedApplication.StackName} was created from a different deployment recommendation. " + + "Deploying to an existing stack must be performed with the original deployment recommendation to avoid unintended destructive changes to the stack."; + if (_toolInteractiveService.Diagnostics) + { + errorMessage += Environment.NewLine + $"The original deployment recipe ID was {deployedApplication.RecipeId} and the current deployment recipe ID is {deploymentSettingRecipeId}"; + } + throw new InvalidUserDeploymentSettingsException(errorMessage.Trim()); + } + selectedRecommendation = selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); var header = $"Loading {deployedApplication.Name} settings:"; @@ -370,7 +433,7 @@ private string GetCloudApplicationName(string? stackName, UserDeploymentSettings throw new InvalidCliArgumentException("Found invalid CLI arguments"); } - if (userDeploymentSettings != null && !string.IsNullOrEmpty(userDeploymentSettings.StackName)) + if (!string.IsNullOrEmpty(userDeploymentSettings?.StackName)) { if (_cloudApplicationNameGenerator.IsValidName(userDeploymentSettings.StackName)) return userDeploymentSettings.StackName; @@ -388,26 +451,37 @@ private string GetCloudApplicationName(string? stackName, UserDeploymentSettings return AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); } + /// + /// 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(UserDeploymentSettings? userDeploymentSettings, List recommendations) { - if (userDeploymentSettings == null || string.IsNullOrEmpty(userDeploymentSettings.RecipeId)) + var deploymentSettingsRecipeId = userDeploymentSettings?.RecipeId; + + 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. " + - "Please provide a deployment recipe and try again"; + 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"; + throw new InvalidCliArgumentException(message); } return _consoleUtilities.AskToChooseRecommendation(recommendations); } - - Recommendation? selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(userDeploymentSettings.RecipeId)); - + + var selectedRecommendation = recommendations.FirstOrDefault(x => x.Recipe.Id.Equals(deploymentSettingsRecipeId, StringComparison.Ordinal)); if (selectedRecommendation == null) + { throw new InvalidUserDeploymentSettingsException($"The user deployment settings provided contains an invalid value for the property '{nameof(userDeploymentSettings.RecipeId)}'."); - + } + _toolInteractiveService.WriteLine(); - _toolInteractiveService.WriteLine($"Configuring Recommendation with specified value '{selectedRecommendation.Name}'."); + _toolInteractiveService.WriteLine($"Configuring Recommendation with: '{selectedRecommendation.Name}'."); return selectedRecommendation; } diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs index 413c90124..f03151b5a 100644 --- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -22,6 +22,8 @@ namespace AWS.Deploy.CLI.Commands /// public class GenerateDeploymentProjectCommand { + private const int DEFAULT_PERSISTED_RECIPE_PRIORITY = 1000; + private readonly IToolInteractiveService _toolInteractiveService; private readonly IConsoleUtilities _consoleUtilities; private readonly ICdkProjectHandler _cdkProjectHandler; @@ -64,7 +66,7 @@ public GenerateDeploymentProjectCommand( public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDisplayName) { var orchestrator = new Orchestrator(_session, new[] { RecipeLocator.FindRecipeDefinitionsPath() }); - var recommendations = await GenerateDeploymentRecommendations(orchestrator); + var recommendations = await GenerateRecommendationsToSaveDeploymentProject(orchestrator); var selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations); if (string.IsNullOrEmpty(saveCdkDirectoryPath)) @@ -115,9 +117,9 @@ public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDispla /// /// /// A list of - private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator) + private async Task> GenerateRecommendationsToSaveDeploymentProject(Orchestrator orchestrator) { - var recommendations = await orchestrator.GenerateDeploymentRecommendations(forDeployment: false); + var recommendations = await orchestrator.GenerateRecommendationsToSaveDeploymentProject(); if (recommendations.Count == 0) { throw new FailedToGenerateAnyRecommendations("The project you are trying to deploy is currently not supported."); @@ -230,8 +232,15 @@ private async Task GenerateDeploymentRecipeSnapShot(Recommendation recommendatio recipe.Name = recipeName; recipe.CdkProjectTemplateId = null; recipe.CdkProjectTemplate = null; + recipe.PersistedDeploymentProject = true; + recipe.RecipePriority = DEFAULT_PERSISTED_RECIPE_PRIORITY; - var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, Formatting.Indented); + var recipeSnapshotBody = JsonConvert.SerializeObject(recipe, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new SerializeRecipeContractResolver() + }); await _fileManager.WriteAllTextAsync(recipeSnapshotFilePath, recipeSnapshotBody); } } diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index f771e6826..e8e95fbfb 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -53,6 +53,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDisplayedResourcesHandler), typeof(DisplayedResourcesHandler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IZipFileManager), typeof(ZipFileManager), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentManifestEngine), typeof(DeploymentManifestEngine), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICustomRecipeLocator), typeof(CustomRecipeLocator), 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 583e63837..8436ccf0d 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -5,8 +5,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; - -using Amazon.Runtime.CredentialManagement; using Microsoft.Extensions.DependencyInjection; using System.Threading.Tasks; using Amazon; @@ -19,7 +17,6 @@ using AWS.Deploy.CLI.ServerMode.Models; using AWS.Deploy.CLI.ServerMode.Services; using AWS.Deploy.Orchestration; - using Swashbuckle.AspNetCore.Annotations; using AWS.Deploy.CLI.ServerMode.Hubs; using Microsoft.AspNetCore.SignalR; @@ -459,6 +456,7 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), new DockerEngine.DockerEngine(session.ProjectDefinition), + serviceProvider.GetRequiredService(), new List { RecipeLocator.FindRecipeDefinitionsPath() } ); } diff --git a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs index eaa6aaac0..e0eaccb1e 100644 --- a/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs +++ b/src/AWS.Deploy.Common/DeploymentManifest/DeploymentManifestEngine.cs @@ -13,6 +13,7 @@ namespace AWS.Deploy.Common.DeploymentManifest public interface IDeploymentManifestEngine { Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, string targetApplicationFullPath); + Task> GetRecipeDefinitionPaths(string targetApplicationFullPath); } /// @@ -44,8 +45,12 @@ public async Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, { try { + if (!_directoryManager.Exists(saveCdkDirectoryFullPath)) + return; + var deploymentManifestFilePath = GetDeploymentManifestFilePath(targetApplicationFullPath); - var saveCdkDirectoryRelativePath = _directoryManager.GetRelativePath(targetApplicationFullPath, saveCdkDirectoryFullPath); + var targetApplicationDirectoryPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; + var saveCdkDirectoryRelativePath = _directoryManager.GetRelativePath(targetApplicationDirectoryPath, saveCdkDirectoryFullPath); DeploymentManifestModel deploymentManifestModel; @@ -68,7 +73,37 @@ public async Task UpdateDeploymentManifestFile(string saveCdkDirectoryFullPath, throw new FailedToUpdateDeploymentManifestFileException($"Failed to update the deployment manifest file " + $"for the deployment project stored at '{saveCdkDirectoryFullPath}'", ex); } - + } + + /// + /// This method deserializes the deployment-manifest file and returns a list of absolute paths of directories at which different CDK + /// deployment projects are stored. The custom recipe snapshots are stored in this directory. + /// The absolute path to the target application csproj or fsproj file + /// + /// A list containing absolute directory paths for CDK deployment projects. + public async Task> GetRecipeDefinitionPaths(string targetApplicationFullPath) + { + var recipeDefinitionPaths = new List(); + var deploymentManifestFilePath = GetDeploymentManifestFilePath(targetApplicationFullPath); + var targetApplicationDirectoryPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; + + if (_fileManager.Exists(deploymentManifestFilePath)) + { + var deploymentManifestModel = await ReadManifestFile(deploymentManifestFilePath); + foreach (var entry in deploymentManifestModel.DeploymentManifestEntries) + { + var saveCdkDirectoryRelativePath = entry.SaveCdkDirectoryRelativePath; + if (string.IsNullOrEmpty(saveCdkDirectoryRelativePath)) + continue; + + var saveCdkDirectoryAbsolutePath = _directoryManager.GetAbsolutePath(targetApplicationDirectoryPath, saveCdkDirectoryRelativePath); + + if (_directoryManager.Exists(saveCdkDirectoryAbsolutePath)) + recipeDefinitionPaths.Add(saveCdkDirectoryAbsolutePath); + } + } + + return recipeDefinitionPaths; } /// @@ -103,5 +138,7 @@ private string GetDeploymentManifestFilePath(string targetApplicationFullPath) var deploymentManifestFileFullPath = Path.Combine(projectDirectoryFullPath, DEPLOYMENT_MANIFEST_FILE_NAME); return deploymentManifestFileFullPath; } + + } } diff --git a/src/AWS.Deploy.Common/IO/DirectoryManager.cs b/src/AWS.Deploy.Common/IO/DirectoryManager.cs index 498978656..f8fdd7fda 100644 --- a/src/AWS.Deploy.Common/IO/DirectoryManager.cs +++ b/src/AWS.Deploy.Common/IO/DirectoryManager.cs @@ -17,6 +17,7 @@ public interface IDirectoryManager bool ExistsInsideDirectory(string parentDirectoryPath, string childPath); void Delete(string path, bool recursive = false); string GetRelativePath(string referenceFullPath, string targetFullPath); + string GetAbsolutePath(string referenceFullPath, string targetRelativePath); } public class DirectoryManager : IDirectoryManager @@ -45,5 +46,6 @@ public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) public void Delete(string path, bool recursive = false) => Directory.Delete(path, recursive); public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); + public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index c872b6dc5..75341f818 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -6,6 +6,7 @@ using System.Linq; using AWS.Deploy.Common.Recipes.Validation; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace AWS.Deploy.Common.Recipes { @@ -39,12 +40,14 @@ public partial class OptionSettingItem /// The type of primitive value expected for this setting. /// For example String, Int /// + [JsonConverter(typeof(StringEnumConverter))] public OptionSettingValueType Type { get; set; } /// /// Hint the the UI what type of setting this is optionally add additional UI features to select a value. /// For example a value of BeanstalkApplication tells the UI it can display the list of available Beanstalk applications for the user to pick from. /// + [JsonConverter(typeof(StringEnumConverter))] public OptionSettingTypeHint? TypeHint { get; set; } /// diff --git a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs index 48ab60316..873449b49 100644 --- a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs +++ b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using AWS.Deploy.Common.Recipes.Validation; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace AWS.Deploy.Common.Recipes { @@ -62,11 +64,13 @@ public class RecipeDefinition /// The type of deployment to perform. This controls what other tool to use to perform the deployment. For example a value of `CdkProject` means that CDK should /// be used to perform the deployment. /// + [JsonConverter(typeof(StringEnumConverter))] public DeploymentTypes DeploymentType { get; set; } /// /// The type of deployment bundle the project should be converted into before deploying. For example turning the project into a build container or a zip file of the build binaries. /// + [JsonConverter(typeof(StringEnumConverter))] public DeploymentBundleTypes DeploymentBundle { get; set; } /// @@ -99,6 +103,11 @@ public class RecipeDefinition /// public int RecipePriority { get; set; } + /// + /// Flag to indicate if this recipe is generated while saving a deployment project. + /// + public bool PersistedDeploymentProject { get; set; } + public RecipeDefinition( string id, string version, diff --git a/src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs b/src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs new file mode 100644 index 000000000..b93359986 --- /dev/null +++ b/src/AWS.Deploy.Common/SerializeRecipeContractResolver.cs @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace AWS.Deploy.Common +{ + public class SerializeRecipeContractResolver : DefaultContractResolver + { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + if (property != null && property.PropertyType != null && property.PropertyName != null && property.PropertyType != typeof(string)) + { + if (property.PropertyType.GetInterface(nameof(IEnumerable)) != null) + property.ShouldSerialize = + instance => (instance?.GetType()?.GetProperty(property.PropertyName)?.GetValue(instance) as IEnumerable)?.Any() ?? false; + } + return property ?? throw new ArgumentException(); + } + } +} diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 80fa912fc..5a0a1b67d 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -40,10 +40,22 @@ public async Task CreateCdkDeployment(OrchestratorSession session, CloudApplicat { EnvironmentVariableKeys.AWS_EXECUTION_ENV, recipeInfo } }; - // Create a new temporary CDK project for a new deployment - _interactiveService.LogMessageLine($"Generating a {recommendation.Recipe.Name} CDK Project"); - var cdkProjectPath = await CreateCdkProjectForDeployment(recommendation, session); + string? cdkProjectPath; + if (recommendation.Recipe.PersistedDeploymentProject) + { + if (string.IsNullOrEmpty(recommendation.Recipe.RecipePath)) + throw new InvalidOperationException($"{nameof(recommendation.Recipe.RecipePath)} cannot be null"); + // The CDK deployment project is already saved in the same directory. + cdkProjectPath = _directoryManager.GetDirectoryInfo(recommendation.Recipe.RecipePath).Parent.FullName; + } + else + { + // Create a new temporary CDK project for a new deployment + _interactiveService.LogMessageLine($"Generating a {recommendation.Recipe.Name} CDK Project"); + cdkProjectPath = await CreateCdkProjectForDeployment(recommendation, session); + } + // Write required configuration in appsettings.json var appSettingsBody = _appSettingsBuilder.Build(cloudApplication, recommendation, session); var appSettingsFilePath = Path.Combine(cdkProjectPath, "appsettings.json"); diff --git a/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs b/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs new file mode 100644 index 000000000..89461944f --- /dev/null +++ b/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs @@ -0,0 +1,185 @@ +// 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.Linq; +using System.Threading.Tasks; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.Utilities; + +namespace AWS.Deploy.Orchestration +{ + public interface ICustomRecipeLocator + { + Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath); + } + + /// + /// This class supports the functionality to fetch custom recipe paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + public class CustomRecipeLocator : ICustomRecipeLocator + { + private const string GIT_STATUS_COMMAND = "git status"; + private const string SVN_STATUS_COMMAND = "svn status"; + + private readonly string _ignorePathSubstring = Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly ICommandLineWrapper _commandLineWrapper; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IDirectoryManager _directoryManager; + + public CustomRecipeLocator(IDeploymentManifestEngine deploymentManifestEngine, IOrchestratorInteractiveService orchestratorInteractiveService, + ICommandLineWrapper commandLineWrapper, IDirectoryManager directoryManager) + { + _orchestratorInteractiveService = orchestratorInteractiveService; + _commandLineWrapper = commandLineWrapper; + _deploymentManifestEngine = deploymentManifestEngine; + _directoryManager = directoryManager; + } + + /// + /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + /// The absolute path to the csproj or fsproj file of the target application + /// The absolute path of the directory which contains the solution file for the target application + /// A containing absolute paths of directories inside which the custom recipe snapshot is stored + public async Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) + { + var customRecipePaths = new HashSet(); + + foreach (var recipePath in await LocateRecipePathsFromManifestFile(targetApplicationFullPath)) + { + if (ContainsRecipeFile(recipePath)) + customRecipePaths.Add(recipePath); + } + + foreach (var recipePath in await LocateAlternateRecipePaths(targetApplicationFullPath, solutionDirectoryPath)) + { + if (ContainsRecipeFile(recipePath)) + customRecipePaths.Add(recipePath); + } + + return customRecipePaths; + } + + /// + /// Fetches recipe definition paths by parsing the deployment-manifest file that is associated with the target application. + /// + /// The absolute path to the target application csproj or fsproj file + /// A list containing absolute paths to the saved CDK deployment projects + private async Task> LocateRecipePathsFromManifestFile(string targetApplicationFullPath) + { + try + { + return await _deploymentManifestEngine.GetRecipeDefinitionPaths(targetApplicationFullPath); + } + catch + { + _orchestratorInteractiveService.LogMessageLine(Environment.NewLine); + _orchestratorInteractiveService.LogErrorMessageLine("Failed to load custom deployment recommendations " + + "from the deployment-manifest file due to an error while trying to deserialze the file."); + return await Task.FromResult(new List()); + } + } + + /// + /// Fetches custom recipe paths from other locations that are monitored by the same source control root + /// as the target application that needs to be deployed. + /// If the target application is not under source control then it scans the sub-directories of the solution folder for custom recipes. + /// + /// The absolute path to the target application csproj or fsproj file + /// The absolute path of the directory which contains the solution file for the target application + /// A list of recipe definition paths. + private async Task> LocateAlternateRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath ) + { + var targetApplicationDirectoryPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; + string? rootDirectoryPath; + + if (await IsDirectoryUnderSourceControl(targetApplicationDirectoryPath)) + { + rootDirectoryPath = await GetSourceControlRootDirectory(targetApplicationDirectoryPath); + } + else + { + rootDirectoryPath = solutionDirectoryPath; + } + + return GetRecipePathsFromRootDirectory(rootDirectoryPath); + } + + /// + /// This method takes a root directory path and recursively searches all its sub-directories for custom recipe paths. + /// However, it ignores any recipe file located inside a "bin" folder. + /// + /// The absolute path of the root directory. + /// A list of recipe definition paths. + private List GetRecipePathsFromRootDirectory(string? rootDirectoryPath) + { + var recipePaths = new List(); + if (!string.IsNullOrEmpty(rootDirectoryPath) && _directoryManager.Exists(rootDirectoryPath)) + { + foreach (var recipeFilePath in _directoryManager.GetFiles(rootDirectoryPath, "*.recipe", SearchOption.AllDirectories)) + { + if (recipeFilePath.Contains(_ignorePathSubstring)) + continue; + recipePaths.Add(_directoryManager.GetDirectoryInfo(recipeFilePath).Parent.FullName); + } + } + return recipePaths; + } + + /// + /// This method finds the root directory that is monitored by the same source control as the current directory. + /// + /// The absolute path of the current directory + /// The source control root directory absolute path. + private async Task GetSourceControlRootDirectory(string currentDirectoryPath) + { + var possibleRootDirectoryPath = currentDirectoryPath; + while (currentDirectoryPath != null && await IsDirectoryUnderSourceControl(currentDirectoryPath)) + { + possibleRootDirectoryPath = currentDirectoryPath; + currentDirectoryPath = _directoryManager.GetDirectoryInfo(currentDirectoryPath).Parent.FullName; + } + return possibleRootDirectoryPath; + } + + /// + /// Helper method to find if the directory is monitored by a source control system. + /// + /// An absolute directory path. + /// + private async Task IsDirectoryUnderSourceControl(string? directoryPath) + { + if (!string.IsNullOrEmpty(directoryPath)) + { + var gitStatusResult = await _commandLineWrapper.TryRunWithResult(GIT_STATUS_COMMAND, directoryPath); + var svnStatusResult = await _commandLineWrapper.TryRunWithResult(SVN_STATUS_COMMAND, directoryPath); + return gitStatusResult.Success || svnStatusResult.Success; + } + return false; + } + + /// + /// This method determines if the given directory contains any recipe files + /// + /// The path of the directory that needs to be validated + /// A bool indicating the presence of a recipe file inside the directory. + private bool ContainsRecipeFile(string directoryPath) + { + var directoryName = _directoryManager.GetDirectoryInfo(directoryPath).Name; + var recipeFilePaths = _directoryManager.GetFiles(directoryPath, "*.recipe"); + if (!recipeFilePaths.Any()) + { + return false; + } + + return recipeFilePaths.All(filePath => Path.GetFileNameWithoutExtension(filePath).Equals(directoryName, StringComparison.Ordinal)); + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index a457fa5f8..1ab8ce10d 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; @@ -31,6 +32,7 @@ public class Orchestrator private readonly IDeploymentBundleHandler? _deploymentBundleHandler; private readonly IDockerEngine? _dockerEngine; private readonly IList? _recipeDefinitionPaths; + private readonly ICustomRecipeLocator? _customRecipeLocator; private readonly OrchestratorSession? _session; @@ -42,6 +44,7 @@ public Orchestrator( IAWSResourceQueryer awsResourceQueryer, IDeploymentBundleHandler deploymentBundleHandler, IDockerEngine dockerEngine, + ICustomRecipeLocator customRecipeLocator, IList recipeDefinitionPaths) { _session = session; @@ -51,6 +54,7 @@ public Orchestrator( _awsResourceQueryer = awsResourceQueryer; _deploymentBundleHandler = deploymentBundleHandler; _dockerEngine = dockerEngine; + _customRecipeLocator = customRecipeLocator; _recipeDefinitionPaths = recipeDefinitionPaths; } @@ -60,7 +64,24 @@ public Orchestrator(OrchestratorSession session, IList recipeDefinitionP _recipeDefinitionPaths = recipeDefinitionPaths; } - public async Task> GenerateDeploymentRecommendations(bool forDeployment = true) + public async Task> GenerateDeploymentRecommendations() + { + 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 targetApplicationFullPath = new DirectoryInfo(_session.ProjectDefinition.ProjectPath).FullName; + var solutionDirectoryPath = !string.IsNullOrEmpty(_session.ProjectDefinition.ProjectSolutionPath) ? + new DirectoryInfo(_session.ProjectDefinition.ProjectSolutionPath).Parent.FullName : string.Empty; + + var customRecipePaths = await LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath); + var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths.Union(customRecipePaths), _session); + var additionalReplacements = await GetReplacements(); + return await engine.ComputeRecommendations(additionalReplacements); + } + + public async Task> GenerateRecommendationsToSaveDeploymentProject() { if (_recipeDefinitionPaths == null) throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); @@ -68,10 +89,17 @@ public async Task> GenerateDeploymentRecommendations(bool f throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths, _session); + return await engine.ComputeRecommendations(); + } - if (!forDeployment) - return await engine.ComputeRecommendations(); + public async Task> GenerateRecommendationsFromSavedDeploymentProject(string deploymentProjectPath) + { + if (_session == null) + throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); + if (!Directory.Exists(deploymentProjectPath)) + throw new InvalidCliArgumentException($"The path '{deploymentProjectPath}' does not exists on the file system. Please provide a valid deployment project path and try again."); + var engine = new RecommendationEngine.RecommendationEngine(new List { deploymentProjectPath }, _session); var additionalReplacements = await GetReplacements(); return await engine.ComputeRecommendations(additionalReplacements); } @@ -188,5 +216,18 @@ public async Task CreateDotnetPublishDeploymentBundle(Recommendation recom return true; } + + private async Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) + { + if (_customRecipeLocator == null) + throw new InvalidOperationException($"{nameof(_customRecipeLocator)} is null as part of the orchestartor object"); + + var customRecipePaths = new List(); + foreach (var customRecipePath in await _customRecipeLocator.LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath)) + { + customRecipePaths.Add(customRecipePath); + } + return customRecipePaths; + } } } diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index 9e15f914f..8a2d0592c 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -24,6 +24,8 @@ public RecommendationEngine(IEnumerable recipeDefinitionPaths, Orchestra recipeDefinitionPaths ??= new List(); + var uniqueRecipeId = new HashSet(); + foreach (var recommendationPath in recipeDefinitionPaths) { foreach (var recipeFile in Directory.GetFiles(recommendationPath, "*.recipe", SearchOption.TopDirectoryOnly)) @@ -33,8 +35,11 @@ public RecommendationEngine(IEnumerable recipeDefinitionPaths, Orchestra var content = File.ReadAllText(recipeFile); var definition = JsonConvert.DeserializeObject(content); definition.RecipePath = recipeFile; - - _availableRecommendations.Add(definition); + if (!uniqueRecipeId.Contains(definition.Id)) + { + _availableRecommendations.Add(definition); + uniqueRecipeId.Add(definition.Id); + } } catch (Exception e) { diff --git a/src/AWS.Deploy.Orchestration/TemplateEngine.cs b/src/AWS.Deploy.Orchestration/TemplateEngine.cs index b7bb128cc..8fc658b02 100644 --- a/src/AWS.Deploy.Orchestration/TemplateEngine.cs +++ b/src/AWS.Deploy.Orchestration/TemplateEngine.cs @@ -71,12 +71,6 @@ public async Task GenerateCDKProjectFromTemplate(Recommendation recommendation, ?? throw new InvalidAWSDeployRecipesCDKCommonVersionException("The version number of the AWS.Deploy.Recipes.CDK.Common package is invalid.") } }; - foreach(var option in recommendation.Recipe.OptionSettings) - { - var currentValue = recommendation.GetOptionSettingValue(option); - templateParameters[option.Id] = currentValue?.ToString() ?? ""; - } - try { //Generate the CDK project using the installed template into the output directory diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs index 68c6b9fbc..e5fe7f380 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/DeploymentManifestFile/DeploymentManifestFileTests.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using Xunit; using Should; +using AWS.Deploy.CLI.Common.UnitTests.IO; namespace AWS.Deploy.CLI.Common.UnitTests.DeploymentManifestFile { @@ -17,6 +18,7 @@ public class DeploymentManifestFileTests : IDisposable { private readonly IFileManager _fileManager; private readonly IDirectoryManager _directoryManager; + private readonly IDirectoryManager _testDirectoryManager; private readonly string _targetApplicationFullPath; private readonly string _targetApplicationDirectoryFullPath; private readonly IDeploymentManifestEngine _deploymentManifestEngine; @@ -27,23 +29,28 @@ public DeploymentManifestFileTests() { _fileManager = new FileManager(); _directoryManager = new DirectoryManager(); + _testDirectoryManager = new TestDirectoryManager(); var targetApplicationPath = Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj"); _targetApplicationFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).FullName; _targetApplicationDirectoryFullPath = _directoryManager.GetDirectoryInfo(targetApplicationPath).Parent.FullName; - _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _deploymentManifestEngine = new DeploymentManifestEngine(_testDirectoryManager, _fileManager); } [Fact] - public async Task Create() + public async Task UpdateDeploymentManifestFile() { // 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); + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath); + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath2); + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath3); + + var saveCdkDirectoryRelativePath = Path.GetRelativePath(_targetApplicationDirectoryFullPath, saveCdkDirectoryFullPath); + var saveCdkDirectoryRelativePath2 = Path.GetRelativePath(_targetApplicationDirectoryFullPath, saveCdkDirectoryFullPath2); + var saveCdkDirectoryRelativePath3 = Path.GetRelativePath(_targetApplicationDirectoryFullPath, saveCdkDirectoryFullPath3); var deploymentManifestFilePath = Path.Combine(_targetApplicationDirectoryFullPath, "aws-deployments.json"); @@ -73,6 +80,39 @@ public async Task Create() Assert.False(_fileManager.Exists(deploymentManifestFilePath)); } + [Fact] + public async Task GetRecipeDefinitionPaths() + { + // Arrange + var saveCdkDirectoryFullPath = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp"); + var saveCdkDirectoryFullPath2 = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp2"); + var saveCdkDirectoryFullPath3 = Path.Combine(_targetApplicationDirectoryFullPath, "DeploymentProjects", "MyCdkApp3"); + + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath); + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath2); + _testDirectoryManager.CreateDirectory(saveCdkDirectoryFullPath3); + + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath, _targetApplicationFullPath); + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath2, _targetApplicationFullPath); + await _deploymentManifestEngine.UpdateDeploymentManifestFile(saveCdkDirectoryFullPath3, _targetApplicationFullPath); + + var deploymentManifestFilePath = Path.Combine(_targetApplicationDirectoryFullPath, "aws-deployments.json"); + + // Act + var recipeDefinitionPaths = await _deploymentManifestEngine.GetRecipeDefinitionPaths(_targetApplicationFullPath); + + // Assert + Assert.True(_fileManager.Exists(deploymentManifestFilePath)); + recipeDefinitionPaths.Count.ShouldEqual(3); + recipeDefinitionPaths.ShouldContain(saveCdkDirectoryFullPath); + recipeDefinitionPaths.ShouldContain(saveCdkDirectoryFullPath2); + recipeDefinitionPaths.ShouldContain(saveCdkDirectoryFullPath3); + + // cleanup + File.Delete(deploymentManifestFilePath); + Assert.False(_fileManager.Exists(deploymentManifestFilePath)); + } + private async Task> GetDeploymentManifestEntries(string deploymentManifestFilePath) { var deploymentProjectPaths = new List(); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs index e8f46657d..376177979 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs @@ -18,6 +18,12 @@ public DirectoryInfo CreateDirectory(string path) return new DirectoryInfo(path); } + public DirectoryInfo GetDirectoryInfo(string path) => new DirectoryInfo(path); + + public string GetRelativePath(string referenceFullPath, string targetFullPath) => Path.GetRelativePath(referenceFullPath, targetFullPath); + + public string GetAbsolutePath(string referenceFullPath, string targetRelativePath) => Path.GetFullPath(targetRelativePath, referenceFullPath); + public void Delete(string path, bool recursive = false) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); @@ -32,16 +38,10 @@ public bool Exists(string path) 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/CustomRecipeLocatorTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs new file mode 100644 index 000000000..5d39978aa --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs @@ -0,0 +1,127 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using AWS.Deploy.CLI.Utilities; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration; +using Xunit; +using Task = System.Threading.Tasks.Task; +using Should; +using System; +using AWS.Deploy.Common; + +namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject +{ + [Collection("SaveCdkDeploymentProjectTests")] + public class CustomRecipeLocatorTests : IDisposable + { + private readonly string _webAppWithDockerFilePath; + private readonly string _webAppWithNoDockerFilePath; + private readonly string _webAppWithDockerCsproj; + private readonly string _webAppNoDockerCsproj; + private readonly string _testArtifactsDirectoryPath; + private readonly string _solutionDirectoryPath; + private readonly ICustomRecipeLocator _customRecipeLocator; + + private bool _isDisposed; + + public CustomRecipeLocatorTests() + { + var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); + + _webAppWithDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); + _webAppWithNoDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppNoDockerFile"); + + _webAppWithDockerCsproj = Path.Combine(_webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); + _webAppNoDockerCsproj = Path.Combine(_webAppWithNoDockerFilePath, "WebAppNoDockerFile.csproj"); + + _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); + + var directoryManager = new DirectoryManager(); + var fileManager = new FileManager(); + var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); + var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); + var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); + var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); + _customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, commandLineWrapper, directoryManager); + + var solutionPath = new ProjectDefinitionParser(fileManager, directoryManager).Parse(_webAppWithDockerFilePath).Result.ProjectSolutionPath; + _solutionDirectoryPath = directoryManager.GetDirectoryInfo(solutionPath).Parent.FullName; + } + + [Fact] + public async Task LocateCustomRecipePathsWithManifestFile() + { + // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot + await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp2")); + + // ACT - Fetch custom recipes corresponding to the same target application that has a deployment-manifest file. + var customRecipePaths = await _customRecipeLocator.LocateCustomRecipePaths(_webAppWithDockerCsproj, _solutionDirectoryPath); + + // ASSERT + File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); + customRecipePaths.Count.ShouldEqual(2); + customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + + CleanUp(); + } + + [Fact] + public async Task LocateCustomRecipePathsWithoutManifestFile() + { + // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot + await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp2")); + + // ACT - Fetch custom recipes corresponding to a different target application (under source control) without a deployment-manifest file. + var customRecipePaths = await _customRecipeLocator.LocateCustomRecipePaths(_webAppNoDockerCsproj, _solutionDirectoryPath); + + // ASSERT + File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); + customRecipePaths.Count.ShouldEqual(2); + customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + customRecipePaths.ShouldContain(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp1")); + + CleanUp(); + } + + private void CleanUp() + { + if (Directory.Exists(_testArtifactsDirectoryPath)) + Directory.Delete(_testArtifactsDirectoryPath, true); + + if (File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json"))) + File.Delete(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")); + + if (File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json"))) + File.Delete(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + CleanUp(); + } + + _isDisposed = true; + } + + ~CustomRecipeLocatorTests() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs new file mode 100644 index 000000000..ef9958848 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -0,0 +1,265 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AWS.Deploy.CLI.Utilities; +using AWS.Deploy.CLI.IntegrationTests.Utilities; +using AWS.Deploy.Common; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; +using AWS.Deploy.DockerEngine; +using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.CDK; +using AWS.Deploy.Recipes; +using Moq; +using Xunit; +using Should; +using AWS.Deploy.Common.Recipes; +using Newtonsoft.Json; + +namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject +{ + [Collection("SaveCdkDeploymentProjectTests")] + public class RecommendationTests : IDisposable + { + private readonly string _testArtifactsDirectoryPath; + private readonly string _webAppWithDockerFilePath; + private readonly string _webAppWithNoDockerFilePath; + private readonly string _blazorAppPath; + private bool _isDisposed; + + public RecommendationTests() + { + var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); + _webAppWithDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); + _webAppWithNoDockerFilePath = Path.Combine(testAppsDirectoryPath, "WebAppNoDockerFile"); + _blazorAppPath = Path.Combine(testAppsDirectoryPath, "BlazorWasm50"); + _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); + } + + [Fact] + public async Task GenerateRecommendationsWithoutCustomRecipes() + { + // ARRANGE + var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); + + // ACT + var recommendations = await orchestrator.GenerateDeploymentRecommendations(); + + // ASSERT + recommendations.Count.ShouldEqual(3); + recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe + recommendations[1].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe + recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe + + CleanUp(); + } + + [Fact] + public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() + { + // ARRANGE + var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); + + var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); + var saveDirectoryPathEbsProject = Path.Combine(_testArtifactsDirectoryPath, "EBS-CDK"); + + var customEcsRecipeName = "Custom ECS Fargate Recipe"; + var customEbsRecipeName = "Custom Elastic Beanstalk Recipe"; + + // select ECS Fargate recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + + // select Elastic Beanstalk recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEbsRecipeName, "2", saveDirectoryPathEbsProject); + + // Get custom recipe IDs + var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + var customEbsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); + + // ACT + var recommendations = await orchestrator.GenerateDeploymentRecommendations(); + + // ASSERT - Recipes are ordered by priority + recommendations.Count.ShouldEqual(5); + recommendations[0].Name.ShouldEqual(customEcsRecipeName); // custom recipe + recommendations[1].Name.ShouldEqual(customEbsRecipeName); // custom recipe + recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); // default recipe + recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe + recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe + + // ASSERT - Recipe paths + recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + recommendations[1].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); + + // ASSERT - custom recipe IDs + recommendations[0].Recipe.Id.ShouldEqual(customEcsRecipeId); + recommendations[1].Recipe.Id.ShouldEqual(customEbsRecipeId); + + File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); + + CleanUp(); + } + + [Fact] + public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() + { + // ARRANGE + var orchestrator = await GetOrchestrator(_webAppWithNoDockerFilePath); + + var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); + var saveDirectoryPathEbsProject = Path.Combine(_testArtifactsDirectoryPath, "EBS-CDK"); + + var customEcsRecipeName = "Custom ECS Fargate Recipe"; + var customEbsRecipeName = "Custom Elastic Beanstalk Recipe"; + + // Select ECS Fargate recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + + // Select Elastic Beanstalk recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEbsRecipeName, "2", saveDirectoryPathEbsProject); + + // Get custom recipe IDs + var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + var customEbsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); + + // ACT + var recommendations = await orchestrator.GenerateDeploymentRecommendations(); + + // ASSERT - Recipes are ordered by priority + recommendations.Count.ShouldEqual(5); + recommendations[0].Name.ShouldEqual(customEbsRecipeName); + recommendations[1].Name.ShouldEqual(customEcsRecipeName); + recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); + recommendations[3].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using Fargate"); + recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); + + // ASSERT - Recipe paths + recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); + recommendations[1].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + + // ASSERT - Custom recipe IDs + recommendations[0].Recipe.Id.ShouldEqual(customEbsRecipeId); + recommendations[1].Recipe.Id.ShouldEqual(customEcsRecipeId); + File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); + + CleanUp(); + } + + [Fact] + public async Task GenerateRecommendationsFromCompatibleDeploymentProject() + { + // ARRANGE + var orchestrator = await GetOrchestrator(_webAppWithDockerFilePath); + var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); + var customEcsRecipeName = "Custom ECS Fargate Recipe"; + + // Select ECS Fargate recipe + await Utilities.CreateCDKDeploymentProjectWithRecipeName(_webAppWithDockerFilePath, customEcsRecipeName, "1", saveDirectoryPathEcsProject); + + // Get custom recipe IDs + var customEcsRecipeId = await GetCustomRecipeId(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + + // ACT + var recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(saveDirectoryPathEcsProject); + + // ASSERT + recommendations.Count.ShouldEqual(1); + recommendations[0].Name.ShouldEqual("Custom ECS Fargate Recipe"); + recommendations[0].Recipe.Id.ShouldEqual(customEcsRecipeId); + recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); + + CleanUp(); + } + + [Fact] + public async Task GenerateRecommendationsFromIncompatibleDeploymentProject() + { + // ARRANGE + var orchestrator = await GetOrchestrator(_blazorAppPath); + var saveDirectoryPathEcsProject = Path.Combine(_testArtifactsDirectoryPath, "ECS-CDK"); + await Utilities.CreateCDKDeploymentProject(_webAppWithDockerFilePath, saveDirectoryPathEcsProject); + + // ACT + var recommendations = await orchestrator.GenerateRecommendationsFromSavedDeploymentProject(saveDirectoryPathEcsProject); + + // ASSERT + recommendations.ShouldBeEmpty(); + + CleanUp(); + } + + private async Task GetOrchestrator(string targetApplicationProjectPath) + { + var directoryManager = new DirectoryManager(); + var fileManager = new FileManager(); + var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); + var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); + var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); + var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); + var customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, commandLineWrapper, directoryManager); + + var projectDefinition = await new ProjectDefinitionParser(fileManager, directoryManager).Parse(targetApplicationProjectPath); + var session = new OrchestratorSession(projectDefinition); + + return new Orchestrator(session, + consoleOrchestratorLogger, + new Mock().Object, + new Mock().Object, + new TestToolAWSResourceQueryer(), + new Mock().Object, + new Mock().Object, + customRecipeLocator, + new List { RecipeLocator.FindRecipeDefinitionsPath() }); + } + + private async Task GetCustomRecipeId(string recipeFilePath) + { + var recipeBody = await File.ReadAllTextAsync(recipeFilePath); + var recipe = JsonConvert.DeserializeObject(recipeBody); + return recipe.Id; + } + + private void CleanUp() + { + if (Directory.Exists(_testArtifactsDirectoryPath)) + Directory.Delete(_testArtifactsDirectoryPath, true); + + if (File.Exists(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json"))) + File.Delete(Path.Combine(_webAppWithDockerFilePath, "aws-deployments.json")); + + if (File.Exists(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json"))) + File.Delete(Path.Combine(_webAppWithNoDockerFilePath, "aws-deployments.json")); + + if (File.Exists(Path.Combine(_blazorAppPath, "aws-deployments.json"))) + File.Delete(Path.Combine(_blazorAppPath, "aws-deployments.json")); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + if (disposing) + { + CleanUp(); + } + + _isDisposed = true; + } + + ~RecommendationTests() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs index ee68c6836..341c84dd9 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs @@ -4,149 +4,75 @@ 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 { + [Collection("SaveCdkDeploymentProjectTests")] public class SaveCdkDeploymentProjectTests : IDisposable { - private readonly App _app; - private readonly InMemoryInteractiveService _interactiveService; private readonly string _targetApplicationProjectPath; private readonly string _deploymentManifestFilePath; + private readonly string _testArtifactsDirectoryPath; 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"); + var testAppsDirectoryPath = Utilities.ResolvePathToTestApps(); + _targetApplicationProjectPath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile"); + _deploymentManifestFilePath = Path.Combine(testAppsDirectoryPath, "WebAppWithDockerFile", "aws-deployments.json"); + _testArtifactsDirectoryPath = Path.Combine(testAppsDirectoryPath, "TestArtifacts"); } [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)); + _saveDirectoryPath = _targetApplicationProjectPath + "CDK"; + await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath); + CleanUp(); } [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)); + _saveDirectoryPath = Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath); + CleanUp(); } [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)); + + _saveDirectoryPath = Path.Combine(_targetApplicationProjectPath, "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath, false); + CleanUp(); } [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")); + Directory.CreateDirectory(Path.Combine(_testArtifactsDirectoryPath, "MyCdkAPP", "MyFolder")); + _saveDirectoryPath = Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp"); + await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath, false); + CleanUp(); + } - // 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); + private void CleanUp() + { + if (Directory.Exists(_testArtifactsDirectoryPath)) + Directory.Delete(_testArtifactsDirectoryPath, true); - Assert.Equal(CommandReturnCodes.USER_ERROR, returnCode); + if (File.Exists(_deploymentManifestFilePath)) + File.Delete(_deploymentManifestFilePath); - Directory.Delete("DeploymentProjects", true); + if (Directory.Exists(_saveDirectoryPath)) + Directory.Delete(_saveDirectoryPath, true); } public void Dispose() @@ -161,11 +87,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - if (Directory.Exists(_saveDirectoryPath)) - Directory.Delete(_saveDirectoryPath, true); - - if (File.Exists(_deploymentManifestFilePath)) - File.Delete(_deploymentManifestFilePath); + CleanUp(); } _isDisposed = true; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs new file mode 100644 index 000000000..2f9b9d3a8 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/Utilities.cs @@ -0,0 +1,139 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AWS.Deploy.CLI.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Services; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Should; +using System.Reflection; + +namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject +{ + 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 + + string[] deployArgs; + // default save directory + if (string.IsNullOrEmpty(saveDirectoryPath)) + { + saveDirectoryPath = targetApplicationPath + "CDK"; + deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath }; + } + else + { + deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--output", saveDirectoryPath }; + } + + + var returnCode = await app.Run(deployArgs); + + // Verify project is saved + var stdOut = interactiveService.StdOutReader.ReadAllLines(); + var successMessage = $"The CDK deployment project is saved at: {saveDirectoryPath}"; + + if (!isValid) + { + returnCode.ShouldEqual(CommandReturnCodes.USER_ERROR); + return; + } + + returnCode.ShouldEqual(CommandReturnCodes.SUCCESS); + stdOut.ShouldContain(successMessage); + + VerifyCreatedArtifacts(targetApplicationPath, saveDirectoryPath); + } + + public static async Task CreateCDKDeploymentProjectWithRecipeName(string targetApplicationPath, string recipeName, string option, 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(option); // select recipe to save the CDK deployment project + await interactiveService.StdInWriter.FlushAsync(); + + + string[] deployArgs; + // default save directory + if (string.IsNullOrEmpty(saveDirectoryPath)) + { + saveDirectoryPath = targetApplicationPath + "CDK"; + deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--project-display-name", recipeName}; + } + else + { + deployArgs = new[] { "deployment-project", "generate", "--project-path", targetApplicationPath, "--output", saveDirectoryPath, "--project-display-name", recipeName }; + } + + + var returnCode = await app.Run(deployArgs); + + // Verify project is saved + var stdOut = interactiveService.StdOutReader.ReadAllLines(); + var successMessage = $"The CDK deployment project is saved at: {saveDirectoryPath}"; + + if (!isValid) + { + returnCode.ShouldEqual(CommandReturnCodes.USER_ERROR); + return; + } + + returnCode.ShouldEqual(CommandReturnCodes.SUCCESS); + stdOut.ShouldContain(successMessage); + + VerifyCreatedArtifacts(targetApplicationPath, saveDirectoryPath); + } + + public static string ResolvePathToTestApps() + { + var testsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + while (testsPath != null && !string.Equals(new DirectoryInfo(testsPath).Name, "test", StringComparison.OrdinalIgnoreCase)) + { + testsPath = Directory.GetParent(testsPath).FullName; + } + return new DirectoryInfo(Path.Combine(testsPath, "..", "testapps")).FullName; + } + + private static (App app, InMemoryInteractiveService interactiveService) GetAppServiceProvider() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var app = serviceProvider.GetService(); + var interactiveService = serviceProvider.GetService(); + return (app, interactiveService); + } + + private static void VerifyCreatedArtifacts(string targetApplicationPath, string saveDirectoryPath) + { + var saveDirectoryName = new DirectoryInfo(saveDirectoryPath).Name; + + Assert.True(Directory.Exists(saveDirectoryPath)); + 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(File.Exists(Path.Combine(saveDirectoryPath, $"{saveDirectoryName}.recipe"))); + Assert.True(File.Exists(Path.Combine(targetApplicationPath, "aws-deployments.json"))); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs new file mode 100644 index 000000000..5965446d2 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Amazon.CloudFormation.Model; +using Amazon.CloudWatchEvents.Model; +using Amazon.EC2.Model; +using Amazon.ECR.Model; +using Amazon.ECS.Model; +using Amazon.ElasticBeanstalk.Model; +using Amazon.IdentityManagement.Model; +using Amazon.SecurityToken.Model; +using AWS.Deploy.Orchestration.Data; + +namespace AWS.Deploy.CLI.IntegrationTests.Utilities +{ + public class TestToolAWSResourceQueryer : IAWSResourceQueryer + { + public Task GetLatestElasticBeanstalkPlatformArn() + { + return System.Threading.Tasks.Task.FromResult(new PlatformSummary() { PlatformArn = string.Empty }); + } + + public Task CreateEC2KeyPair(string keyName, string saveLocation) => throw new NotImplementedException(); + public Task CreateECRRepository(string repositoryName) => throw new NotImplementedException(); + public Task> DescribeCloudFormationResources(string stackName) => throw new NotImplementedException(); + public Task DescribeCloudWatchRule(string ruleName) => throw new NotImplementedException(); + public Task DescribeElasticBeanstalkEnvironment(string environmentId) => throw new NotImplementedException(); + public Task DescribeElasticLoadBalancer(string loadBalancerArn) => throw new NotImplementedException(); + public Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn) => throw new NotImplementedException(); + public Task GetCallerIdentity() => throw new NotImplementedException(); + public Task> GetCloudFormationStacks() => throw new NotImplementedException(); + public Task> GetECRAuthorizationToken() => throw new NotImplementedException(); + public Task> GetECRRepositories(List repositoryNames) => throw new NotImplementedException(); + public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); + public Task> GetListOfVpcs() => throw new NotImplementedException(); + public Task GetS3BucketLocation(string bucketName) => throw new NotImplementedException(); + public Task> ListOfEC2KeyPairs() => throw new NotImplementedException(); + public Task> ListOfECSClusters() => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkApplications() => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkEnvironments(string applicationName) => throw new NotImplementedException(); + public Task> ListOfIAMRoles(string servicePrincipal) => throw new NotImplementedException(); + public Task DescribeAppRunnerService(string serviceArn) => throw new NotImplementedException(); + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index f3f57cf63..a75f24641 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -50,8 +50,12 @@ public Task> GetECRRepositories(List repositoryNames) return Task.FromResult>(new List() { repository }); } + public Task GetLatestElasticBeanstalkPlatformArn() + { + return Task.FromResult(new PlatformSummary() { PlatformArn = string.Empty }); + } + public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); - public Task GetLatestElasticBeanstalkPlatformArn() => throw new NotImplementedException(); public Task> GetListOfVpcs() => throw new NotImplementedException(); public Task> ListOfEC2KeyPairs() => throw new NotImplementedException(); public Task> ListOfECSClusters() => throw new NotImplementedException(); From f1f5caf931fe9f54fbea14cea6a4fa6c14d752d5 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Mon, 9 Aug 2021 11:18:04 -0400 Subject: [PATCH 3/3] fix: Fix integration tests that are failing in CodePipeline --- .../SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs | 2 +- .../SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs index 5d39978aa..c179eb09b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs @@ -47,7 +47,7 @@ public CustomRecipeLocatorTests() var commandLineWrapper = new CommandLineWrapper(consoleOrchestratorLogger); _customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, commandLineWrapper, directoryManager); - var solutionPath = new ProjectDefinitionParser(fileManager, directoryManager).Parse(_webAppWithDockerFilePath).Result.ProjectSolutionPath; + var solutionPath = Path.Combine(testAppsDirectoryPath, "..", "AWS.Deploy.sln"); _solutionDirectoryPath = directoryManager.GetDirectoryInfo(solutionPath).Parent.FullName; } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs index 341c84dd9..397e779dd 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/SaveCdkDeploymentProjectTests.cs @@ -57,7 +57,7 @@ public async Task InvalidSaveCdkDirectoryInsideProjectDirectory() [Fact] public async Task InvalidNonEmptySaveCdkDirectory() { - Directory.CreateDirectory(Path.Combine(_testArtifactsDirectoryPath, "MyCdkAPP", "MyFolder")); + Directory.CreateDirectory(Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp", "MyFolder")); _saveDirectoryPath = Path.Combine(_testArtifactsDirectoryPath, "MyCdkApp"); await Utilities.CreateCDKDeploymentProject(_targetApplicationProjectPath, _saveDirectoryPath, false); CleanUp();