From 5d5d688412ba014361cb7f254f785ce8c2ae2057 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 22 Jun 2021 11:52:29 -0400 Subject: [PATCH 1/7] build: Version bump to 0.12 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index d1fe0a04a..866f49c19 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.11", + "version": "0.12", "publicReleaseRefSpec": [ ".*" ], From 91535d5bad406ce8e1c7467c37a64fd176e303ff Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Wed, 30 Jun 2021 19:34:06 -0400 Subject: [PATCH 2/7] chore: Refactor the Deploy command to provide separation of responsibilities. --- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 2 +- .../DeployCommandHandlerInput.cs | 1 - src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 260 +++++++++++------- src/AWS.Deploy.Orchestration/Orchestrator.cs | 5 + .../OrchestratorSession.cs | 4 + 5 files changed, 175 insertions(+), 97 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index 8e12d4925..07f9c89ad 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -168,7 +168,7 @@ private Command BuildDeployCommand() _consoleUtilities, session); - await deploy.ExecuteAsync(input.StackName ?? "", input.SaveCdkProject, userDeploymentSettings); + await deploy.ExecuteAsync(input.StackName ?? "", userDeploymentSettings); return CommandReturnCodes.SUCCESS; } diff --git a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs index f053112bd..6a1557a91 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandHandlerInput/DeployCommandHandlerInput.cs @@ -17,6 +17,5 @@ public class DeployCommandHandlerInput public string? Apply { get; set; } public bool Diagnostics { get; set; } public bool Silent { get; set; } - public bool SaveCdkProject { get; set; } } } diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 6da6c4689..5358a4124 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -67,10 +67,37 @@ public DeployCommand( _cdkManager = cdkManager; } - public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploymentSettings? userDeploymentSettings = null) + public async Task ExecuteAsync(string stackName, UserDeploymentSettings? userDeploymentSettings = null) { - var orchestrator = - new Orchestrator( + var (orchestrator, selectedRecommendation, cloudApplication) = await InitializeDeployment(stackName, userDeploymentSettings); + + // Verify Docker installation and minimum NodeJS version. + await EvaluateSystemCapabilities(_session, selectedRecommendation); + + // Configure option settings. + await ConfigureDeployment(cloudApplication, orchestrator, selectedRecommendation, userDeploymentSettings); + + if (!ConfirmDeployment(selectedRecommendation)) + { + return; + } + + await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication); + + await orchestrator.DeployRecommendation(cloudApplication, selectedRecommendation); + } + + /// + /// Initiates a deployment or a re-deployment. + /// If a new Cloudformation stack name is selected, then a fresh deployment is initiated with the user-selected deployment recipe. + /// If an existing Cloudformation stack name is selected, then a re-deployment is initiated with the same deployment recipe. + /// + /// The stack name provided via the --stack-name CLI argument + /// The deserialized object from the user provided config file. + /// A tuple consisting of the Orchestrator object, Selected Recommendation, Cloud Application metadata. + public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string stackName, UserDeploymentSettings? userDeploymentSettings) + { + var orchestrator = new Orchestrator( _session, _orchestratorInteractiveService, _cdkProjectHandler, @@ -81,84 +108,36 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploy new[] { RecipeLocator.FindRecipeDefinitionsPath() }); // Determine what recommendations are possible for the project. - var recommendations = await orchestrator.GenerateDeploymentRecommendations(); - if (recommendations.Count == 0) - { - throw new FailedToGenerateAnyRecommendations("The project you are trying to deploy is currently not supported."); - } + var recommendations = await GenerateDeploymentRecommendations(orchestrator); // Look to see if there are any existing deployed applications using any of the compatible recommendations. var deployedApplications = await _deployedApplicationQueryer.GetExistingDeployedApplications(recommendations); - if (!string.IsNullOrEmpty(stackName) && !_cloudApplicationNameGenerator.IsValidName(stackName)) - { - PrintInvalidStackNameMessage(); - throw new InvalidCliArgumentException("Found invalid CLI arguments"); - } - - _toolInteractiveService.WriteLine(); - + // Get Cloudformation stack name. var cloudApplicationName = GetCloudApplicationName(stackName, userDeploymentSettings, deployedApplications); + // Find existing application with the same CloudFormation stack name. var deployedApplication = deployedApplications.FirstOrDefault(x => string.Equals(x.Name, cloudApplicationName)); Recommendation? selectedRecommendation = null; - - _toolInteractiveService.WriteLine(); - - // If using a previous deployment preset settings for deployment based on last deployment. if (deployedApplication != null) - { - var existingCloudApplicationMetadata = await _templateMetadataReader.LoadCloudApplicationMetadata(deployedApplication.Name); - - 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."); - } - - selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); - - var header = $"Loading {deployedApplication.Name} settings:"; - - _toolInteractiveService.WriteLine(header); - _toolInteractiveService.WriteLine(new string('-', header.Length)); - var optionSettings = - selectedRecommendation - .Recipe - .OptionSettings - .Where(x => - { - if (!selectedRecommendation.IsOptionSettingDisplayable(x)) - return false; - - var value = selectedRecommendation.GetOptionSettingValue(x); - if (value == null || value.ToString() == string.Empty || object.Equals(value, x.DefaultValue)) - return false; - - return true; - }) - .ToArray(); - - foreach (var setting in optionSettings) - { - DisplayOptionSetting(selectedRecommendation, setting, -1, optionSettings.Length, DisplayOptionSettingsMode.Readonly); - } - } + // preset settings for deployment based on last deployment. + selectedRecommendation = await GetSelectedRecommendationFromPreviousDeployment(recommendations, deployedApplication, userDeploymentSettings); else - { selectedRecommendation = GetSelectedRecommendation(userDeploymentSettings, recommendations); - } - // Apply the user enter project name to the recommendation so that any default settings based on project name are applied. - selectedRecommendation.OverrideProjectName(cloudApplicationName); + var cloudApplication = new CloudApplication(cloudApplicationName, selectedRecommendation.Recipe.Id); + + return (orchestrator, selectedRecommendation, cloudApplication); + } + /// + /// Checks if the system meets all the necessary requirements for deployment. + /// + /// Holds metadata about the deployment project and the AWS account used for deployment. + /// The selected recommendation settings used for deployment. + public async Task EvaluateSystemCapabilities(OrchestratorSession session, Recommendation selectedRecommendation) + { if (_session.SystemCapabilities == null) throw new SystemCapabilitiesNotProvidedException("The system capabilities were not provided."); @@ -181,6 +160,19 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploy throw new DockerContainerTypeException("The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue."); } } + } + + /// + /// Configure option setings using the CLI or a user provided configuration file. + /// + /// + /// + /// + /// + public async Task ConfigureDeployment(CloudApplication cloudApplication, Orchestrator orchestrator, Recommendation selectedRecommendation, UserDeploymentSettings? userDeploymentSettings) + { + // Apply the user entered project name to the recommendation so that any default settings based on project name are applied. + selectedRecommendation.OverrideProjectName(cloudApplication.Name); var deploymentBundleDefinition = orchestrator.GetDeploymentBundleDefinition(selectedRecommendation); @@ -188,24 +180,70 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploy if (userDeploymentSettings != null) { - ConfigureDeployment(selectedRecommendation, deploymentBundleDefinition, userDeploymentSettings); + ConfigureDeploymentFromConfigFile(selectedRecommendation, deploymentBundleDefinition, userDeploymentSettings); } - + if (!_toolInteractiveService.DisableInteractive) { - await ConfigureDeployment(selectedRecommendation, configurableOptionSettings, false); - } + await ConfigureDeploymentFromCli(selectedRecommendation, configurableOptionSettings, false); + } + } - var cloudApplication = new CloudApplication(cloudApplicationName, string.Empty); + private async Task> GenerateDeploymentRecommendations(Orchestrator orchestrator) + { + var recommendations = await orchestrator.GenerateDeploymentRecommendations(); + if (recommendations.Count == 0) + { + throw new FailedToGenerateAnyRecommendations("The project you are trying to deploy is currently not supported."); + } + return recommendations; + } - if (!ConfirmDeployment(selectedRecommendation)) + private async Task GetSelectedRecommendationFromPreviousDeployment(List recommendations, CloudApplication deployedApplication, UserDeploymentSettings? userDeploymentSettings) + { + var existingCloudApplicationMetadata = await _templateMetadataReader.LoadCloudApplicationMetadata(deployedApplication.Name); + + 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)) { - return; + 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."); } - await CreateDeploymentBundle(orchestrator, selectedRecommendation, cloudApplication); + selectedRecommendation.ApplyPreviousSettings(existingCloudApplicationMetadata.Settings); - await orchestrator.DeployRecommendation(cloudApplication, selectedRecommendation); + var header = $"Loading {deployedApplication.Name} settings:"; + + _toolInteractiveService.WriteLine(header); + _toolInteractiveService.WriteLine(new string('-', header.Length)); + var optionSettings = + selectedRecommendation + .Recipe + .OptionSettings + .Where(x => + { + if (!selectedRecommendation.IsOptionSettingDisplayable(x)) + return false; + + var value = selectedRecommendation.GetOptionSettingValue(x); + if (value == null || value.ToString() == string.Empty || object.Equals(value, x.DefaultValue)) + return false; + + return true; + }) + .ToArray(); + + foreach (var setting in optionSettings) + { + DisplayOptionSetting(selectedRecommendation, setting, -1, optionSettings.Length, DisplayOptionSettingsMode.Readonly); + } + + return selectedRecommendation; } /// @@ -213,8 +251,8 @@ public async Task ExecuteAsync(string stackName, bool saveCdkProject, UserDeploy /// /// The selected recommendation settings used for deployment /// The container for the deployment bundle used by an application. - /// The deserialized object from the user provided config file. - private void ConfigureDeployment(Recommendation recommendation, DeploymentBundleDefinition deploymentBundleDefinition, UserDeploymentSettings userDeploymentSettings) + /// The deserialized object from the user provided config file. + private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, DeploymentBundleDefinition deploymentBundleDefinition, UserDeploymentSettings userDeploymentSettings) { foreach (var entry in userDeploymentSettings.LeafOptionSettingItems) { @@ -260,6 +298,27 @@ private void ConfigureDeployment(Recommendation recommendation, DeploymentBundle SetDeploymentBundleOptionSetting(recommendation, optionSetting.Id, settingValue); } } + + var validatorFailedResults = + recommendation.Recipe + .BuildValidators() + .Select(validator => validator.Validate(recommendation.Recipe, _session)) + .Where(x => !x.IsValid) + .ToList(); + + if (!validatorFailedResults.Any()) + { + // validation successful + // deployment configured + return; + } + + var errorMessage = "The deployment configuration needs to be adjusted before it can be deployed:" + Environment.NewLine; + foreach (var result in validatorFailedResults) + { + errorMessage += result.ValidationFailedMessage + Environment.NewLine; + } + throw new InvalidUserDeploymentSettingsException(errorMessage.Trim()); } private void SetDeploymentBundleOptionSetting(Recommendation recommendation, string optionSettingId, object settingValue) @@ -288,22 +347,32 @@ private void SetDeploymentBundleOptionSetting(Recommendation recommendation, str private string GetCloudApplicationName(string? stackName, UserDeploymentSettings? userDeploymentSettings, List deployedApplications) { + // validate the stackName provided by the --stack-name cli argument if present. if (!string.IsNullOrEmpty(stackName)) - return stackName; + { + if (_cloudApplicationNameGenerator.IsValidName(stackName)) + return stackName; - if (userDeploymentSettings == null || string.IsNullOrEmpty(userDeploymentSettings.StackName)) + PrintInvalidStackNameMessage(); + throw new InvalidCliArgumentException("Found invalid CLI arguments"); + } + + if (userDeploymentSettings != null && !string.IsNullOrEmpty(userDeploymentSettings.StackName)) { - if (_toolInteractiveService.DisableInteractive) - { - var message = "The \"--silent\" CLI argument can only be used if a CDK stack name is provided either via the CLI argument \"--stack-name\" or through a deployment-settings file. " + - "Please provide a stack name and try again"; - throw new InvalidCliArgumentException(message); - } - return AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); + if (_cloudApplicationNameGenerator.IsValidName(userDeploymentSettings.StackName)) + return userDeploymentSettings.StackName; + + PrintInvalidStackNameMessage(); + throw new InvalidUserDeploymentSettingsException("Please provide a valid stack name and try again."); } - - _toolInteractiveService.WriteLine($"Configuring Stack Name with specified value '{userDeploymentSettings.StackName}'."); - return userDeploymentSettings.StackName; + + if (_toolInteractiveService.DisableInteractive) + { + var message = "The \"--silent\" CLI argument can only be used if a CDK stack name is provided either via the CLI argument \"--stack-name\" or through a deployment-settings file. " + + "Please provide a stack name and try again"; + throw new InvalidCliArgumentException(message); + } + return AskUserForCloudApplicationName(_session.ProjectDefinition, deployedApplications); } private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDeploymentSettings, List recommendations) @@ -324,6 +393,7 @@ private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDep 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}'."); return selectedRecommendation; } @@ -441,7 +511,7 @@ private async Task CreateDeploymentBundle(Orchestrator orchestrator, Recommendat } } - private async Task ConfigureDeployment(Recommendation recommendation, IEnumerable configurableOptionSettings, bool showAdvancedSettings) + private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEnumerable configurableOptionSettings, bool showAdvancedSettings) { _toolInteractiveService.WriteLine(string.Empty); @@ -514,7 +584,7 @@ private async Task ConfigureDeployment(Recommendation recommendation, IEnumerabl selectedNumber >= 1 && selectedNumber <= optionSettings.Length) { - await ConfigureDeployment(recommendation, optionSettings[selectedNumber - 1]); + await ConfigureDeploymentFromCli(recommendation, optionSettings[selectedNumber - 1]); } _toolInteractiveService.WriteLine(); @@ -536,7 +606,7 @@ private void DisplayOptionSetting(Recommendation recommendation, OptionSettingIt DisplayValue(recommendation, optionSetting, optionSettingNumber, optionSettingsCount, typeHintResponseType, mode); } - private async Task ConfigureDeployment(Recommendation recommendation, OptionSettingItem setting) + private async Task ConfigureDeploymentFromCli(Recommendation recommendation, OptionSettingItem setting) { _toolInteractiveService.WriteLine(string.Empty); _toolInteractiveService.WriteLine($"{setting.Name}:"); @@ -582,7 +652,7 @@ private async Task ConfigureDeployment(Recommendation recommendation, OptionSett foreach (var childSetting in setting.ChildOptionSettings) { if (recommendation.IsOptionSettingDisplayable(childSetting)) - await ConfigureDeployment(recommendation, childSetting); + await ConfigureDeploymentFromCli(recommendation, childSetting); } break; default: @@ -602,7 +672,7 @@ private async Task ConfigureDeployment(Recommendation recommendation, OptionSett _toolInteractiveService.WriteErrorLine( $"Value [{settingValue}] is not valid: {ex.Message}"); - await ConfigureDeployment(recommendation, setting); + await ConfigureDeploymentFromCli(recommendation, setting); } } } diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index e926b72eb..f721f04bf 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -15,6 +15,11 @@ namespace AWS.Deploy.Orchestration { + /// + /// The Orchestrator holds all the metadata that the CLI and the AWS toolkit for Visual studio interact with to perform a deployment. + /// It is responsible for generating deployment recommendations, creating deployment bundles and also acts as a mediator + /// between the client UI and the CDK. + /// public class Orchestrator { private const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; diff --git a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs index 0cd8ce8dc..bf0830a30 100644 --- a/src/AWS.Deploy.Orchestration/OrchestratorSession.cs +++ b/src/AWS.Deploy.Orchestration/OrchestratorSession.cs @@ -8,6 +8,10 @@ namespace AWS.Deploy.Orchestration { + /// + /// The Orchestrator session holds the relevant metadata about the project that needs to be deployed + /// and also contains information about the AWS account and region used for deployment. + /// public class OrchestratorSession : IDeployToolValidationContext { public ProjectDefinition ProjectDefinition { get; set; } From e33d72fc1c451f8d7e787257f05b147983e9b335 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Sun, 27 Jun 2021 00:18:25 -0700 Subject: [PATCH 3/7] feat: Update ECS templates and docker generation to support .NET 6 --- .../Properties/DockerFileConfig.json | 10 ++++++++++ .../RecipeDefinitions/ASP.NETAppECSFargate.recipe | 2 +- .../ConsoleAppECSFargateScheduleTask.recipe | 2 +- .../ConsoleAppECSFargateService.recipe | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/AWS.Deploy.DockerEngine/Properties/DockerFileConfig.json b/src/AWS.Deploy.DockerEngine/Properties/DockerFileConfig.json index bbcf968d9..38348841e 100644 --- a/src/AWS.Deploy.DockerEngine/Properties/DockerFileConfig.json +++ b/src/AWS.Deploy.DockerEngine/Properties/DockerFileConfig.json @@ -2,6 +2,11 @@ { "SdkType": "Microsoft.NET.Sdk.Web", "ImageMapping": [ + { + "TargetFramework": "net6.0", + "BaseImage": "mcr.microsoft.com/dotnet/aspnet:6.0", + "BuildImage": "mcr.microsoft.com/dotnet/sdk:6.0" + }, { "TargetFramework": "net5.0", "BaseImage": "mcr.microsoft.com/dotnet/aspnet:5.0", @@ -32,6 +37,11 @@ { "SdkType": "Microsoft.NET.Sdk", "ImageMapping": [ + { + "TargetFramework": "net6.0", + "BaseImage": "mcr.microsoft.com/dotnet/runtime:6.0", + "BuildImage": "mcr.microsoft.com/dotnet/sdk:6.0" + }, { "TargetFramework": "net5.0", "BaseImage": "mcr.microsoft.com/dotnet/runtime:5.0", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index bfb53ea09..c7227553d 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -24,7 +24,7 @@ "Type": "MSProperty", "Condition": { "PropertyName": "TargetFramework", - "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0" ] + "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0", "net6.0" ] } } ] diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index 43a12b997..74add34f0 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -24,7 +24,7 @@ "Type": "MSProperty", "Condition": { "PropertyName": "TargetFramework", - "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0" ] + "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0", "net6.0" ] } }, { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index d0d3be755..981889503 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -24,7 +24,7 @@ "Type": "MSProperty", "Condition": { "PropertyName": "TargetFramework", - "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0" ] + "AllowedValues": [ "netcoreapp2.1", "netcoreapp3.1", "net5.0", "net6.0" ] } }, { From 5229c4113c1b248b3d8dc523435154eac6e735ce Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 29 Jun 2021 14:19:40 -0400 Subject: [PATCH 4/7] fix: Fix misleading language in user input prompt to deploy to a new or an existing Cloudformation stack. --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 12 +++-- .../TypeHints/DockerBuildArgsCommand.cs | 3 +- .../DockerExecutionDirectoryCommand.cs | 3 +- .../TypeHints/DotnetPublishArgsCommand.cs | 3 +- src/AWS.Deploy.CLI/ConsoleUtilities.cs | 48 ++++++++++--------- src/AWS.Deploy.Constants/CLI.cs | 3 ++ 6 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 6da6c4689..e604d9862 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -354,7 +354,8 @@ private string AskUserForCloudApplicationName(ProjectDefinition project, List x.Name), title, askNewName: true, - defaultNewName: defaultName); + defaultNewName: defaultName, + defaultChoosePrompt: Constants.CLI.PROMPT_CHOOSE_STACK_NAME, + defaultCreateNewPrompt: Constants.CLI.PROMPT_NEW_STACK_NAME, + defaultCreateNewLabel: Constants.CLI.CREATE_NEW_STACK_LABEL) ; cloudApplicationName = userResponse.SelectedOption ?? userResponse.NewName; } @@ -382,8 +386,8 @@ private string AskUserForCloudApplicationName(ProjectDefinition project, List Execute(Recommendation recommendation, OptionSettingItem opt recommendation.GetOptionSettingValue(optionSetting), allowEmpty: true, resetValue: recommendation.GetOptionSettingDefaultValue(optionSetting) ?? "", - // validators: - buildArgs => ValidateBuildArgs(buildArgs)) + validators: buildArgs => ValidateBuildArgs(buildArgs)) .ToString() .Replace("\"", "\"\""); diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index 644f2f293..68e5f1014 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -25,8 +25,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt recommendation.GetOptionSettingValue(optionSetting), allowEmpty: true, resetValue: recommendation.GetOptionSettingDefaultValue(optionSetting) ?? "", - // validators: - executionDirectory => ValidateExecutionDirectory(executionDirectory)); + validators: executionDirectory => ValidateExecutionDirectory(executionDirectory)); recommendation.DeploymentBundle.DockerExecutionDirectory = settingValue; return Task.FromResult(settingValue); diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index 664b71c5c..39d797622 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -25,8 +25,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt recommendation.GetOptionSettingValue(optionSetting), allowEmpty: true, resetValue: recommendation.GetOptionSettingDefaultValue(optionSetting) ?? "", - // validators: - publishArgs => ValidateDotnetPublishArgs(publishArgs)) + validators: publishArgs => ValidateDotnetPublishArgs(publishArgs)) .ToString() .Replace("\"", "\"\""); diff --git a/src/AWS.Deploy.CLI/ConsoleUtilities.cs b/src/AWS.Deploy.CLI/ConsoleUtilities.cs index bd868d64b..7bbde9a3d 100644 --- a/src/AWS.Deploy.CLI/ConsoleUtilities.cs +++ b/src/AWS.Deploy.CLI/ConsoleUtilities.cs @@ -19,13 +19,13 @@ public enum YesNo public interface IConsoleUtilities { Recommendation AskToChooseRecommendation(IList recommendations); - string AskUserToChoose(IList values, string title, string? defaultValue); - T AskUserToChoose(IList options, string title, T defaultValue) + string AskUserToChoose(IList values, string title, string? defaultValue, string? defaultChoosePrompt = null); + T AskUserToChoose(IList options, string title, T defaultValue, string? defaultChoosePrompt = null) where T : IUserInputOption; void DisplayRow((string, int)[] row); - UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, bool askNewName = true, string defaultNewName = "", bool canBeEmpty = false); - UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, UserInputConfiguration userInputConfiguration); - string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", params Func[] validators); + UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, bool askNewName = true, string defaultNewName = "", bool canBeEmpty = false, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null); + UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, UserInputConfiguration userInputConfiguration, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null); + string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func[] validators); string AskForEC2KeyPairSaveDirectory(string projectPath); YesNo AskYesNoQuestion(string question, string? defaultValue); YesNo AskYesNoQuestion(string question, YesNo? defaultValue = default); @@ -73,7 +73,7 @@ public Recommendation AskToChooseRecommendation(IList recommenda return ReadOptionFromUser(recommendations, 1); } - public string AskUserToChoose(IList values, string title, string? defaultValue) + public string AskUserToChoose(IList values, string title, string? defaultValue, string? defaultChoosePrompt = null) { var options = new List(); foreach (var value in values) @@ -83,12 +83,13 @@ public string AskUserToChoose(IList values, string title, string? defaul UserInputOption? defaultOption = defaultValue != null ? new UserInputOption(defaultValue) : null; - return AskUserToChoose(options, title, defaultOption).Name; + return AskUserToChoose(options, title, defaultOption, defaultChoosePrompt).Name; } - public T AskUserToChoose(IList options, string title, T? defaultValue) + public T AskUserToChoose(IList options, string title, T? defaultValue, string? defaultChoosePrompt = null) where T : IUserInputOption { + var choosePrompt = !(string.IsNullOrEmpty(defaultChoosePrompt)) ? defaultChoosePrompt : "Choose option"; if (!string.IsNullOrEmpty(title)) { var dashLength = -1; @@ -136,19 +137,19 @@ public T AskUserToChoose(IList options, string title, T? defaultValue) if (defaultValueIndex != -1) { - _interactiveService.WriteLine($"Choose option (default {defaultValueIndex}):"); + _interactiveService.WriteLine(choosePrompt + $" (default {defaultValueIndex}):"); } else { if(options.Count == 1) { - _interactiveService.WriteLine($"Choose option (default 1):"); + _interactiveService.WriteLine(choosePrompt + " (default 1):"); defaultValueIndex = 1; defaultValue = options[0]; } else { - _interactiveService.WriteLine($"Choose option:"); + _interactiveService.WriteLine(choosePrompt + ":"); } } @@ -170,7 +171,7 @@ public void DisplayRow((string, int)[] row) _interactiveService.WriteLine(string.Format(format, values)); } - public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, bool askNewName = true, string defaultNewName = "", bool canBeEmpty = false) + public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, bool askNewName = true, string defaultNewName = "", bool canBeEmpty = false, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null) { var configuration = new UserInputConfiguration( option => option, @@ -181,14 +182,15 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable optio CanBeEmpty = canBeEmpty }; - return AskUserToChooseOrCreateNew(options, title, configuration); + return AskUserToChooseOrCreateNew(options, title, configuration, defaultChoosePrompt, defaultCreateNewPrompt, defaultCreateNewLabel); } - public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, UserInputConfiguration userInputConfiguration) + public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, UserInputConfiguration userInputConfiguration, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null) { var optionStrings = options.Select(userInputConfiguration.DisplaySelector); var defaultOption = options.FirstOrDefault(userInputConfiguration.DefaultSelector); var defaultValue = ""; + var createNewLabel = !string.IsNullOrEmpty(defaultCreateNewLabel) ? defaultCreateNewLabel : Constants.CLI.CREATE_NEW_LABEL; if (defaultOption != null) { defaultValue = userInputConfiguration.DisplaySelector(defaultOption); @@ -198,7 +200,7 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str if (userInputConfiguration.CurrentValue != null && string.IsNullOrEmpty(userInputConfiguration.CurrentValue.ToString())) defaultValue = Constants.CLI.EMPTY_LABEL; else - defaultValue = userInputConfiguration.CreateNew || !options.Any() ? Constants.CLI.CREATE_NEW_LABEL : userInputConfiguration.DisplaySelector(options.First()); + defaultValue = userInputConfiguration.CreateNew || !options.Any() ? createNewLabel : userInputConfiguration.DisplaySelector(options.First()); } if (optionStrings.Any()) @@ -207,9 +209,9 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str if (userInputConfiguration.EmptyOption) displayOptionStrings.Insert(0, Constants.CLI.EMPTY_LABEL); if (userInputConfiguration.CreateNew) - displayOptionStrings.Add(Constants.CLI.CREATE_NEW_LABEL); - - var selectedString = AskUserToChoose(displayOptionStrings, title, defaultValue); + displayOptionStrings.Add(createNewLabel); + + var selectedString = AskUserToChoose(displayOptionStrings, title, defaultValue, defaultChoosePrompt); if (selectedString == Constants.CLI.EMPTY_LABEL) { @@ -219,7 +221,7 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str }; } - if (selectedString != Constants.CLI.CREATE_NEW_LABEL) + if (selectedString != createNewLabel) { var selectedOption = options.FirstOrDefault(option => userInputConfiguration.DisplaySelector(option) == selectedString); return new UserResponse @@ -232,7 +234,7 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str if (userInputConfiguration.AskNewName) { - var newName = AskUserForValue(string.Empty, userInputConfiguration.DefaultNewName, false); + var newName = AskUserForValue(string.Empty, userInputConfiguration.DefaultNewName, false, defaultAskValuePrompt: defaultCreateNewPrompt); return new UserResponse { CreateNew = true, @@ -246,14 +248,16 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str }; } - public string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", params Func[] validators) + public string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func[] validators) { const string RESET = ""; + var prompt = !string.IsNullOrEmpty(defaultAskValuePrompt) ? defaultAskValuePrompt : "Enter value"; + if (!string.IsNullOrEmpty(defaultValue)) + prompt += $" (default {defaultValue}"; if (!string.IsNullOrEmpty(message)) _interactiveService.WriteLine(message); - var prompt = $"Enter value (default {defaultValue}"; if (allowEmpty) prompt += $". Type {RESET} to reset."; prompt += "): "; diff --git a/src/AWS.Deploy.Constants/CLI.cs b/src/AWS.Deploy.Constants/CLI.cs index 71d061cfb..5243231f3 100644 --- a/src/AWS.Deploy.Constants/CLI.cs +++ b/src/AWS.Deploy.Constants/CLI.cs @@ -9,5 +9,8 @@ internal static class CLI public const string CREATE_NEW_LABEL = "*** Create new ***"; public const string DEFAULT_LABEL = "*** Default ***"; public const string EMPTY_LABEL = "*** Empty ***"; + public const string CREATE_NEW_STACK_LABEL = "*** Deploy to a new stack ***"; + public const string PROMPT_NEW_STACK_NAME = "Enter the name of the new stack"; + public const string PROMPT_CHOOSE_STACK_NAME = "Choose stack to deploy to"; } } From 214f054dab5e4f654a42a64d9c4423c07fad770e Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Thu, 8 Jul 2021 17:03:11 -0700 Subject: [PATCH 5/7] feat: Add logging messages to show what docker build command and from what directory the build will happen --- src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 61a09d6cc..49bf6bbeb 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -56,6 +56,9 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Re var buildArgs = GetDockerBuildArgs(recommendation); var dockerBuildCommand = $"docker build -t {imageTag} -f \"{dockerFile}\"{buildArgs} ."; + _interactiveService.LogMessageLine($"Docker Execution Directory: {Path.GetFullPath(dockerExecutionDirectory)}"); + _interactiveService.LogMessageLine($"Docker Build Command: {dockerBuildCommand}"); + recommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; From 88aa85095605388a46dde8949b7272ab85c33869 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Mon, 12 Jul 2021 15:41:55 -0700 Subject: [PATCH 6/7] fix: server mode not shutting down when parent process exits --- .../Commands/ServerModeCommand.cs | 55 ++++++------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs index 0989ca11c..834e58748 100644 --- a/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/ServerModeCommand.cs @@ -59,33 +59,29 @@ public ServerModeCommand(IToolInteractiveService interactiveService, int port, i } else { - var monitorTask = WaitOnParentPid(cancellationToken); - var webTask = host.RunAsync(cancellationToken); - - //This call will wait until one of the tasks completes. - //The monitor task will complete if a parent pid is not found. - await Task.WhenAny(monitorTask, webTask); - - //The monitor task is expected to complete only when a parent pid is not found. - if (monitorTask.IsCompleted && monitorTask.Result) + try { - _interactiveService.WriteLine(string.Empty); - _interactiveService.WriteLine("The parent process is no longer running."); - _interactiveService.WriteLine("Server mode is shutting down..."); - await host.StopAsync(cancellationToken); + var process = Process.GetProcessById((int)_parentPid); + process.EnableRaisingEvents = true; + process.Exited += async (sender, args) => { await ShutDownHost(host, cancellationToken); }; } - - //If the web task completes with a fault because an exception was thrown, - //We need to capture the inner exception and rethrow it so it can bubble up to the end user. - if (webTask.IsCompleted && webTask.IsFaulted) + catch (Exception) { - var innerException = webTask.Exception?.InnerException; - if (innerException != null) - throw innerException; + return; } + + await host.RunAsync(cancellationToken); } } + private async Task ShutDownHost(IWebHost host, CancellationToken cancellationToken) + { + _interactiveService.WriteLine(string.Empty); + _interactiveService.WriteLine("The parent process is no longer running."); + _interactiveService.WriteLine("Server mode is shutting down..."); + await host.StopAsync(cancellationToken); + } + private IEncryptionProvider CreateEncryptionProvider() { IEncryptionProvider encryptionProvider; @@ -127,25 +123,6 @@ private IEncryptionProvider CreateEncryptionProvider() return encryptionProvider; } - private async Task WaitOnParentPid(CancellationToken token) - { - if (_parentPid == null) - return true; - - while (true) - { - try - { - Process.GetProcessById((int)_parentPid); - await Task.Delay(1000, token); - } - catch - { - return true; - } - } - } - private bool IsPortInUse(int port) { var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); From 371dee1e86a75d5de99131f5602634b6ce8c60d1 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Mon, 12 Jul 2021 14:42:29 -0700 Subject: [PATCH 7/7] feat: add GetRecipe API to describe Recipes returned by other APIs --- .../Controllers/RecipeController.cs | 51 ++++++++ .../ServerMode/Models/RecipeSummary.cs | 33 +++++ src/AWS.Deploy.Orchestration/Exceptions.cs | 9 ++ src/AWS.Deploy.Orchestration/RecipeHandler.cs | 44 +++++++ src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 123 ++++++++++++++++++ .../ServerModeTests.cs | 30 +++++ 6 files changed, 290 insertions(+) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/RecipeSummary.cs create mode 100644 src/AWS.Deploy.Orchestration/RecipeHandler.cs diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs new file mode 100644 index 000000000..36b8d67ff --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using AWS.Deploy.CLI.ServerMode.Models; +using AWS.Deploy.Orchestration; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace AWS.Deploy.CLI.ServerMode.Controllers +{ + [Produces("application/json")] + [ApiController] + [Route("api/v1/[controller]")] + public class RecipeController : ControllerBase + { + /// + /// Gets a summary of the specified Recipe. + /// + [HttpGet("{recipeId}")] + [SwaggerOperation(OperationId = "GetRecipe")] + [SwaggerResponse(200, type: typeof(RecipeSummary))] + public IActionResult GetRecipe(string recipeId, [FromQuery] string? projectPath = null) + { + if (string.IsNullOrEmpty(recipeId)) + { + return BadRequest($"A Recipe ID was not provided."); + } + + var recipeDefinitions = RecipeHandler.GetRecipeDefinitions(); + var selectedRecipeDefinition = recipeDefinitions.FirstOrDefault(x => x.Id.Equals(recipeId)); + + if (selectedRecipeDefinition == null) + { + return BadRequest($"Recipe ID {recipeId} not found."); + } + + var output = new RecipeSummary( + selectedRecipeDefinition.Id, + selectedRecipeDefinition.Version, + selectedRecipeDefinition.Name, + selectedRecipeDefinition.Description, + selectedRecipeDefinition.TargetService, + selectedRecipeDefinition.DeploymentType.ToString(), + selectedRecipeDefinition.DeploymentBundle.ToString() + ); + + return Ok(output); + } + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/RecipeSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/RecipeSummary.cs new file mode 100644 index 000000000..174332a1c --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/RecipeSummary.cs @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + public class RecipeSummary + { + public string Id { get; set; } + + public string Version { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string TargetService { get; set; } + + public string DeploymentType { get; set; } + + public string DeploymentBundle { get; set; } + + public RecipeSummary(string id, string version, string name, string description, string targetService, string deploymentType, string deploymentBundle) + { + Id = id; + Version = version; + Name = name; + Description = description; + TargetService = targetService; + DeploymentType = deploymentType; + DeploymentBundle = deploymentBundle; + } + } +} diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 713965c40..cc9bcc7da 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -93,6 +93,15 @@ public class NoDeploymentBundleDefinitionsFoundException : Exception public NoDeploymentBundleDefinitionsFoundException(string message, Exception? innerException = null) : base(message, innerException) { } } + /// + /// Exception is thrown if we cannot retrieve recipe definitions + /// + [AWSDeploymentExpectedException] + public class NoRecipeDefinitionsFoundException : Exception + { + public NoRecipeDefinitionsFoundException(string message, Exception? innerException = null) : base(message, innerException) { } + } + /// /// Exception is thrown if dotnet publish attempt failed /// diff --git a/src/AWS.Deploy.Orchestration/RecipeHandler.cs b/src/AWS.Deploy.Orchestration/RecipeHandler.cs new file mode 100644 index 000000000..dc7d17d34 --- /dev/null +++ b/src/AWS.Deploy.Orchestration/RecipeHandler.cs @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Recipes; +using Newtonsoft.Json; + +namespace AWS.Deploy.Orchestration +{ + public class RecipeHandler + { + public static List GetRecipeDefinitions() + { + var recipeDefinitionsPath = RecipeLocator.FindRecipeDefinitionsPath(); + var recipeDefinitions = new List(); + + try + { + foreach (var recipeDefinitionFile in Directory.GetFiles(recipeDefinitionsPath, "*.recipe", SearchOption.TopDirectoryOnly)) + { + try + { + var content = File.ReadAllText(recipeDefinitionFile); + var definition = JsonConvert.DeserializeObject(content); + recipeDefinitions.Add(definition); + } + catch (Exception e) + { + throw new Exception($"Failed to Deserialize Recipe Definition [{recipeDefinitionFile}]: {e.Message}", e); + } + } + } + catch(IOException) + { + throw new NoRecipeDefinitionsFoundException("Failed to find recipe definitions"); + } + + return recipeDefinitions; + } + } +} diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index 6c28f6a2b..6a13cbcbc 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -132,6 +132,17 @@ public partial interface IRestAPIClient /// A server side error occurred. System.Threading.Tasks.Task HealthAsync(System.Threading.CancellationToken cancellationToken); + /// Gets a summary of the specified Recipe. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetRecipeAsync(string recipeId, string projectPath); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Gets a summary of the specified Recipe. + /// Success + /// A server side error occurred. + System.Threading.Tasks.Task GetRecipeAsync(string recipeId, string projectPath, System.Threading.CancellationToken cancellationToken); + } [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v12.0.0.0))")] @@ -965,6 +976,91 @@ public async System.Threading.Tasks.Task HealthAsync(System. } } + /// Gets a summary of the specified Recipe. + /// Success + /// A server side error occurred. + public System.Threading.Tasks.Task GetRecipeAsync(string recipeId, string projectPath) + { + return GetRecipeAsync(recipeId, projectPath, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Gets a summary of the specified Recipe. + /// Success + /// A server side error occurred. + public async System.Threading.Tasks.Task GetRecipeAsync(string recipeId, string projectPath, System.Threading.CancellationToken cancellationToken) + { + if (recipeId == null) + throw new System.ArgumentNullException("recipeId"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v1/Recipe/{recipeId}?"); + urlBuilder_.Replace("{recipeId}", System.Uri.EscapeDataString(ConvertToString(recipeId, System.Globalization.CultureInfo.InvariantCulture))); + if (projectPath != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("projectPath") + "=").Append(System.Uri.EscapeDataString(ConvertToString(projectPath, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + protected struct ObjectResponseResult { public ObjectResponseResult(T responseObject, string responseText) @@ -1193,6 +1289,33 @@ public partial class OptionSettingItemSummary public System.Collections.Generic.ICollection ChildOptionSettings { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] + public partial class RecipeSummary + { + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Id { get; set; } + + [Newtonsoft.Json.JsonProperty("version", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Version { get; set; } + + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Description { get; set; } + + [Newtonsoft.Json.JsonProperty("targetService", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string TargetService { get; set; } + + [Newtonsoft.Json.JsonProperty("deploymentType", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string DeploymentType { get; set; } + + [Newtonsoft.Json.JsonProperty("deploymentBundle", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string DeploymentBundle { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v12.0.0.0)")] diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index f5ef90a8d..67bea70c9 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -1,8 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System.Linq; using System.Threading.Tasks; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.ServerMode.Controllers; +using AWS.Deploy.CLI.ServerMode.Models; +using AWS.Deploy.Orchestration; +using Microsoft.AspNetCore.Mvc; using Xunit; namespace AWS.Deploy.CLI.UnitTests @@ -30,5 +35,30 @@ public async Task TcpPortIsInUseTest() Assert.IsType(serverModeTask2.Exception.InnerException); } + + [Theory] + [InlineData("")] + [InlineData("InvalidId")] + public void RecipeController_GetRecipe_EmptyId(string recipeId) + { + var recipeController = new RecipeController(); + var response = recipeController.GetRecipe(recipeId); + + Assert.IsType(response); + } + + [Fact] + public void RecipeController_GetRecipe_HappyPath() + { + var recipeController = new RecipeController(); + var recipeDefinitions = RecipeHandler.GetRecipeDefinitions(); + var recipe = recipeDefinitions.First(); + + var response = recipeController.GetRecipe(recipe.Id); + + var result = Assert.IsType(response); + var resultRecipe = Assert.IsType(result.Value); + Assert.Equal(recipe.Id, resultRecipe.Id); + } } }