From 4ee1b6df4d30a82b6f1f8200341701ca253bd22f Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Thu, 12 May 2022 14:46:26 +0000 Subject: [PATCH 1/8] build: version bump to 0.45 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index c64fac87c..4cf1ffd91 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.44", + "version": "0.45", "publicReleaseRefSpec": [ ".*" ], From 645e780a994b6dcdea068af270df5c47eb55bc72 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Fri, 13 May 2022 11:26:02 -0400 Subject: [PATCH 2/8] chore: add process exit code to improve telemetry of failed processes --- .../Controllers/DeploymentController.cs | 2 +- .../ServerMode/ExtensionMethods.cs | 3 ++- .../Models/DeployToolExceptionSummary.cs | 4 +++- src/AWS.Deploy.Common/Exceptions.cs | 9 ++++++--- .../CdkProjectHandler.cs | 14 +++++++------- .../DeploymentBundleHandler.cs | 12 ++++++------ src/AWS.Deploy.Orchestration/Exceptions.cs | 16 ++++++++-------- src/AWS.Deploy.Orchestration/Orchestrator.cs | 4 ++-- .../Utilities/ZipFileManager.cs | 4 ++-- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 3 +++ 10 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 86f6b8028..c3ac6105b 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -583,7 +583,7 @@ public IActionResult GetDeploymentStatus(string sessionId) var innerException = state.DeploymentTask.Exception.InnerException; if (innerException is DeployToolException deployToolException) { - output.Exception = new DeployToolExceptionSummary(deployToolException.ErrorCode.ToString(), deployToolException.Message); + output.Exception = new DeployToolExceptionSummary(deployToolException.ErrorCode.ToString(), deployToolException.Message, deployToolException.ProcessExitCode); } else { diff --git a/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs b/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs index 95eb368ea..88afe9c88 100644 --- a/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs +++ b/src/AWS.Deploy.CLI/ServerMode/ExtensionMethods.cs @@ -59,7 +59,8 @@ public static void ConfigureExceptionHandler(this IApplicationBuilder app) exceptionString = JsonSerializer.Serialize( new DeployToolExceptionSummary( deployToolException.ErrorCode.ToString(), - deployToolException.Message)); + deployToolException.Message, + deployToolException.ProcessExitCode)); } else { diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/DeployToolExceptionSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/DeployToolExceptionSummary.cs index 93e641ca2..b5c1bc58f 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/DeployToolExceptionSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/DeployToolExceptionSummary.cs @@ -7,11 +7,13 @@ public class DeployToolExceptionSummary { public string ErrorCode { get; set; } public string Message { get; set; } + public int? ProcessExitCode { get; set; } - public DeployToolExceptionSummary(string errorCode, string message) + public DeployToolExceptionSummary(string errorCode, string message, int? processExitCode = null) { ErrorCode = errorCode; Message = message; + ProcessExitCode = processExitCode; } } } diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index b5df6d0ad..aea70ed5f 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -9,10 +9,12 @@ namespace AWS.Deploy.Common public abstract class DeployToolException : Exception { public DeployToolErrorCode ErrorCode { get; set; } + public int? ProcessExitCode { get; set; } - public DeployToolException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(message, innerException) + public DeployToolException(DeployToolErrorCode errorCode, string message, Exception? innerException = null, int? processExitCode = null) : base(message, innerException) { ErrorCode = errorCode; + ProcessExitCode = processExitCode; } } @@ -110,7 +112,8 @@ public enum DeployToolErrorCode FailedToRunCDKDiff = 10009000, FailedToCreateCDKProject = 10009100, ResourceQuery = 10009200, - FailedToRetrieveStackId = 10009300 + FailedToRetrieveStackId = 10009300, + FailedToGetECRAuthorizationToken = 10009400 } public class ProjectFileNotFoundException : DeployToolException @@ -183,7 +186,7 @@ public ParsingExistingCloudApplicationMetadataException(DeployToolErrorCode erro /// public class FailedToCreateDeploymentBundleException : DeployToolException { - public FailedToCreateDeploymentBundleException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public FailedToCreateDeploymentBundleException(DeployToolErrorCode errorCode, string message, int? processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index ce979c2ab..6993eba8e 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -91,7 +91,7 @@ public async Task PerformCdkDiff(string cdkProjectPath, CloudApplication needAwsCredentials: true); if (cdkDiff.ExitCode != 0) - throw new FailedToRunCDKDiffException(DeployToolErrorCode.FailedToRunCDKDiff, "The CDK Diff command encountered an error and failed."); + throw new FailedToRunCDKDiffException(DeployToolErrorCode.FailedToRunCDKDiff, "The CDK Diff command encountered an error and failed.", cdkDiff.ExitCode); var templateFilePath = Path.Combine(cdkProjectPath, "cdk.out", $"{cloudApplication.Name}.template.json"); return await _fileManager.ReadAllTextAsync(templateFilePath); @@ -117,7 +117,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication streamOutputToInteractiveService: true); if (cdkBootstrap.ExitCode != 0) - throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToRunCDKBootstrap, "The AWS CDK Bootstrap, which is the process of provisioning initial resources for the deployment environment, has failed. Please review the output above for additional details [and check out our troubleshooting guide for the most common failure reasons]. You can learn more about CDK bootstrapping at https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html."); + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToRunCDKBootstrap, "The AWS CDK Bootstrap, which is the process of provisioning initial resources for the deployment environment, has failed. Please review the output above for additional details [and check out our troubleshooting guide for the most common failure reasons]. You can learn more about CDK bootstrapping at https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html.", cdkBootstrap.ExitCode); } else { @@ -156,7 +156,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication cdkDeploy = await cdkDeployTask; // We recapture the deployment end date at this point after the deployment task completes. deploymentEndDate = DateTime.UtcNow; - await CheckCdkDeploymentFailure(stackId, deploymentStartDate, deploymentEndDate); + await CheckCdkDeploymentFailure(stackId, deploymentStartDate, deploymentEndDate, cdkDeploy); } else { @@ -169,7 +169,7 @@ public async Task DeployCdkProject(OrchestratorSession session, CloudApplication var deploymentTotalTime = Math.Round((deploymentEndDate - deploymentStartDate).TotalSeconds, 2); if (cdkDeploy.ExitCode != 0) - throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, $"We had an issue deploying your application to AWS. Check the deployment output for more details. Deployment took {deploymentTotalTime}s."); + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, $"We had an issue deploying your application to AWS. Check the deployment output for more details. Deployment took {deploymentTotalTime}s.", cdkDeploy.ExitCode); } public async Task DetermineIfCDKBootstrapShouldRun() @@ -219,7 +219,7 @@ await WaitUntilHelper.WaitUntil(async () => return stack?.StackId ?? throw new ResourceQueryException(DeployToolErrorCode.FailedToRetrieveStackId, "We were unable to retrieve the CloudFormation stack identifier."); } - private async Task CheckCdkDeploymentFailure(string stackId, DateTime deploymentStartDate, DateTime deploymentEndDate) + private async Task CheckCdkDeploymentFailure(string stackId, DateTime deploymentStartDate, DateTime deploymentEndDate, TryRunResult cdkDeployResult) { try { @@ -239,13 +239,13 @@ private async Task CheckCdkDeploymentFailure(string stackId, DateTime deployment if (failedEvents.Any()) { var errors = string.Join(". ", failedEvents.Reverse().Select(x => x.ResourceStatusReason)); - throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, errors); + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToDeployCdkApplication, errors, cdkDeployResult.ExitCode); } } catch (ResourceQueryException exception) when (exception.InnerException != null && exception.InnerException.Message.Equals($"Stack [{stackId}] does not exist")) { var deploymentTotalTime = Math.Round((deploymentEndDate - deploymentStartDate).TotalSeconds, 2); - throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToCreateCdkStack, $"A CloudFormation stack was not created. Check the deployment output for more details. Deployment took {deploymentTotalTime}s."); + throw new FailedToDeployCDKAppException(DeployToolErrorCode.FailedToCreateCdkStack, $"A CloudFormation stack was not created. Check the deployment output for more details. Deployment took {deploymentTotalTime}s.", cdkDeployResult.ExitCode); } } diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 7142c9914..68a47975f 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -68,7 +68,7 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Recommenda errorMessage += $"{Environment.NewLine}Docker builds usually fail due to executing them from a working directory that is incompatible with the Dockerfile."; errorMessage += $"{Environment.NewLine}You can try setting the 'Docker Execution Directory' in the option settings."; - throw new DockerBuildFailedException(DeployToolErrorCode.DockerBuildFailed, errorMessage); + throw new DockerBuildFailedException(DeployToolErrorCode.DockerBuildFailed, errorMessage, result.ExitCode); } } @@ -125,7 +125,7 @@ public async Task CreateDotnetPublishZip(Recommendation recommendation) if (!string.IsNullOrEmpty(result.StandardError)) errorMessage = $"We were unable to package the application using 'dotnet publish' due to the following error:{Environment.NewLine}{result.StandardError}"; - throw new DotnetPublishFailedException(DeployToolErrorCode.DotnetPublishFailed, errorMessage); + throw new DotnetPublishFailedException(DeployToolErrorCode.DotnetPublishFailed, errorMessage, result.ExitCode); } var zipFilePath = $"{publishDirectoryInfo.FullName}.zip"; @@ -200,7 +200,7 @@ private async Task InitiateDockerLogin() var authorizationTokens = await _awsResourceQueryer.GetECRAuthorizationToken(); if (authorizationTokens.Count == 0) - throw new DockerLoginFailedException(DeployToolErrorCode.DockerLoginFailed, "Failed to login to Docker"); + throw new DockerLoginFailedException(DeployToolErrorCode.FailedToGetECRAuthorizationToken, "Failed to login to Docker", null); var authTokenBytes = Convert.FromBase64String(authorizationTokens[0].AuthorizationToken); var authToken = Encoding.UTF8.GetString(authTokenBytes); @@ -214,7 +214,7 @@ private async Task InitiateDockerLogin() var errorMessage = "Failed to login to Docker"; if (!string.IsNullOrEmpty(result.StandardError)) errorMessage = $"Failed to login to Docker due to the following reason:{Environment.NewLine}{result.StandardError}"; - throw new DockerLoginFailedException(DeployToolErrorCode.DockerLoginFailed, errorMessage); + throw new DockerLoginFailedException(DeployToolErrorCode.DockerLoginFailed, errorMessage, result.ExitCode); } } @@ -242,7 +242,7 @@ private async Task TagDockerImage(string sourceTagName, string targetTagName) var errorMessage = "Failed to tag Docker image"; if (!string.IsNullOrEmpty(result.StandardError)) errorMessage = $"Failed to tag Docker Image due to the following reason:{Environment.NewLine}{result.StandardError}"; - throw new DockerTagFailedException(DeployToolErrorCode.DockerTagFailed, errorMessage); + throw new DockerTagFailedException(DeployToolErrorCode.DockerTagFailed, errorMessage, result.ExitCode); } } @@ -256,7 +256,7 @@ private async Task PushDockerImage(string targetTagName) var errorMessage = "Failed to push Docker Image"; if (!string.IsNullOrEmpty(result.StandardError)) errorMessage = $"Failed to push Docker Image due to the following reason:{Environment.NewLine}{result.StandardError}"; - throw new DockerPushFailedException(DeployToolErrorCode.DockerPushFailed, errorMessage); + throw new DockerPushFailedException(DeployToolErrorCode.DockerPushFailed, errorMessage, result.ExitCode); } } } diff --git a/src/AWS.Deploy.Orchestration/Exceptions.cs b/src/AWS.Deploy.Orchestration/Exceptions.cs index 41271992e..b800fe264 100644 --- a/src/AWS.Deploy.Orchestration/Exceptions.cs +++ b/src/AWS.Deploy.Orchestration/Exceptions.cs @@ -42,7 +42,7 @@ public PackageJsonFileException(DeployToolErrorCode errorCode, string message, E /// public class DockerBuildFailedException : DeployToolException { - public DockerBuildFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public DockerBuildFailedException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -58,7 +58,7 @@ public NPMCommandFailedException(DeployToolErrorCode errorCode, string message, /// public class DockerLoginFailedException : DeployToolException { - public DockerLoginFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public DockerLoginFailedException(DeployToolErrorCode errorCode, string message, int? processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -66,7 +66,7 @@ public DockerLoginFailedException(DeployToolErrorCode errorCode, string message, /// public class DockerTagFailedException : DeployToolException { - public DockerTagFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public DockerTagFailedException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -74,7 +74,7 @@ public DockerTagFailedException(DeployToolErrorCode errorCode, string message, E /// public class DockerPushFailedException : DeployToolException { - public DockerPushFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public DockerPushFailedException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -90,7 +90,7 @@ public NoRecipeDefinitionsFoundException(DeployToolErrorCode errorCode, string m /// public class DotnetPublishFailedException : DeployToolException { - public DotnetPublishFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public DotnetPublishFailedException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -98,7 +98,7 @@ public DotnetPublishFailedException(DeployToolErrorCode errorCode, string messag /// public class FailedToCreateZipFileException : DeployToolException { - public FailedToCreateZipFileException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public FailedToCreateZipFileException(DeployToolErrorCode errorCode, string message, int? processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -138,7 +138,7 @@ public InvalidAWSDeployRecipesCDKCommonVersionException(DeployToolErrorCode erro /// public class FailedToDeployCDKAppException : DeployToolException { - public FailedToDeployCDKAppException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public FailedToDeployCDKAppException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// @@ -146,7 +146,7 @@ public FailedToDeployCDKAppException(DeployToolErrorCode errorCode, string messa /// public class FailedToRunCDKDiffException : DeployToolException { - public FailedToRunCDKDiffException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + public FailedToRunCDKDiffException(DeployToolErrorCode errorCode, string message, int processExitCode, Exception? innerException = null) : base(errorCode, message, innerException, processExitCode) { } } /// diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index b9008d556..0bbdb0d1b 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -210,7 +210,7 @@ public async Task CreateDeploymentBundle(CloudApplication cloudApplication, Reco } catch (DeployToolException ex) { - throw new FailedToCreateDeploymentBundleException(ex.ErrorCode, ex.Message, ex); + throw new FailedToCreateDeploymentBundleException(ex.ErrorCode, ex.Message, ex.ProcessExitCode, ex); } } else if (recommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.DotnetPublishZipFile) @@ -223,7 +223,7 @@ public async Task CreateDeploymentBundle(CloudApplication cloudApplication, Reco } catch (DeployToolException ex) { - throw new FailedToCreateDeploymentBundleException(ex.ErrorCode, ex.Message, ex); + throw new FailedToCreateDeploymentBundleException(ex.ErrorCode, ex.Message, ex.ProcessExitCode, ex); } } } diff --git a/src/AWS.Deploy.Orchestration/Utilities/ZipFileManager.cs b/src/AWS.Deploy.Orchestration/Utilities/ZipFileManager.cs index 20d2156f7..352af439b 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/ZipFileManager.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/ZipFileManager.cs @@ -49,7 +49,7 @@ private async Task BuildZipForLinux(string sourceDirectoryName, string destinati var zipCLI = FindExecutableInPath("zip"); if (string.IsNullOrEmpty(zipCLI)) - throw new FailedToCreateZipFileException(DeployToolErrorCode.FailedToFindZipUtility, "Failed to find the \"zip\" utility program in path. This program is required to maintain Linux file permissions in the zip archive."); + throw new FailedToCreateZipFileException(DeployToolErrorCode.FailedToFindZipUtility, "Failed to find the \"zip\" utility program in path. This program is required to maintain Linux file permissions in the zip archive.", null); var args = new StringBuilder($"\"{destinationArchiveFileName}\""); @@ -68,7 +68,7 @@ private async Task BuildZipForLinux(string sourceDirectoryName, string destinati errorMessage = $"We were unable to create a zip archive of the packaged application due to the following reason:{Environment.NewLine}{result.StandardError}"; errorMessage += $"{Environment.NewLine}Normally this indicates a problem running the \"zip\" utility. Make sure that application is installed and available in your PATH."; - throw new FailedToCreateZipFileException(DeployToolErrorCode.ZipUtilityFailedToZip, errorMessage); + throw new FailedToCreateZipFileException(DeployToolErrorCode.ZipUtilityFailedToZip, errorMessage, result.ExitCode); } } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index b0cbfb8d9..6a7d0dd53 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1817,6 +1817,9 @@ public partial class DeployToolExceptionSummary [Newtonsoft.Json.JsonProperty("message", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Message { get; set; } + [Newtonsoft.Json.JsonProperty("processExitCode", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public int? ProcessExitCode { get; set; } + } From 0ff83fface7271d8f197f57037a6f76df19569fd Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Thu, 12 May 2022 11:36:23 -0400 Subject: [PATCH 3/8] chore: Add constructor injection support to option and recipe validators --- docs/OptionSettingsItems-input-validation.md | 22 +++--- src/AWS.Deploy.CLI/Commands/CommandFactory.cs | 9 ++- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 15 ++-- .../CustomServiceCollectionExtension.cs | 2 + .../OptionSettingItem.ValueOverride.cs | 4 +- .../Recipes/OptionSettingItem.cs | 7 +- .../Recipes/Validation/IRecipeValidator.cs | 2 +- .../FargateTaskCpuMemorySizeValidator.cs | 13 +++- .../MinMaxConstraintValidator.cs | 13 +++- .../Recipes/Validation/ValidatorFactory.cs | 72 +++++++++++++++++-- .../OptionSettingHandler.cs | 10 ++- ...pRunnerOptionSettingItemValidationTests.cs | 7 +- ...FargateOptionSettingItemValidationTests.cs | 6 +- ...anStalkOptionSettingItemValidationTests.cs | 6 +- .../OptionSettingsItemValidationTests.cs | 6 +- .../Validation/ValidatorFactoryTests.cs | 25 +++++-- .../RecommendationTests.cs | 3 +- .../ApplyPreviousSettingsTests.cs | 6 +- .../ConsoleUtilitiesTests.cs | 20 +++--- .../GetOptionSettingTests.cs | 6 +- .../RecommendationTests.cs | 6 +- .../SetOptionSettingTests.cs | 6 +- .../ExistingSecurityGroubsCommandTest.cs | 6 +- .../ExistingSubnetsCommandTest.cs | 6 +- .../ExistingVpcCommandTest.cs | 6 +- .../VPCConnectorCommandTest.cs | 6 +- .../AWS.Deploy.CLI.UnitTests/TypeHintTests.cs | 17 +++-- .../CDK/CDKProjectHandlerTests.cs | 5 +- .../ElasticBeanstalkHandlerTests.cs | 5 +- 29 files changed, 242 insertions(+), 75 deletions(-) diff --git a/docs/OptionSettingsItems-input-validation.md b/docs/OptionSettingsItems-input-validation.md index 50fb2294e..dad54ec0e 100644 --- a/docs/OptionSettingsItems-input-validation.md +++ b/docs/OptionSettingsItems-input-validation.md @@ -141,18 +141,24 @@ In the `RangeValidator` example above, a recipe author can customize `Validation ### Dependencies -Because Validators will be deserialized as part of a `RecipeDefinition` they need to have parameterless constructors and therefore can't use Constructor Injection. +Validators may require other services during validation. For example, an option that selects a file path may need an `IFileManager` to validate that it exists. -Validators are currently envisoned to be relatively simple to the point where they shouldn't need any dependencies. If dependencies in are needed in the future, we can explore adding an `Initialize` method that uses the ServiceLocation (anti-)pattern: +When deserializing and initializing validators from the `RecipeDefinition` we shall inject any required services into their constructor via an `IServiceProvider` created from the collection of the Deploy Tool's custom services. ```csharp -public interface IOptionSettingItemValidator +public class FileExistsValidator : IOptionSettingItemValidator { - /// - /// One possibile solution if we need to create a Validator that needs - /// dependencies. - /// - void Initialize(IServiceLocator serviceLocator); + private readonly IFileManager _fileManager; + + public FileExistsValidator(IFileManager fileManager) + { + _fileManager = fileManager; + } + + public ValidationResult Validate(object input) + { + // Validate that the provided file path is valid + } } ``` diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index bf8f449ab..c85ab5bac 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -25,6 +25,7 @@ using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.ServiceHandlers; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.CLI.Commands { @@ -74,6 +75,7 @@ public class CommandFactory : ICommandFactory private readonly ICDKVersionDetector _cdkVersionDetector; private readonly IAWSServiceHandler _awsServiceHandler; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IValidatorFactory _validatorFactory; public CommandFactory( IServiceProvider serviceProvider, @@ -101,7 +103,8 @@ public CommandFactory( ILocalUserSettingsEngine localUserSettingsEngine, ICDKVersionDetector cdkVersionDetector, IAWSServiceHandler awsServiceHandler, - IOptionSettingHandler optionSettingHandler) + IOptionSettingHandler optionSettingHandler, + IValidatorFactory validatorFactory) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -129,6 +132,7 @@ public CommandFactory( _cdkVersionDetector = cdkVersionDetector; _awsServiceHandler = awsServiceHandler; _optionSettingHandler = optionSettingHandler; + _validatorFactory = validatorFactory; } public Command BuildRootCommand() @@ -230,7 +234,8 @@ private Command BuildDeployCommand() _directoryManager, _fileManager, _awsServiceHandler, - _optionSettingHandler); + _optionSettingHandler, + _validatorFactory); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; if (!string.IsNullOrEmpty(deploymentProjectPath)) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 878f4c276..837936ba2 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -52,6 +52,7 @@ public class DeployCommand private readonly ICDKVersionDetector _cdkVersionDetector; private readonly IAWSServiceHandler _awsServiceHandler; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IValidatorFactory _validatorFactory; public DeployCommand( IServiceProvider serviceProvider, @@ -76,7 +77,8 @@ public DeployCommand( IDirectoryManager directoryManager, IFileManager fileManager, IAWSServiceHandler awsServiceHandler, - IOptionSettingHandler optionSettingHandler) + IOptionSettingHandler optionSettingHandler, + IValidatorFactory validatorFactory) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -101,6 +103,7 @@ public DeployCommand( _systemCapabilityEvaluator = systemCapabilityEvaluator; _awsServiceHandler = awsServiceHandler; _optionSettingHandler = optionSettingHandler; + _validatorFactory = validatorFactory; } public async Task ExecuteAsync(string applicationName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) @@ -458,9 +461,8 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us } var validatorFailedResults = - recommendation.Recipe - .BuildValidators() - .Select(validator => validator.Validate(recommendation, _session, _optionSettingHandler)) + _validatorFactory.BuildValidators(recommendation.Recipe) + .Select(validator => validator.Validate(recommendation, _session)) .Where(x => !x.IsValid) .ToList(); @@ -768,9 +770,8 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEn if (string.IsNullOrEmpty(input)) { var validatorFailedResults = - recommendation.Recipe - .BuildValidators() - .Select(validator => validator.Validate(recommendation, _session, _optionSettingHandler)) + _validatorFactory.BuildValidators(recommendation.Recipe) + .Select(validator => validator.Validate(recommendation, _session)) .Where(x => !x.IsValid) .ToList(); diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 52791f41b..770ed5940 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -9,6 +9,7 @@ using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; @@ -64,6 +65,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IS3Handler), typeof(AWSS3Handler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IElasticBeanstalkHandler), typeof(AWSElasticBeanstalkHandler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IOptionSettingHandler), typeof(OptionSettingHandler), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IValidatorFactory), typeof(ValidatorFactory), lifetime)); var packageJsonTemplate = typeof(PackageJsonGenerator).Assembly.ReadEmbeddedFile(PackageJsonGenerator.TemplateIdentifier); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IPackageJsonGenerator), (serviceProvider) => new PackageJsonGenerator(packageJsonTemplate), lifetime)); diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 009aa3cdf..63ab219a5 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -92,11 +92,11 @@ public object GetValue(IDictionary replacementTokens, IDictionar /// Thrown if one or more determine /// is not valid. /// - public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride) + public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride, IOptionSettingItemValidator[] validators) { var isValid = true; var validationFailedMessage = string.Empty; - foreach (var validator in this.BuildValidators()) + foreach (var validator in validators) { var result = validator.Validate(valueOverride); if (!result.IsValid) diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index d3dba87af..a9a14cbe9 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -38,9 +38,10 @@ public interface IOptionSettingItem /// /// Set the value of an while validating the provided input. /// - /// - /// - void SetValue(IOptionSettingHandler optionSettingHandler, object value); + /// Handler use to set any child option settings + /// Value to set + /// /// Validators for this item + void SetValue(IOptionSettingHandler optionSettingHandler, object value, IOptionSettingItemValidator[] validators); } /// diff --git a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs index c7676e73a..8eb7a3c31 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs @@ -10,6 +10,6 @@ namespace AWS.Deploy.Common.Recipes.Validation /// public interface IRecipeValidator { - ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext, IOptionSettingHandler optionSettingHandler); + ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs index 9d0128a6f..097291d78 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs @@ -20,6 +20,13 @@ namespace AWS.Deploy.Common.Recipes.Validation /// public class FargateTaskCpuMemorySizeValidator : IRecipeValidator { + private readonly IOptionSettingHandler _optionSettingHandler; + + public FargateTaskCpuMemorySizeValidator(IOptionSettingHandler optionSettingHandler) + { + _optionSettingHandler = optionSettingHandler; + } + private readonly Dictionary _cpuMemoryMap = new() { { "256", new[] { "512", "1024", "2048" } }, @@ -51,15 +58,15 @@ private static IEnumerable BuildMemoryArray(int start, int end, int incr public string? InvalidCpuValueValidationFailedMessage { get; set; } /// - public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext, IOptionSettingHandler optionSettingHandler) + public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { string cpu; string memory; try { - cpu = optionSettingHandler.GetOptionSettingValue(recommendation, optionSettingHandler.GetOptionSetting(recommendation, CpuOptionSettingsId)); - memory = optionSettingHandler.GetOptionSettingValue(recommendation, optionSettingHandler.GetOptionSetting(recommendation, MemoryOptionSettingsId)); + cpu = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, CpuOptionSettingsId)); + memory = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, MemoryOptionSettingsId)); } catch (OptionSettingItemDoesNotExistException) { diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs index a4fd6a80a..53659f9ad 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs @@ -10,19 +10,26 @@ namespace AWS.Deploy.Common.Recipes.Validation /// public class MinMaxConstraintValidator : IRecipeValidator { + private readonly IOptionSettingHandler _optionSettingHandler; + + public MinMaxConstraintValidator(IOptionSettingHandler optionSettingHandler) + { + _optionSettingHandler = optionSettingHandler; + } + public string MinValueOptionSettingsId { get; set; } = string.Empty; public string MaxValueOptionSettingsId { get; set; } = string.Empty; public string ValidationFailedMessage { get; set; } = "The value specified for {{MinValueOptionSettingsId}} must be less than or equal to the value specified for {{MaxValueOptionSettingsId}}"; - public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext, IOptionSettingHandler optionSettingHandler) + public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { double minVal; double maxValue; try { - minVal = optionSettingHandler.GetOptionSettingValue(recommendation, optionSettingHandler.GetOptionSetting(recommendation, MinValueOptionSettingsId)); - maxValue = optionSettingHandler.GetOptionSettingValue(recommendation, optionSettingHandler.GetOptionSetting(recommendation, MaxValueOptionSettingsId)); + minVal = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, MinValueOptionSettingsId)); + maxValue = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, MaxValueOptionSettingsId)); } catch (OptionSettingItemDoesNotExistException) { diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 2fc086fe3..5c0abd807 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -4,15 +4,45 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace AWS.Deploy.Common.Recipes.Validation { + /// + /// Factory that builds the validators for a given option or recipe + /// + public interface IValidatorFactory + { + /// + /// Builds the validators that apply to the given option + /// + /// Option to validate + /// Array of validators for the given option + IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem); + + /// + /// Builds the validators that apply to the given recipe + /// + /// Recipe to validate + /// Array of validators for the given recipe + IRecipeValidator[] BuildValidators(RecipeDefinition recipeDefinition); + } + /// /// Builds and instances. /// - public static class ValidatorFactory + public class ValidatorFactory : IValidatorFactory { + private readonly IServiceProvider _serviceProvider; + + public ValidatorFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + private static readonly Dictionary _optionSettingItemValidatorTypeMapping = new() { { OptionSettingItemValidatorList.Range, typeof(RangeValidator) }, @@ -26,7 +56,7 @@ public static class ValidatorFactory { RecipeValidatorList.MinMaxConstraint, typeof(MinMaxConstraintValidator) } }; - public static IOptionSettingItemValidator[] BuildValidators(this OptionSettingItem optionSettingItem) + public IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem) { return optionSettingItem.Validators .Select(v => Activate(v.ValidatorType, v.Configuration, _optionSettingItemValidatorTypeMapping)) @@ -34,7 +64,7 @@ public static IOptionSettingItemValidator[] BuildValidators(this OptionSettingIt .ToArray(); } - public static IRecipeValidator[] BuildValidators(this RecipeDefinition recipeDefinition) + public IRecipeValidator[] BuildValidators(RecipeDefinition recipeDefinition) { return recipeDefinition.Validators .Select(v => Activate(v.ValidatorType, v.Configuration,_recipeValidatorTypeMapping)) @@ -42,11 +72,11 @@ public static IRecipeValidator[] BuildValidators(this RecipeDefinition recipeDef .ToArray(); } - private static object? Activate(TValidatorList validatorType, object? configuration, Dictionary typeMappings) where TValidatorList : struct + private object? Activate(TValidatorList validatorType, object? configuration, Dictionary typeMappings) where TValidatorList : struct { if (null == configuration) { - var validatorInstance = Activator.CreateInstance(typeMappings[validatorType]); + var validatorInstance = ActivatorUtilities.CreateInstance(_serviceProvider, typeMappings[validatorType]); if (validatorInstance == null) throw new InvalidValidatorTypeException(DeployToolErrorCode.UnableToCreateValidatorInstance, $"Could not create an instance of validator type {validatorType}"); return validatorInstance; @@ -54,7 +84,14 @@ public static IRecipeValidator[] BuildValidators(this RecipeDefinition recipeDef if (configuration is JObject jObject) { - var validatorInstance = jObject.ToObject(typeMappings[validatorType]); + var validatorInstance = JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(jObject), + typeMappings[validatorType], + new JsonSerializerSettings + { + ContractResolver = new ServiceContractResolver(_serviceProvider) + }); + if (validatorInstance == null) throw new InvalidValidatorTypeException(DeployToolErrorCode.UnableToCreateValidatorInstance, $"Could not create an instance of validator type {validatorType}"); return validatorInstance; @@ -63,4 +100,27 @@ public static IRecipeValidator[] BuildValidators(this RecipeDefinition recipeDef return configuration; } } + + /// + /// Custom contract resolver that can inject services from an IServiceProvider + /// into the constructor of the type that is being deserialized from Json + /// + public class ServiceContractResolver : DefaultContractResolver + { + private readonly IServiceProvider _serviceProvider; + + public ServiceContractResolver(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override JsonObjectContract CreateObjectContract(Type objectType) + { + var contract = base.CreateObjectContract(objectType); + + contract.DefaultCreator = () => ActivatorUtilities.CreateInstance(_serviceProvider, objectType); + + return contract; + } + } } diff --git a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs index 3f666bee9..fb431512f 100644 --- a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs +++ b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs @@ -6,11 +6,19 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.Orchestration { public class OptionSettingHandler : IOptionSettingHandler { + private readonly IValidatorFactory _validatorFactory; + + public OptionSettingHandler(IValidatorFactory validatorFactory) + { + _validatorFactory = validatorFactory; + } + /// /// Assigns a value to the OptionSettingItem. /// @@ -20,7 +28,7 @@ public class OptionSettingHandler : IOptionSettingHandler /// public void SetOptionSettingValue(OptionSettingItem optionSettingItem, object value) { - optionSettingItem.SetValue(this, value); + optionSettingItem.SetValue(this, value, _validatorFactory.BuildValidators(optionSettingItem)); } /// diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs index 1291ac33a..1b6a9abcd 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs @@ -2,12 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Collections.Generic; -using System.Text; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using Moq; using Should; using Xunit; @@ -16,10 +15,12 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation public class AppRunnerOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public AppRunnerOptionSettingItemValidationTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Theory] diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs index 38d0d9585..ef48efe7a 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -1,10 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using Moq; using Should; using Xunit; using Xunit.Abstractions; @@ -14,10 +16,12 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation public class ECSFargateOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ECSFargateOptionSettingItemValidationTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Theory] diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs index b5d45cb9f..9b07bfd19 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs @@ -1,10 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using Moq; using Should; using Xunit; @@ -13,10 +15,12 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation public class ElasticBeanStalkOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ElasticBeanStalkOptionSettingItemValidationTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Theory] diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs index c241fae29..a88a21c0f 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs @@ -1,10 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using Moq; using Should; using Xunit; using Xunit.Abstractions; @@ -22,11 +24,13 @@ public class OptionSettingsItemValidationTests { private readonly ITestOutputHelper _output; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public OptionSettingsItemValidationTests(ITestOutputHelper output) { _output = output; - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Theory] diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs index cdd74e91c..ee76c1974 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs @@ -5,8 +5,11 @@ using System.Collections.Generic; using System.Linq; using Amazon.Runtime.Internal; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; +using Moq; using Newtonsoft.Json; using Should; using Xunit; @@ -21,6 +24,20 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation /// public class ValidatorFactoryTests { + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; + private readonly IValidatorFactory _validatorFactory; + + public ValidatorFactoryTests() + { + _optionSettingHandler = new Mock().Object; + + var mockServiceProvider = new Mock(); + mockServiceProvider.Setup(x => x.GetService(typeof(IOptionSettingHandler))).Returns(_optionSettingHandler); + _serviceProvider = mockServiceProvider.Object; + _validatorFactory = new ValidatorFactory(_serviceProvider); + } + [Fact] public void HasABindingForAllOptionSettingItemValidators() { @@ -42,7 +59,7 @@ public void HasABindingForAllOptionSettingItemValidators() }; // ACT - var validators = optionSettingItem.BuildValidators(); + var validators = _validatorFactory.BuildValidators(optionSettingItem); // ASSERT validators.Length.ShouldEqual(allValidators.Length); @@ -70,7 +87,7 @@ public void HasABindingForAllRecipeValidators() }; // ACT - var validators = recipeDefinition.BuildValidators(); + var validators = _validatorFactory.BuildValidators(recipeDefinition); // ASSERT validators.Length.ShouldEqual(allValidators.Length); @@ -106,7 +123,7 @@ public void CanBuildRehydratedOptionSettingsItem() var deserialized = JsonConvert.DeserializeObject(json); // ACT - var validators = deserialized.BuildValidators(); + var validators = _validatorFactory.BuildValidators(deserialized); // ASSERT validators.Length.ShouldEqual(1); @@ -155,7 +172,7 @@ public void WhenValidatorTypeAndConfigurationHaveAMismatchThenValidatorTypeWins( // ACT try { - validators = deserialized.BuildValidators(); + validators = _validatorFactory.BuildValidators(deserialized); } catch (Exception e) { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 355ad6396..9272450b5 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -26,6 +26,7 @@ using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -251,7 +252,7 @@ private async Task GetOrchestrator(string targetApplicationProject fileManager, directoryManager, new Mock().Object, - new OptionSettingHandler()); + new OptionSettingHandler(new Mock().Object)); } private async Task GetCustomRecipeId(string recipeFilePath) diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index 950eabd99..a92f47ffb 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -18,6 +18,7 @@ using Xunit; using Assert = Should.Core.Assertions.Assert; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.CLI.UnitTests { @@ -25,10 +26,13 @@ public class ApplyPreviousSettingsTests { private readonly IOptionSettingHandler _optionSettingHandler; private readonly Orchestrator _orchestrator; + private readonly IServiceProvider _serviceProvider; + public ApplyPreviousSettingsTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); _orchestrator = new Orchestrator(null, null, null, null, null, null, null, null, null, null, null, null, null, null, _optionSettingHandler); } diff --git a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs index 34bc2920a..cff537b3a 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs @@ -1,22 +1,22 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System.Collections.Generic; -using System.Net.Http; -using Should; -using AWS.Deploy.Common; -using Xunit; -using Amazon.Runtime; -using AWS.Deploy.CLI.Utilities; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Amazon.EC2.Model; -using AWS.Deploy.Common.IO; +using Amazon.Runtime; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.CLI.Utilities; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using Moq; +using Should; +using Xunit; namespace AWS.Deploy.CLI.UnitTests { @@ -24,11 +24,13 @@ public class ConsoleUtilitiesTests { private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ConsoleUtilitiesTests() { + _serviceProvider = new Mock().Object; _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } private readonly List _options = new List diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index 7196d8b68..1e612a55d 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; @@ -20,10 +22,12 @@ namespace AWS.Deploy.CLI.UnitTests public class GetOptionSettingTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public GetOptionSettingTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } private async Task BuildRecommendationEngine(string testProjectName) diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index d23df368f..bc7414068 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; @@ -25,11 +27,13 @@ public class RecommendationTests private OrchestratorSession _session; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public RecommendationTests() { _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } private async Task BuildRecommendationEngine(string testProjectName) diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index 8886c6a07..df05aabef 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; @@ -22,6 +24,7 @@ public class SetOptionSettingTests { private readonly List _recommendations; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public SetOptionSettingTests() { @@ -40,7 +43,8 @@ public SetOptionSettingTests() var engine = new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); _recommendations = engine.ComputeRecommendations().GetAwaiter().GetResult(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } /// diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs index 4de5d590a..c08086b76 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; using Moq; @@ -22,12 +24,14 @@ public class ExistingSecurityGroubsCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ExistingSecurityGroubsCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs index 7cea29afe..0a12fb394 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; using Moq; @@ -22,12 +24,14 @@ public class ExistingSubnetsCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ExistingSubnetsCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs index f309f88b8..0366633b1 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; using Moq; @@ -22,12 +24,14 @@ public class ExistingVpcCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ExistingVpcCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs index 374a97dd3..0499f9c7f 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading.Tasks; @@ -12,6 +13,7 @@ using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; using Moq; @@ -25,13 +27,15 @@ public class VPCConnectorCommandTest private readonly IDirectoryManager _directoryManager; private readonly IToolInteractiveService _toolInteractiveService; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public VPCConnectorCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); _toolInteractiveService = new TestToolInteractiveServiceImpl(); - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs index c32451d24..2b35cb96c 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs @@ -3,14 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; - -using Moq; -using Xunit; - using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.S3; @@ -20,21 +14,26 @@ using Amazon.SQS; using Amazon.SQS.Model; using AWS.Deploy.CLI.Commands.TypeHints; -using AWS.Deploy.Common; -using AWS.Deploy.Orchestration.Data; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.Data; +using Moq; +using Xunit; namespace AWS.Deploy.CLI.UnitTests { public class TypeHintTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public TypeHintTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs index 73f1e3f24..af2726a03 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs @@ -12,16 +12,19 @@ using Amazon.CloudFormation.Model; using System.Collections.Generic; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.Orchestration.UnitTests.CDK { public class CDKProjectHandlerTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public CDKProjectHandlerTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] diff --git a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs index 1f36625fc..348480577 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs @@ -11,6 +11,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration.ServiceHandlers; using AWS.Deploy.Orchestration.UnitTests.Utilities; using AWS.Deploy.Recipes; @@ -22,10 +23,12 @@ namespace AWS.Deploy.Orchestration.UnitTests public class ElasticBeanstalkHandlerTests { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IServiceProvider _serviceProvider; public ElasticBeanstalkHandlerTests() { - _optionSettingHandler = new OptionSettingHandler(); + _serviceProvider = new Mock().Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } [Fact] From 13d8e1608a210ad57a41e87a50f67221411cc492 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Sat, 14 May 2022 17:08:29 -0700 Subject: [PATCH 4/8] feat: Add categories for all settings --- .../Controllers/DeploymentController.cs | 3 + .../ServerMode/Models/CategorySummary.cs | 47 ++++++++ .../Models/ExistingDeploymentSummary.cs | 3 + .../Models/OptionSettingItemSummary.cs | 2 + .../Models/RecommendationSummary.cs | 3 + src/AWS.Deploy.Common/Recipes/Category.cs | 40 ++++++ .../Recipes/OptionSettingItem.cs | 5 + .../Recipes/RecipeDefinition.cs | 5 + src/AWS.Deploy.Common/Recommendation.cs | 29 +++++ .../RecommendationEngine.cs | 8 ++ .../ASP.NETAppAppRunner.recipe | 84 +++++++++---- .../ASP.NETAppECSFargate.recipe | 114 +++++++++++------- .../ASP.NETAppElasticBeanstalk.recipe | 83 ++++++++++++- ....NETAppExistingBeanstalkEnvironment.recipe | 19 ++- .../RecipeDefinitions/BlazorWasm.recipe | 34 +++++- .../ConsoleAppECSFargateScheduleTask.recipe | 44 ++++++- .../ConsoleAppECSFargateService.recipe | 70 ++++++++--- .../PushContainerImageECR.recipe | 11 +- .../aws-deploy-recipe-schema.json | 92 ++++++++++++-- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 28 +++++ .../ServerModeTests.cs | 63 ++++++++++ .../AWS.Deploy.CLI.UnitTests/CategoryTests.cs | 59 +++++++++ .../ServerModeTests.cs | 2 + 23 files changed, 734 insertions(+), 114 deletions(-) create mode 100644 src/AWS.Deploy.CLI/ServerMode/Models/CategorySummary.cs create mode 100644 src/AWS.Deploy.Common/Recipes/Category.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/CategoryTests.cs diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index c3ac6105b..f509b85ad 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -146,6 +146,7 @@ public async Task GetRecommendations(string sessionId) baseRecipeId: recommendation.Recipe.BaseRecipeId, recipeId: recommendation.Recipe.Id, name: recommendation.Name, + settingsCategories: CategorySummary.FromCategories(recommendation.GetConfigurableOptionSettingCategories()), isPersistedDeploymentProject: recommendation.Recipe.PersistedDeploymentProject, shortDescription: recommendation.ShortDescription, description: recommendation.Description, @@ -198,6 +199,7 @@ private List ListOptionSettingSummary(IOptionSettingHa { var settingSummary = new OptionSettingItemSummary(setting.Id, setting.Name, setting.Description, setting.Type.ToString()) { + Category = setting.Category, TypeHint = setting.TypeHint?.ToString(), TypeHintData = setting.TypeHintData, Value = optionSettingHandler.GetOptionSettingValue(recommendation, setting), @@ -340,6 +342,7 @@ public async Task GetExistingDeployments(string sessionId) baseRecipeId: recommendation.Recipe.BaseRecipeId, recipeId: deployment.RecipeId, recipeName: recommendation.Name, + settingsCategories: CategorySummary.FromCategories(recommendation.GetConfigurableOptionSettingCategories()), isPersistedDeploymentProject: recommendation.Recipe.PersistedDeploymentProject, shortDescription: recommendation.ShortDescription, description: recommendation.Description, diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/CategorySummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/CategorySummary.cs new file mode 100644 index 000000000..890af0024 --- /dev/null +++ b/src/AWS.Deploy.CLI/ServerMode/Models/CategorySummary.cs @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AWS.Deploy.CLI.ServerMode.Models +{ + /// + /// A category defined in the recipe that settings will be mapped to via the Id property. + /// + public class CategorySummary + { + /// + /// The id of the category that will be specified on top level settings. + /// + public string Id { get; set; } + + /// + /// The display name of the category shown to users in UI screens. + /// + public string DisplayName { get; set; } + + /// + /// The order used to sort categories in UI screens. Categories will be shown in sorted descending order. + /// + public int Order { get; set; } + + public CategorySummary(string id, string displayName, int order) + { + Id = id; + DisplayName = displayName; + Order = order; + } + + /// + /// Transform recipe category types into the this ServerMode model type. + /// + /// + /// + public static List FromCategories(List categories) + { + return categories.Select(x => new CategorySummary(id: x.Id, displayName: x.DisplayName, order: x.Order)).ToList(); + } + } +} diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs index 1158ef352..8364d82cb 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/ExistingDeploymentSummary.cs @@ -39,6 +39,7 @@ public class ExistingDeploymentSummary public bool UpdatedByCurrentUser { get; set; } public DeploymentTypes DeploymentType { get; set; } + public List SettingsCategories { get; set; } public string ExistingDeploymentId { get; set; } @@ -47,6 +48,7 @@ public ExistingDeploymentSummary( string? baseRecipeId, string recipeId, string recipeName, + List settingsCategories, bool isPersistedDeploymentProject, string shortDescription, string description, @@ -61,6 +63,7 @@ string uniqueIdentifier BaseRecipeId = baseRecipeId; RecipeId = recipeId; RecipeName = recipeName; + SettingsCategories = settingsCategories; IsPersistedDeploymentProject = isPersistedDeploymentProject; ShortDescription = shortDescription; Description = description; diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs index d20264033..51138340e 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs @@ -12,6 +12,8 @@ public class OptionSettingItemSummary public string Name { get; set; } + public string? Category { get; set; } + public string Description { get; set; } public object? Value { get; set; } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs index 8d48464ed..28345cc1e 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/RecommendationSummary.cs @@ -24,11 +24,13 @@ public class RecommendationSummary public string Description { get; set; } public string TargetService { get; set; } public DeploymentTypes DeploymentType { get; set; } + public List SettingsCategories { get; set; } public RecommendationSummary( string? baseRecipeId, string recipeId, string name, + List settingsCategories, bool isPersistedDeploymentProject, string shortDescription, string description, @@ -39,6 +41,7 @@ Common.Recipes.DeploymentTypes deploymentType BaseRecipeId = baseRecipeId; RecipeId = recipeId; Name = name; + SettingsCategories = settingsCategories; IsPersistedDeploymentProject = isPersistedDeploymentProject; ShortDescription = shortDescription; Description = description; diff --git a/src/AWS.Deploy.Common/Recipes/Category.cs b/src/AWS.Deploy.Common/Recipes/Category.cs new file mode 100644 index 000000000..5e176fbac --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Category.cs @@ -0,0 +1,40 @@ +// 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; + +namespace AWS.Deploy.Common.Recipes +{ + /// + /// A category defined in the recipe that settings will be mapped to via the Id property. + /// + public class Category + { + public static readonly Category General = new Category("General", "General", 0); + public static readonly Category DeploymentBundle = new Category("DeploymentBuildSettings", "Project Build", 1000); + + /// + /// The id of the category that will be specified on top level settings. + /// + public string Id { get; set; } + + /// + /// The display name of the category shown to users in UI screens. + /// + public string DisplayName { get; set; } + + /// + /// The order used to sort categories in UI screens. Categories will be shown in sorted descending order. + /// + public int Order { get; set; } + + public Category(string id, string displayName, int order) + { + Id = id; + DisplayName = displayName; + Order = order; + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index a9a14cbe9..35c88a0bc 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -65,6 +65,11 @@ public partial class OptionSettingItem : IOptionSettingItem /// public string Name { get; set; } + /// + /// The category for the setting. This value must match an id field in the list of categories. + /// + public string? Category { get; set; } + /// /// The description of what the setting is used for. /// diff --git a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs index 4c4babd81..f09525993 100644 --- a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs +++ b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs @@ -93,6 +93,11 @@ public class RecipeDefinition /// public List RecommendationRules { get; set; } = new (); + /// + /// The list of categories for the recipes. + /// + public List Categories { get; set; } = new (); + /// /// The settings that can be configured by the user before deploying. /// diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 39c8fd623..76a3db6ac 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -53,8 +53,37 @@ public Recommendation(RecipeDefinition recipe, ProjectDefinition projectDefiniti } } + public List GetConfigurableOptionSettingCategories() + { + var categories = Recipe.Categories; + + // If any top level settings has a category of General make sure the General category is added to the list. + if(!categories.Any(x => string.Equals(x.Id, Category.General.Id)) && + Recipe.OptionSettings.Any(x => string.IsNullOrEmpty(x.Category) || string.Equals(x.Category, Category.General.Id))) + { + categories.Insert(0, Category.General); + } + + // Add the build settings category if it is not already in the list of categories. + if(!categories.Any(x => string.Equals(x.Id, Category.DeploymentBundle.Id))) + { + categories.Add(Category.DeploymentBundle); + } + + return categories; + } + public IEnumerable GetConfigurableOptionSettingItems() { + // For any top level settings that don't have a category assigned to them assign the General category. + foreach(var setting in Recipe.OptionSettings) + { + if(string.IsNullOrEmpty(setting.Category)) + { + setting.Category = Category.General.Id; + } + } + if (DeploymentBundleSettings == null) return Recipe.OptionSettings; diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index d6257cde4..96f360723 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -95,7 +95,15 @@ public List GetDeploymentBundleSettings(DeploymentBundleTypes if (definition == null) throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeDeploymentBundle, $"Failed to Deserialize Deployment Bundle [{deploymentBundleFile}]"); if (definition.Type.Equals(deploymentBundleTypes)) + { + // Assign Build category to all of the deployment bundle settings. + foreach(var setting in definition.Parameters) + { + setting.Category = Category.DeploymentBundle.Id; + } + return definition.Parameters; + } } catch (Exception e) { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index e10f3a646..782380894 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -53,11 +53,43 @@ } ], - + "Categories": [ + { + "Id": "General", + "DisplayName": "General", + "Order": 10 + }, + { + "Id": "Compute", + "DisplayName": "Compute", + "Order": 20 + }, + { + "Id": "Health", + "DisplayName": "Health", + "Order": 30 + }, + { + "Id": "Permissions", + "DisplayName": "Permissions", + "Order": 40 + }, + { + "Id": "VPC", + "DisplayName": "VPC", + "Order": 50 + }, + { + "Id": "EnvVariables", + "DisplayName": "Environment Variables", + "Order": 60 + } + ], "OptionSettings": [ { "Id": "ServiceName", "Name": "Service Name", + "Category": "General", "Description": "The name of the AWS App Runner service.", "Type": "String", "TypeHint": "AppRunnerService", @@ -67,8 +99,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "^([A-Za-z0-9][A-Za-z0-9_-]{3,39})$", "ValidationFailedMessage": "Invalid service name. The service name must be between 4 and 40 characters in length and can contain uppercase and lowercase letters, numbers, hyphen(-) and underscore(_). It must start with a letter or a number." } @@ -78,6 +109,7 @@ { "Id": "Port", "Name": "Port", + "Category": "General", "Description": "The port the container is listening for requests on.", "Type": "Int", "DefaultValue": 80, @@ -86,8 +118,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 51200 } @@ -97,6 +128,7 @@ { "Id": "StartCommand", "Name": "Start Command", + "Category": "General", "Description": "Override the start command from the image's default start command.", "Type": "String", "AdvancedSetting": true, @@ -105,6 +137,7 @@ { "Id": "ApplicationIAMRole", "Name": "Application IAM Role", + "Category": "Permissions", "Description": "The Identity and Access Management (IAM) role that provides AWS credentials to the application to access AWS services.", "Type": "Object", "TypeHint": "IAMRole", @@ -137,8 +170,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-authenticationconfiguration.html" @@ -157,6 +189,7 @@ { "Id": "ServiceAccessIAMRole", "Name": "Service Access IAM Role", + "Category": "Permissions", "Description": "The Identity and Access Management (IAM) role that provides gives the AWS App Runner service access to pull the container image from ECR.", "Type": "Object", "TypeHint": "IAMRole", @@ -189,8 +222,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-authenticationconfiguration.html" @@ -209,6 +241,7 @@ { "Id": "Cpu", "Name": "CPU", + "Category": "Compute", "Description": "The number of CPU units reserved for each instance of your App Runner service.", "Type": "String", "AdvancedSetting": false, @@ -226,6 +259,7 @@ { "Id": "Memory", "Name": "Memory", + "Category": "Compute", "Description": "The amount of memory reserved for each instance of your App Runner service.", "Type": "String", "AdvancedSetting": false, @@ -245,6 +279,7 @@ { "Id": "EncryptionKmsKey", "Name": "Encryption KMS Key", + "Category": "Permissions", "Description": "The ARN of the KMS key that's used for encryption of application logs.", "Type": "String", "AdvancedSetting": true, @@ -252,8 +287,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "arn:aws(-[\\w]+)*:kms:[a-z\\-]+-[0-9]{1}:[0-9]{12}:key/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid KMS key ARN. The ARN should contain the arn:[PARTITION]:kms namespace, followed by the region, account ID, and then the key-id. For example - arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab is a valid KMS key ARN. For more information visit https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apprunner-service-encryptionconfiguration.html" @@ -264,6 +298,7 @@ { "Id": "HealthCheckProtocol", "Name": "Health Check Protocol", + "Category": "Health", "Description": "The IP protocol that App Runner uses to perform health checks for your service.", "Type": "String", "DefaultValue": "TCP", @@ -277,6 +312,7 @@ { "Id": "HealthCheckPath", "Name": "Health Check Path", + "Category": "Health", "Description": "The URL that health check requests are sent to.", "Type": "String", "AdvancedSetting": true, @@ -291,6 +327,7 @@ { "Id": "HealthCheckInterval", "Name": "Health Check Interval", + "Category": "Health", "Description": "The time interval, in seconds, between health checks.", "Type": "Int", "DefaultValue": 5, @@ -298,9 +335,8 @@ "Updatable": true, "Validators": [ { - "ValidatorType":"Range", - "Configuration": - { + "ValidatorType": "Range", + "Configuration": { "Min": 1, "Max": 20 } @@ -310,6 +346,7 @@ { "Id": "HealthCheckTimeout", "Name": "Health Check Timeout", + "Category": "Health", "Description": "The time, in seconds, to wait for a health check response before deciding it failed.", "Type": "Int", "DefaultValue": 2, @@ -317,9 +354,8 @@ "Updatable": true, "Validators": [ { - "ValidatorType":"Range", - "Configuration": - { + "ValidatorType": "Range", + "Configuration": { "Min": 1, "Max": 20 } @@ -329,6 +365,7 @@ { "Id": "HealthCheckHealthyThreshold", "Name": "Health Check Healthy Threshold", + "Category": "Health", "Description": "The number of consecutive checks that must succeed before App Runner decides that the service is healthy.", "Type": "Int", "DefaultValue": 3, @@ -336,9 +373,8 @@ "Updatable": true, "Validators": [ { - "ValidatorType":"Range", - "Configuration": - { + "ValidatorType": "Range", + "Configuration": { "Min": 1, "Max": 20 } @@ -348,6 +384,7 @@ { "Id": "HealthCheckUnhealthyThreshold", "Name": "Health Check Unhealthy Threshold", + "Category": "Health", "Description": "The number of consecutive checks that must fail before App Runner decides that the service is unhealthy.", "Type": "Int", "DefaultValue": 3, @@ -355,9 +392,8 @@ "Updatable": true, "Validators": [ { - "ValidatorType":"Range", - "Configuration": - { + "ValidatorType": "Range", + "Configuration": { "Min": 1, "Max": 20 } @@ -367,6 +403,7 @@ { "Id": "VPCConnector", "Name": "VPC Connector", + "Category": "VPC", "Description": "App Runner requires this resource when you want to associate your App Runner service to a custom Amazon Virtual Private Cloud (Amazon VPC).", "Type": "Object", "TypeHint": "VPCConnector", @@ -481,6 +518,7 @@ { "Id": "AppRunnerEnvironmentVariables", "Name": "Environment Variables", + "Category": "EnvVariables", "Description": "Configure environment properties for your application.", "Type": "KeyValue", "AdvancedSetting": false, diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 88bb0c219..f2d7c8b6f 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -68,18 +68,54 @@ }, { "ValidatorType": "MinMaxConstraint", - "Configuration": - { + "Configuration": { "MinValueOptionSettingsId": "AutoScaling.MinCapacity", "MaxValueOptionSettingsId": "AutoScaling.MaxCapacity" } } ], - + "Categories": [ + { + "Id": "General", + "DisplayName": "General", + "Order": 10 + }, + { + "Id": "Compute", + "DisplayName": "Compute", + "Order": 20 + }, + { + "Id": "LoadBalancer", + "DisplayName": "Load Balancer", + "Order": 30 + }, + { + "Id": "AutoScaling", + "DisplayName": "Auto Scaling", + "Order": 40 + }, + { + "Id": "Permissions", + "DisplayName": "Permissions", + "Order": 50 + }, + { + "Id": "VPC", + "DisplayName": "VPC", + "Order": 60 + }, + { + "Id": "EnvVariables", + "DisplayName": "Environment Variables", + "Order": 70 + } + ], "OptionSettings": [ { "Id": "ECSCluster", "Name": "ECS Cluster", + "Category": "General", "Description": "The ECS cluster used for the deployment.", "Type": "Object", "TypeHint": "ECSCluster", @@ -151,6 +187,7 @@ "Id": "ECSServiceName", "ParentSettingId": "ClusterName", "Name": "ECS Service Name", + "Category": "General", "Description": "The name of the ECS service running in the cluster.", "Type": "String", "TypeHint": "ECSService", @@ -170,6 +207,7 @@ { "Id": "DesiredCount", "Name": "Desired Task Count", + "Category": "Compute", "Description": "The desired number of ECS tasks to run for the service.", "Type": "Int", "DefaultValue": 3, @@ -188,6 +226,7 @@ { "Id": "ApplicationIAMRole", "Name": "Application IAM Role", + "Category": "Permissions", "Description": "The Identity and Access Management (IAM) role that provides AWS credentials to the application to access AWS services.", "Type": "Object", "TypeHint": "IAMRole", @@ -239,6 +278,7 @@ { "Id": "Vpc", "Name": "Virtual Private Cloud (VPC)", + "Category": "VPC", "Description": "A VPC enables you to launch the application into a virtual network that you've defined.", "Type": "Object", "TypeHint": "Vpc", @@ -304,6 +344,7 @@ { "Id": "AdditionalECSServiceSecurityGroups", "Name": "ECS Service Security Groups", + "Category": "Permissions", "Description": "A comma-delimited list of EC2 security groups to assign to the ECS service. This is commonly used to provide access to Amazon RDS databases running in their own security groups.", "Type": "String", "DefaultValue": "", @@ -313,6 +354,7 @@ { "Id": "TaskCpu", "Name": "Task CPU", + "Category": "Compute", "Description": "The number of CPU units used by the task. See the following for details on CPU values: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html#fargate-task-defs", "Type": "Int", "DefaultValue": 256, @@ -330,6 +372,7 @@ { "Id": "TaskMemory", "Name": "Task Memory", + "Category": "Compute", "Description": "The amount of memory (in MB) used by the task. See the following for details on memory values: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html#fargate-task-defs", "Type": "Int", "DefaultValue": 512, @@ -405,6 +448,7 @@ { "Id": "LoadBalancer", "Name": "Elastic Load Balancer", + "Category": "LoadBalancer", "Description": "Load Balancer the ECS Service will register tasks to.", "Type": "Object", "AdvancedSetting": true, @@ -431,8 +475,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "arn:[^:]+:elasticloadbalancing:[^:]*:[0-9]{12}:loadbalancer/.+", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid load balancer ARN. The ARN should contain the arn:[PARTITION]:elasticloadbalancing namespace, followed by the Region of the load balancer, the AWS account ID of the load balancer owner, the loadbalancer namespace, and then the load balancer name. For example, arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188" @@ -457,10 +500,9 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, - "Max": 3600 + "Max": 3600 } } ] @@ -485,10 +527,9 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 5, - "Max": 300 + "Max": 300 } } ] @@ -504,10 +545,9 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 2, - "Max": 10 + "Max": 10 } } ] @@ -523,10 +563,9 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 2, - "Max": 10 + "Max": 10 } } ] @@ -561,8 +600,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration": - { + "Configuration": { "Regex": "^/[a-zA-Z0-9*?&_\\-.$/~\"'@:+]{0,127}$", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid listener condition path. The path is case-sensitive and can be up to 128. It starts with '/' and consists of alpha-numeric characters, wildcards (* and ?), & (using &), and the following special characters: '_-.$/~\"'@:+'" @@ -591,8 +629,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1, "Max": 50000 } @@ -614,6 +651,7 @@ { "Id": "AutoScaling", "Name": "AutoScaling", + "Category": "AutoScaling", "Description": "The AutoScaling configuration for the ECS service.", "Type": "Object", "AdvancedSetting": true, @@ -639,8 +677,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1, "Max": 5000 } @@ -664,8 +701,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1, "Max": 5000 } @@ -709,8 +745,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1, "Max": 100 } @@ -738,8 +773,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -767,8 +801,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -796,8 +829,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1, "Max": 100 } @@ -825,8 +857,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -854,8 +885,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -883,8 +913,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 1 } } @@ -911,8 +940,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -940,8 +968,7 @@ "Validators": [ { "ValidatorType": "Range", - "Configuration": - { + "Configuration": { "Min": 0, "Max": 3600 } @@ -963,6 +990,7 @@ { "Id": "ECSEnvironmentVariables", "Name": "Environment Variables", + "Category": "EnvVariables", "Description": "Configure environment properties for your application.", "Type": "KeyValue", "AdvancedSetting": false, diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index bae15e6ca..36011703a 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -73,11 +73,63 @@ } } ], - + "Categories": [ + { + "Id": "General", + "DisplayName": "General", + "Order": 10 + }, + { + "Id": "Hosting", + "DisplayName": "Hosting", + "Order": 20 + }, + { + "Id": "Platform", + "DisplayName": "Platform", + "Order": 30 + }, + { + "Id": "RollingUpdates", + "DisplayName": "Rolling updates & deployments", + "Order": 40 + }, + { + "Id": "Health", + "DisplayName": "Health & Monitoring", + "Order": 50 + }, + { + "Id": "Compute", + "DisplayName": "Compute", + "Order": 60 + }, + { + "Id": "LoadBalancer", + "DisplayName": "LoadBalancer", + "Order": 70 + }, + { + "Id": "Permissions", + "DisplayName": "Permissions", + "Order": 80 + }, + { + "Id": "VPC", + "DisplayName": "VPC", + "Order": 90 + }, + { + "Id": "EnvVariables", + "DisplayName": "Environment Variables", + "Order": 100 + } + ], "OptionSettings": [ { "Id": "BeanstalkApplication", "Name": "Application Name", + "Category": "General", "Description": "The Elastic Beanstalk application name.", "Type": "Object", "TypeHint": "BeanstalkApplication", @@ -150,6 +202,7 @@ "Id": "BeanstalkEnvironment", "ParentSettingId": "BeanstalkApplication.ApplicationName", "Name": "Environment Name", + "Category": "General", "Description": "The Elastic Beanstalk environment name.", "Type": "Object", "AdvancedSetting": false, @@ -167,7 +220,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration" : { + "Configuration": { "Regex": "^[a-zA-Z0-9][a-zA-Z0-9-]{2,38}[a-zA-Z0-9]$", "ValidationFailedMessage": "Invalid Environment Name. The Environment Name Must be from 4 to 40 characters in length. The name can contain only letters, numbers, and hyphens. It can't start or end with a hyphen." } @@ -179,6 +232,7 @@ { "Id": "InstanceType", "Name": "EC2 Instance Type", + "Category": "Compute", "Description": "The EC2 instance type of the EC2 instances created for the environment.", "Type": "String", "TypeHint": "InstanceType", @@ -188,6 +242,7 @@ { "Id": "EnvironmentType", "Name": "Environment Type", + "Category": "General", "Description": "The type of environment to create; for example, a single instance for development work or load balanced for production.", "Type": "String", "DefaultValue": "SingleInstance", @@ -205,6 +260,7 @@ { "Id": "LoadBalancerType", "Name": "Load Balancer Type", + "Category": "LoadBalancer", "Description": "The type of load balancer for your environment.", "Type": "String", "DefaultValue": "application", @@ -230,6 +286,7 @@ { "Id": "ApplicationIAMRole", "Name": "Application IAM Role", + "Category": "Permissions", "Description": "The Identity and Access Management (IAM) role that provides AWS credentials to the application to access AWS services.", "Type": "Object", "TypeHint": "IAMRole", @@ -268,7 +325,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration" : { + "Configuration": { "Regex": "arn:.+:iam::[0-9]{12}:.+", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns" @@ -281,6 +338,7 @@ { "Id": "ServiceIAMRole", "Name": "Service IAM Role", + "Category": "Permissions", "Description": "A service role is the IAM role that Elastic Beanstalk assumes when calling other services on your behalf.", "Type": "Object", "TypeHint": "IAMRole", @@ -319,7 +377,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration" : { + "Configuration": { "Regex": "arn:.+:iam::[0-9]{12}:.+", "AllowEmptyString": true, "ValidationFailedMessage": "Invalid IAM Role ARN. The ARN should contain the arn:[PARTITION]:iam namespace, followed by the account ID, and then the resource path. For example - arn:aws:iam::123456789012:role/S3Access is a valid IAM Role ARN. For more information visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns" @@ -332,6 +390,7 @@ { "Id": "EC2KeyPair", "Name": "Key Pair", + "Category": "Permissions", "Description": "The EC2 key pair used to SSH into EC2 instances for the Elastic Beanstalk environment.", "Type": "String", "TypeHint": "EC2KeyPair", @@ -341,7 +400,7 @@ "Validators": [ { "ValidatorType": "Regex", - "Configuration" : { + "Configuration": { "Regex": "^(?! ).+(? FailedConfigUpdates { get; set; } + } + + /// A category defined in the recipe that settings will be mapped to via the Id property. + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] + public partial class CategorySummary + { + /// The id of the category that will be specified on top level settings. + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Id { get; set; } + + /// The display name of the category shown to users in UI screens. + [Newtonsoft.Json.JsonProperty("displayName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string DisplayName { get; set; } + + /// The order used to sort categories in UI screens. Categories will be showin in sorted descending order. + [Newtonsoft.Json.JsonProperty("order", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public int Order { get; set; } + + } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] @@ -1882,6 +1901,9 @@ public partial class ExistingDeploymentSummary [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public DeploymentTypes DeploymentType { get; set; } + [Newtonsoft.Json.JsonProperty("settingsCategories", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection SettingsCategories { get; set; } + [Newtonsoft.Json.JsonProperty("existingDeploymentId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string ExistingDeploymentId { get; set; } @@ -1990,6 +2012,9 @@ public partial class OptionSettingItemSummary [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Name { get; set; } + [Newtonsoft.Json.JsonProperty("category", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Category { get; set; } + [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Description { get; set; } @@ -2117,6 +2142,9 @@ public partial class RecommendationSummary [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public DeploymentTypes DeploymentType { get; set; } + [Newtonsoft.Json.JsonProperty("settingsCategories", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.ICollection SettingsCategories { get; set; } + } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index b0c3dace4..e0ed24da1 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -282,6 +282,11 @@ public async Task WebFargateDeploymentNoConfigChanges() Assert.Equal(fargateRecommendation.Description, existingDeployment.Description); Assert.Equal(fargateRecommendation.TargetService, existingDeployment.TargetService); Assert.Equal(DeploymentTypes.CloudFormationStack, existingDeployment.DeploymentType); + + Assert.NotEmpty(existingDeployment.SettingsCategories); + Assert.Contains(existingDeployment.SettingsCategories, x => string.Equals(x.Id, AWS.Deploy.Common.Recipes.Category.DeploymentBundle.Id)); + Assert.DoesNotContain(existingDeployment.SettingsCategories, x => string.IsNullOrEmpty(x.Id)); + Assert.DoesNotContain(existingDeployment.SettingsCategories, x => string.IsNullOrEmpty(x.DisplayName)); } finally { @@ -410,6 +415,64 @@ public async Task InvalidStackName_ThrowsException(string invalidStackName) } } + [Fact] + public async Task CheckCategories() + { + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var portNumber = 4200; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + + var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); + var cancelSource = new CancellationTokenSource(); + + var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); + try + { + var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); + await WaitTillServerModeReady(restClient); + + var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput + { + AwsRegion = _awsRegion, + ProjectPath = projectPath + }); + + var sessionId = startSessionOutput.SessionId; + Assert.NotNull(sessionId); + + var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); + + foreach(var recommendation in getRecommendationOutput.Recommendations) + { + Assert.NotEmpty(recommendation.SettingsCategories); + Assert.Contains(recommendation.SettingsCategories, x => string.Equals(x.Id, AWS.Deploy.Common.Recipes.Category.DeploymentBundle.Id)); + Assert.DoesNotContain(recommendation.SettingsCategories, x => string.IsNullOrEmpty(x.Id)); + Assert.DoesNotContain(recommendation.SettingsCategories, x => string.IsNullOrEmpty(x.DisplayName)); + } + + var selectedRecommendation = getRecommendationOutput.Recommendations.First(); + await restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput + { + NewDeploymentRecipeId = selectedRecommendation.RecipeId, + NewDeploymentName = "TestStack-" + DateTime.UtcNow.Ticks + }); + + var getConfigSettingsResponse = await restClient.GetConfigSettingsAsync(sessionId); + + // Make sure all top level settings have a category + Assert.DoesNotContain(getConfigSettingsResponse.OptionSettings, x => string.IsNullOrEmpty(x.Category)); + + // Make sure build settings have been applied a category. + var buildSetting = getConfigSettingsResponse.OptionSettings.FirstOrDefault(x => string.Equals(x.Id, "DotnetBuildConfiguration")); + Assert.NotNull(buildSetting); + Assert.Equal(AWS.Deploy.Common.Recipes.Category.DeploymentBundle.Id, buildSetting.Category); + } + finally + { + cancelSource.Cancel(); + } + } + internal static void RegisterSignalRMessageCallbacks(IDeploymentCommunicationClient signalRClient, StringBuilder logOutput) { signalRClient.ReceiveLogSectionStart = (message, description) => diff --git a/test/AWS.Deploy.CLI.UnitTests/CategoryTests.cs b/test/AWS.Deploy.CLI.UnitTests/CategoryTests.cs new file mode 100644 index 000000000..95ad92135 --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/CategoryTests.cs @@ -0,0 +1,59 @@ +// 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 AWS.Deploy.Recipes; +using Xunit; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit.Abstractions; + +namespace AWS.Deploy.CLI.UnitTests +{ + public class CategoryTests + { + private readonly ITestOutputHelper _output; + + public CategoryTests(ITestOutputHelper output) + { + this._output = output; + } + + [Fact] + public void ValidateSettingCategories() + { + var recipes = Directory.GetFiles(RecipeLocator.FindRecipeDefinitionsPath(), "*.recipe", SearchOption.TopDirectoryOnly); + + foreach(var recipe in recipes) + { + _output.WriteLine($"Validating recipe: {recipe}"); + var root = JsonConvert.DeserializeObject(File.ReadAllText(recipe)) as JObject; + + _output.WriteLine("\tCategories"); + var categoryIds = new HashSet(); + var categoryOrders = new HashSet(); + foreach(JObject category in root["Categories"]) + { + _output.WriteLine($"\t\t{category["Id"]}"); + categoryIds.Add(category["Id"].ToString()); + + // Make sure all order ids are unique in recipe + var order = (int)category["Order"]; + Assert.DoesNotContain(order, categoryOrders); + categoryOrders.Add(order); + } + + _output.WriteLine("\tSettings"); + foreach (JObject setting in root["OptionSettings"]) + { + var settingCategoryId = setting["Category"]?.ToString(); + _output.WriteLine($"\t\t{settingCategoryId}"); + Assert.Contains(settingCategoryId, categoryIds); + } + } + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index f1fef868d..6a06a4cae 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -135,6 +135,7 @@ public void ExistingDeploymentSummary_ContainsCorrectDeploymentType(CloudApplica "baseRecipeId", "recipeId", "recipeName", + new List(), false, "shortDescription", "description", @@ -156,6 +157,7 @@ public void RecommendationSummary_ContainsCorrectDeploymentType(Deploy.Common.Re "baseRecipeId", "recipeId", "name", + new List(), false, "shortDescription", "description", From e13fbe0381d75799206a984e89f982f1f5cce513 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Tue, 17 May 2022 13:45:09 -0400 Subject: [PATCH 5/8] fix: Check for duplicate cloud application names --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 77 +++++----------- src/AWS.Deploy.CLI/Exceptions.cs | 8 ++ .../Controllers/DeploymentController.cs | 11 ++- src/AWS.Deploy.Common/CloudApplication.cs | 17 +++- src/AWS.Deploy.Common/Exceptions.cs | 3 +- .../CloudApplicationNameGenerator.cs | 87 +++++++++++++++---- .../ServerModeTests.cs | 46 +--------- .../CloudApplicationNameGeneratorTests.cs | 67 +++++++++++--- 8 files changed, 181 insertions(+), 135 deletions(-) diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 837936ba2..a738ac2e4 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -149,14 +149,12 @@ private void DisplayOutputResources(List displayedResourc /// If a new Cloudformation stack name is selected, then a fresh deployment is initiated with the user-selected deployment recipe. /// If an existing deployment target is selected, then a re-deployment is initiated with the same deployment recipe. /// - /// The cloud application name provided via the --application-name CLI argument + /// The cloud application name provided via the --application-name CLI argument /// The deserialized object from the user provided config file. /// The absolute or relative path of the CDK project that will be used for deployment /// A tuple consisting of the Orchestrator object, Selected Recommendation, Cloud Application metadata. - public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string applicationName, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) + public async Task<(Orchestrator, Recommendation, CloudApplication)> InitializeDeployment(string cloudApplicationName, UserDeploymentSettings? userDeploymentSettings, string deploymentProjectPath) { - string cloudApplicationName; - var orchestrator = new Orchestrator( _session, _orchestratorInteractiveService, @@ -183,8 +181,9 @@ private void DisplayOutputResources(List displayedResourc // Filter compatible applications that can be re-deployed using the current set of recommendations. var compatibleApplications = await _deployedApplicationQueryer.GetCompatibleApplications(recommendations, allDeployedApplications, _session); - // Try finding the CloudApplication name via the --application-name CLI argument or user provided config settings. - cloudApplicationName = GetCloudApplicationNameFromDeploymentSettings(applicationName, userDeploymentSettings); + if (string.IsNullOrEmpty(cloudApplicationName)) + // Try finding the CloudApplication name via the user provided config settings. + cloudApplicationName = userDeploymentSettings?.ApplicationName ?? string.Empty; // Prompt the user with a choice to re-deploy to existing targets or deploy to a new cloud application. if (string.IsNullOrEmpty(cloudApplicationName)) @@ -225,13 +224,20 @@ private void DisplayOutputResources(List displayedResourc // The ECR repository name is already configurable as part of the recipe option settings. if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.ElasticContainerRegistryImage) { - cloudApplicationName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, compatibleApplications); + cloudApplicationName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, compatibleApplications, selectedRecommendation.Recipe.DeploymentType); } else { cloudApplicationName = AskForNewCloudApplicationName(selectedRecommendation.Recipe.DeploymentType, compatibleApplications); } } + // cloudApplication name was already provided via CLI args or the deployment config file + else + { + var validationResult = _cloudApplicationNameGenerator.IsValidName(cloudApplicationName, allDeployedApplications, selectedRecommendation.Recipe.DeploymentType); + if (!validationResult.IsValid) + throw new InvalidCloudApplicationNameException(DeployToolErrorCode.InvalidCloudApplicationName, validationResult.ErrorMessage); + } } await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, cloudApplicationName); @@ -505,33 +511,6 @@ private void SetDeploymentBundleOptionSetting(Recommendation recommendation, str } } - // This method tries to find the cloud application name via the user provided CLI arguments or deployment config file. - // If a name is not present at either of the places then return string.empty - private string GetCloudApplicationNameFromDeploymentSettings(string? applicationName, UserDeploymentSettings? userDeploymentSettings) - { - // validate and return the applicationName provided by the --application-name cli argument if present. - if (!string.IsNullOrEmpty(applicationName)) - { - if (_cloudApplicationNameGenerator.IsValidName(applicationName)) - return applicationName; - - PrintInvalidApplicationNameMessage(applicationName); - throw new InvalidCliArgumentException(DeployToolErrorCode.InvalidCliArguments, "Found invalid CLI arguments"); - } - - // validate and return the applicationName from the deployment settings if present. - if (!string.IsNullOrEmpty(userDeploymentSettings?.ApplicationName)) - { - if (_cloudApplicationNameGenerator.IsValidName(userDeploymentSettings.ApplicationName)) - return userDeploymentSettings.ApplicationName; - - PrintInvalidApplicationNameMessage(userDeploymentSettings.ApplicationName); - throw new InvalidUserDeploymentSettingsException(DeployToolErrorCode.UserDeploymentInvalidStackName, "Please provide a valid cloud application name and try again."); - } - - return string.Empty; - } - // This method prompts the user to select a CloudApplication name for existing deployments or create a new one. // If a user chooses to create a new CloudApplication, then this method returns string.Empty private string AskForCloudApplicationNameFromDeployedApplications(List deployedApplications) @@ -575,14 +554,14 @@ private string AskForNewCloudApplicationName(DeploymentTypes deploymentType, Lis try { - defaultName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, deployedApplications); + defaultName = _cloudApplicationNameGenerator.GenerateValidName(_session.ProjectDefinition, deployedApplications, deploymentType); } catch (Exception exception) { _toolInteractiveService.WriteDebugLine(exception.PrettyPrint()); } - var cloudApplicationName = ""; + var cloudApplicationName = string.Empty; while (true) { @@ -612,12 +591,14 @@ private string AskForNewCloudApplicationName(DeploymentTypes deploymentType, Lis allowEmpty: false, defaultAskValuePrompt: inputPrompt); - if (string.IsNullOrEmpty(cloudApplicationName) || !_cloudApplicationNameGenerator.IsValidName(cloudApplicationName)) - PrintInvalidApplicationNameMessage(cloudApplicationName); - else if (deployedApplications.Any(x => x.Name.Equals(cloudApplicationName))) - PrintApplicationNameAlreadyExistsMessage(); - else + var validationResult = _cloudApplicationNameGenerator.IsValidName(cloudApplicationName, deployedApplications, deploymentType); + if (validationResult.IsValid) + { return cloudApplicationName; + } + + _toolInteractiveService.WriteLine(); + _toolInteractiveService.WriteErrorLine(validationResult.ErrorMessage); } } @@ -655,20 +636,6 @@ private Recommendation GetSelectedRecommendation(UserDeploymentSettings? userDep return selectedRecommendation; } - private void PrintInvalidApplicationNameMessage(string name) - { - _toolInteractiveService.WriteLine(); - _toolInteractiveService.WriteErrorLine(_cloudApplicationNameGenerator.InvalidNameMessage(name)); - } - - private void PrintApplicationNameAlreadyExistsMessage() - { - _toolInteractiveService.WriteLine(); - _toolInteractiveService.WriteErrorLine( - "Invalid application name. There already exists a CloudFormation stack with the name you provided. " + - "Please choose another application name."); - } - private bool ConfirmDeployment(Recommendation recommendation) { var message = recommendation.Recipe.DeploymentConfirmation?.DefaultMessage; diff --git a/src/AWS.Deploy.CLI/Exceptions.cs b/src/AWS.Deploy.CLI/Exceptions.cs index f70fafc75..853dad147 100644 --- a/src/AWS.Deploy.CLI/Exceptions.cs +++ b/src/AWS.Deploy.CLI/Exceptions.cs @@ -84,4 +84,12 @@ public class FailedToGetCredentialsForProfile : DeployToolException { public FailedToGetCredentialsForProfile(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + + /// + /// Throw if cloud application name is invalid. + /// + public class InvalidCloudApplicationNameException : DeployToolException + { + public InvalidCloudApplicationNameException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index f509b85ad..c548f46b7 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -386,14 +386,13 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody return NotFound($"Recommendation {input.NewDeploymentRecipeId} not found."); } + // We only validate the name when the recipe deployment type is not ElasticContainerRegistryImage. + // This is because pushing images to ECR does not need a cloud application name. if (state.SelectedRecommendation.Recipe.DeploymentType != Common.Recipes.DeploymentTypes.ElasticContainerRegistryImage) { - // We only validate the name when the recipe deployment type is not ElasticContainerRegistryImage. - // This is because pushing images to ECR does not need a cloud application name. - if (!cloudApplicationNameGenerator.IsValidName(newDeploymentName)) - { - return ValidationProblem(cloudApplicationNameGenerator.InvalidNameMessage(newDeploymentName)); - } + var validationResult = cloudApplicationNameGenerator.IsValidName(newDeploymentName, state.ExistingDeployments ?? new List(), state.SelectedRecommendation.Recipe.DeploymentType); + if (!validationResult.IsValid) + return ValidationProblem(validationResult.ErrorMessage); } state.ApplicationDetails.Name = newDeploymentName; diff --git a/src/AWS.Deploy.Common/CloudApplication.cs b/src/AWS.Deploy.Common/CloudApplication.cs index d3ffee9b8..5cc42e6de 100644 --- a/src/AWS.Deploy.Common/CloudApplication.cs +++ b/src/AWS.Deploy.Common/CloudApplication.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.Common { @@ -19,6 +20,14 @@ public class CloudApplication { CloudApplicationResourceType.ElasticContainerRegistryImage, "ECR Repository" } }; + private readonly Dictionary _deploymentTypeMapping = + new() + { + { CloudApplicationResourceType.CloudFormationStack, DeploymentTypes.CdkProject}, + { CloudApplicationResourceType.BeanstalkEnvironment, DeploymentTypes.BeanstalkEnvironment }, + { CloudApplicationResourceType.ElasticContainerRegistryImage, DeploymentTypes.ElasticContainerRegistryImage } + }; + /// /// Name of the CloudApplication resource /// @@ -38,8 +47,7 @@ public class CloudApplication public string RecipeId { get; set; } /// - /// indicates the type of the AWS resource which serves as the deployment target. - /// Current supported values are None, CloudFormationStack and BeanstalkEnvironment. + /// Indicates the type of the AWS resource which serves as the deployment target. /// public CloudApplicationResourceType ResourceType { get; set; } @@ -63,6 +71,11 @@ public class CloudApplication /// public override string ToString() => Name; + /// + /// Gets the deployment type of the recommendation that was used to deploy the cloud application. + /// + public DeploymentTypes DeploymentType => _deploymentTypeMapping[ResourceType]; + public CloudApplication(string name, string uniqueIdentifier, CloudApplicationResourceType resourceType, string recipeId, DateTime? lastUpdatedTime = null) { Name = name; diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index aea70ed5f..3c301e218 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -113,7 +113,8 @@ public enum DeployToolErrorCode FailedToCreateCDKProject = 10009100, ResourceQuery = 10009200, FailedToRetrieveStackId = 10009300, - FailedToGetECRAuthorizationToken = 10009400 + FailedToGetECRAuthorizationToken = 10009400, + InvalidCloudApplicationName = 10009500 } public class ProjectFileNotFoundException : DeployToolException diff --git a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs index e1073c084..692325abd 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/CloudApplicationNameGenerator.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.Orchestration.Utilities { @@ -20,23 +21,38 @@ public interface ICloudApplicationNameGenerator /// /// Thrown if can't generate a valid name from . /// - string GenerateValidName(ProjectDefinition target, List existingApplications); + string GenerateValidName(ProjectDefinition target, List existingApplications, DeploymentTypes? deploymentType = null); /// - /// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html + /// Validates the cloud application name /// - bool IsValidName(string name); + /// User provided cloud application name + /// The deployment type of the selected recommendation + /// List of existing deployed applications + /// + CloudApplicationNameValidationResult IsValidName(string name, IList existingApplications, DeploymentTypes? deploymentType = null); + } - /// - /// The message that should be displayed when an invalid name is passed - /// - string InvalidNameMessage(string name); + /// + /// Stores the result from validating the cloud application name. + /// + public class CloudApplicationNameValidationResult + { + public readonly bool IsValid; + public readonly string ErrorMessage; + + public CloudApplicationNameValidationResult(bool isValid, string errorMessage) + { + IsValid = isValid; + ErrorMessage = errorMessage; + } } public class CloudApplicationNameGenerator : ICloudApplicationNameGenerator { private readonly IFileManager _fileManager; private readonly IDirectoryManager _directoryManager; + /// /// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html /// @@ -48,7 +64,7 @@ public CloudApplicationNameGenerator(IFileManager fileManager, IDirectoryManager _directoryManager = directoryManager; } - public string GenerateValidName(ProjectDefinition target, List existingApplications) + public string GenerateValidName(ProjectDefinition target, List existingApplications, DeploymentTypes? deploymentType = null) { // generate recommendation var recommendedPrefix = "deployment"; @@ -86,7 +102,9 @@ public string GenerateValidName(ProjectDefinition target, List var suffix = !string.IsNullOrEmpty(suffixString) ? int.Parse(suffixString): 0; while (suffix < int.MaxValue) { - if (existingApplications.All(x => x.Name != recommendation) && IsValidName(recommendation)) + var validationResult = IsValidName(recommendation, existingApplications, deploymentType); + + if (validationResult.IsValid) return recommendation; recommendation = $"{prefix}{++suffix}"; @@ -95,15 +113,52 @@ public string GenerateValidName(ProjectDefinition target, List throw new ArgumentException("Failed to generate a valid and unique name."); } - /// - /// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html - /// > - public bool IsValidName(string name) => _validatorRegex.IsMatch(name); - public string InvalidNameMessage(string name) + public CloudApplicationNameValidationResult IsValidName(string name, IList existingApplications, DeploymentTypes? deploymentType = null) + { + var errorMessage = string.Empty; + + if (!SatisfiesRegex(name)) + { + errorMessage += $"The application name can contain only alphanumeric characters (case-sensitive) and hyphens. " + + $"It must start with an alphabetic character and can't be longer than 128 characters.{Environment.NewLine}"; + } + if (MatchesExistingDeployment(name, existingApplications, deploymentType)) + { + errorMessage += "A cloud application already exists with this name."; + } + + if (string.IsNullOrEmpty(errorMessage)) + return new CloudApplicationNameValidationResult(true, string.Empty); + + return new CloudApplicationNameValidationResult(false, $"Invalid cloud application name: {name}{Environment.NewLine}{errorMessage}"); + } + + /// + /// This method first filters the existing applications by the current deploymentType if the deploymentType is not null + /// It will then check if the current name matches the filtered list of existing applications + /// + /// User provided cloud application name + /// The deployment type of the selected recommendation + /// List of existing deployed applications + /// true if found a match. false otherwise + private bool MatchesExistingDeployment(string name, IList existingApplications, DeploymentTypes? deploymentType = null) + { + if (!existingApplications.Any()) + return false; + + if (deploymentType != null) + existingApplications = existingApplications.Where(x => x.DeploymentType == deploymentType).ToList(); + + return existingApplications.Any(x => x.Name == name); + } + + /// + /// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html + /// + private bool SatisfiesRegex(string name) { - return $"Invalid cloud application name {name}. The application name can contain only alphanumeric characters (case-sensitive) and hyphens. " + - "It must start with an alphabetic character and can't be longer than 128 characters"; + return _validatorRegex.IsMatch(name); } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index e0ed24da1..b2a3016a8 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -157,49 +157,6 @@ public async Task GetRecommendationsWithEncryptedCredentials() } } - [Fact] - public async Task SetInvalidCloudFormationStackName() - { - var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var portNumber = 4080; - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); - - var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); - var cancelSource = new CancellationTokenSource(); - - var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); - try - { - var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); - await WaitTillServerModeReady(restClient); - - var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput - { - AwsRegion = _awsRegion, - ProjectPath = projectPath - }); - - var sessionId = startSessionOutput.SessionId; - Assert.NotNull(sessionId); - - var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); - Assert.NotEmpty(getRecommendationOutput.Recommendations); - var beanstalkRecommendation = getRecommendationOutput.Recommendations.FirstOrDefault(); - - var exception = await Assert.ThrowsAsync>(() => restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput - { - NewDeploymentRecipeId = beanstalkRecommendation.RecipeId, - NewDeploymentName = "Hello$World" - })); - - Assert.Contains("Invalid cloud application name Hello$World.", exception.Result.Detail); - } - finally - { - cancelSource.Cancel(); - } - } - [Fact] public async Task WebFargateDeploymentNoConfigChanges() { @@ -405,8 +362,7 @@ public async Task InvalidStackName_ThrowsException(string invalidStackName) Assert.Equal(400, exception.StatusCode); - var errorMessage = $"Invalid cloud application name {invalidStackName}. The application name can contain only alphanumeric characters (case-sensitive) and hyphens. " + - "It must start with an alphabetic character and can't be longer than 128 characters"; + var errorMessage = $"Invalid cloud application name: {invalidStackName}"; Assert.Contains(errorMessage, exception.Result.Detail); } finally diff --git a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs index d5f00e5e8..4ecb20e34 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/CloudApplicationNameGeneratorTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Utilities; using Moq; using Should; @@ -38,11 +39,11 @@ public CloudApplicationNameGeneratorTests() [InlineData("Valid")] [InlineData("A21")] [InlineData("Very-Long-With-Hyphens-And-Numbers")] - public void ValidNamesAreValid(string name) + public void ValidNamesAreValid_WithRespectTo_Regex(string name) { - _cloudApplicationNameGenerator - .IsValidName(name) - .ShouldBeTrue(); + var existingApplications = new List(); + var validationResult = _cloudApplicationNameGenerator.IsValidName(name, existingApplications); + validationResult.IsValid.ShouldBeTrue(); } [Theory] @@ -50,11 +51,11 @@ public void ValidNamesAreValid(string name) [InlineData("withSpecial!こんにちは世界Characters")] [InlineData("With.Periods")] [InlineData("With Spaces")] - public void InvalidNamesAreInvalid(string name) + public void InvalidNamesAreInvalid_WithRespectTo_Regex(string name) { - _cloudApplicationNameGenerator - .IsValidName(name) - .ShouldBeFalse(); + var existingApplications = new List(); + var validationResult = _cloudApplicationNameGenerator.IsValidName(name, existingApplications); + validationResult.IsValid.ShouldBeFalse(); } [Theory] @@ -77,10 +78,10 @@ public async Task SuggestsValidName(string projectFile) var recommendation = _cloudApplicationNameGenerator.GenerateValidName(projectDefinition, existingApplication); // ACT - var recommendationIsValid = _cloudApplicationNameGenerator.IsValidName(recommendation); + var validationResult = _cloudApplicationNameGenerator.IsValidName(recommendation, existingApplication); // ASSERT - recommendationIsValid.ShouldBeTrue(); + validationResult.IsValid.ShouldBeTrue(); } [Fact] @@ -173,5 +174,51 @@ public async Task SuggestsValidNameAndRespectsExistingApplications_MultipleProje // ASSERT recommendation.ShouldEqual(expectedRecommendation); } + + [Theory] + [InlineData("application1", DeploymentTypes.CdkProject)] + [InlineData("application2", DeploymentTypes.CdkProject)] + [InlineData("application3", DeploymentTypes.BeanstalkEnvironment)] + public void InvalidNamesAreInvalid_WithRespectTo_ExistingApplications(string name, DeploymentTypes deploymentType) + { + // ARRANGE + var existingApplications = new List() + { + new CloudApplication("application1", "id1", CloudApplicationResourceType.CloudFormationStack, "recipe1"), + new CloudApplication("application2", "id2", CloudApplicationResourceType.CloudFormationStack, "recipe2"), + new CloudApplication("application3", "id3", CloudApplicationResourceType.BeanstalkEnvironment, "recipe3"), + new CloudApplication("application4", "id4", CloudApplicationResourceType.CloudFormationStack, "recipe1"), + }; + + // ACT + var validationResult = _cloudApplicationNameGenerator.IsValidName(name, existingApplications, deploymentType); + + // ASSERT + validationResult.IsValid.ShouldBeFalse(); + } + + [Theory] + [InlineData("application", DeploymentTypes.CdkProject)] + [InlineData("application6", DeploymentTypes.CdkProject)] + [InlineData("application1", DeploymentTypes.BeanstalkEnvironment)] + [InlineData("application3", DeploymentTypes.CdkProject)] + public void ValidNamesAreValid_WithRespectTo_ExistingApplications(string name, DeploymentTypes deploymentType) + { + // ARRANGE + var existingApplications = new List() + { + new CloudApplication("application1", "id1", CloudApplicationResourceType.CloudFormationStack, "recipe1"), + new CloudApplication("application2", "id2", CloudApplicationResourceType.CloudFormationStack, "recipe2"), + new CloudApplication("application3", "id3", CloudApplicationResourceType.BeanstalkEnvironment, "recipe3"), + new CloudApplication("application4", "id4", CloudApplicationResourceType.CloudFormationStack, "recipe1"), + new CloudApplication("application5", "id4", CloudApplicationResourceType.BeanstalkEnvironment, "recipe3"), + }; + + // ACT + var validationResult = _cloudApplicationNameGenerator.IsValidName(name, existingApplications, deploymentType); + + // ASSERT + validationResult.IsValid.ShouldBeTrue(); + } } } From b8770e5d26f7b0f2461fc9fd51703e05c2e3f397 Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Fri, 13 May 2022 22:23:51 -0400 Subject: [PATCH 6/8] fix: Validate and respect deployment bundle settings for the Docker or dotnet builds during server mode --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 30 +--------- .../DockerExecutionDirectoryCommand.cs | 13 ++++- .../TypeHints/DotnetPublishArgsCommand.cs | 22 ++++--- .../Controllers/DeploymentController.cs | 2 +- src/AWS.Deploy.Common/IO/DirectoryManager.cs | 35 +++++++++++ .../Recipes/IOptionSettingHandler.cs | 2 +- .../OptionSettingItem.ValueOverride.cs | 4 +- .../Recipes/OptionSettingItem.cs | 5 +- .../OptionSettingItemValidatorList.cs | 14 ++++- .../DirectoryExistsValidator.cs | 36 ++++++++++++ .../DotnetPublishArgsValidator.cs | 43 ++++++++++++++ .../Recipes/Validation/ValidatorFactory.cs | 5 +- .../OptionSettingHandler.cs | 43 +++++++++++++- src/AWS.Deploy.Orchestration/Orchestrator.cs | 4 +- .../Container.deploymentbundle | 14 ++++- .../DotnetPublishZipFile.deploymentbundle | 7 ++- .../aws-deploy-recipe-schema.json | 5 +- .../IO/TestDirectoryManager.cs | 3 + ...pRunnerOptionSettingItemValidationTests.cs | 2 +- ...anStalkOptionSettingItemValidationTests.cs | 22 ++++++- .../OptionSettingsItemValidationTests.cs | 8 +-- .../Validation/ValidatorFactoryTests.cs | 1 + .../GetOptionSettingTests.cs | 8 +-- .../RecommendationTests.cs | 24 ++++---- .../SetOptionSettingTests.cs | 58 ++++++++++++++++--- .../ElasticBeanstalkHandlerTests.cs | 8 +-- .../TestDirectoryManager.cs | 3 + 27 files changed, 330 insertions(+), 91 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index a738ac2e4..3acb38a3c 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -460,9 +460,7 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidValueForOptionSettingItem, $"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); } - _optionSettingHandler.SetOptionSettingValue(optionSetting, settingValue); - - SetDeploymentBundleOptionSetting(recommendation, optionSetting.Id, settingValue); + _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, settingValue); } } @@ -487,30 +485,6 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us throw new InvalidUserDeploymentSettingsException(DeployToolErrorCode.DeploymentConfigurationNeedsAdjusting, errorMessage.Trim()); } - private void SetDeploymentBundleOptionSetting(Recommendation recommendation, string optionSettingId, object settingValue) - { - switch (optionSettingId) - { - case "DockerExecutionDirectory": - ActivatorUtilities.CreateInstance(_serviceProvider).OverrideValue(recommendation, settingValue.ToString() ?? ""); - break; - case "DockerBuildArgs": - ActivatorUtilities.CreateInstance(_serviceProvider).OverrideValue(recommendation, settingValue.ToString() ?? ""); - break; - case "DotnetBuildConfiguration": - ActivatorUtilities.CreateInstance(_serviceProvider).Overridevalue(recommendation, settingValue.ToString() ?? ""); - break; - case "DotnetPublishArgs": - ActivatorUtilities.CreateInstance(_serviceProvider).OverrideValue(recommendation, settingValue.ToString() ?? ""); - break; - case "SelfContainedBuild": - ActivatorUtilities.CreateInstance(_serviceProvider).OverrideValue(recommendation, (bool)settingValue); - break; - default: - return; - } - } - // This method prompts the user to select a CloudApplication name for existing deployments or create a new one. // If a user chooses to create a new CloudApplication, then this method returns string.Empty private string AskForCloudApplicationNameFromDeployedApplications(List deployedApplications) @@ -855,7 +829,7 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, Opt { try { - _optionSettingHandler.SetOptionSettingValue(setting, settingValue); + _optionSettingHandler.SetOptionSettingValue(recommendation, setting, settingValue); } catch (ValidationFailedException ex) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index 6940f5e4b..390aaa5e1 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -7,6 +7,7 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Common.TypeHintData; namespace AWS.Deploy.CLI.Commands.TypeHints @@ -56,10 +57,16 @@ public void OverrideValue(Recommendation recommendation, string executionDirecto private string ValidateExecutionDirectory(string executionDirectory) { - if (!string.IsNullOrEmpty(executionDirectory) && !_directoryManager.Exists(executionDirectory)) - return "The directory specified for Docker execution does not exist."; + var validationResult = new DirectoryExistsValidator(_directoryManager).Validate(executionDirectory); + + if (validationResult.IsValid) + { + return string.Empty; + } else - return ""; + { // Override the generic ValidationResultMessage with one about the the Docker execution directory + return "The directory specified for Docker execution does not exist."; + } } } } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index eec8a3fe3..043e2b419 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System; using System.Collections.Generic; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Common.TypeHintData; namespace AWS.Deploy.CLI.Commands.TypeHints @@ -56,18 +56,16 @@ public void OverrideValue(Recommendation recommendation, string publishArgs) private string ValidateDotnetPublishArgs(string publishArgs) { - var resultString = string.Empty; + var validationResult = new DotnetPublishArgsValidator().Validate(publishArgs); - if (publishArgs.Contains("-o ") || publishArgs.Contains("--output ")) - resultString += "You must not include -o/--output as an additional argument as it is used internally." + Environment.NewLine; - if (publishArgs.Contains("-c ") || publishArgs.Contains("--configuration ")) - resultString += "You must not include -c/--configuration as an additional argument. You can set the build configuration in the advanced settings." + Environment.NewLine; - if (publishArgs.Contains("--self-contained") || publishArgs.Contains("--no-self-contained")) - resultString += "You must not include --self-contained/--no-self-contained as an additional argument. You can set the self-contained property in the advanced settings." + Environment.NewLine; - - if (!string.IsNullOrEmpty(resultString)) - return "Invalid valid value for Dotnet Publish Arguments." + Environment.NewLine + resultString.Trim(); - return ""; + if (validationResult.IsValid) + { + return string.Empty; + } + else + { + return validationResult.ValidationFailedMessage ?? "Invalid value for Dotnet Publish Arguments."; + } } } } diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index c548f46b7..d18cdf534 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -251,7 +251,7 @@ public IActionResult ApplyConfigSettings(string sessionId, [FromBody] ApplyConfi try { var setting = optionSettingHandler.GetOptionSetting(state.SelectedRecommendation, updatedSetting.Key); - optionSettingHandler.SetOptionSettingValue(setting, updatedSetting.Value); + optionSettingHandler.SetOptionSettingValue(state.SelectedRecommendation, setting, updatedSetting.Value); } catch (Exception ex) { diff --git a/src/AWS.Deploy.Common/IO/DirectoryManager.cs b/src/AWS.Deploy.Common/IO/DirectoryManager.cs index 05a2ad96f..e6ae1765d 100644 --- a/src/AWS.Deploy.Common/IO/DirectoryManager.cs +++ b/src/AWS.Deploy.Common/IO/DirectoryManager.cs @@ -13,7 +13,30 @@ public interface IDirectoryManager { DirectoryInfo CreateDirectory(string path); DirectoryInfo GetDirectoryInfo(string path); + + /// + /// Determines whether the given path refers to an existing directory on disk. + /// This can either be an absolute path or relative to the current working directory. + /// + /// The path to test + /// + /// true if path refers to an existing directory; + /// false if the directory does not exist or an error occurs when trying to determine if the specified directory exists + /// bool Exists(string path); + + /// + /// Determines whether the given path refers to an existing directory on disk. + /// This can either be an absolute path or relative to the given directory. + /// + /// The path to test + /// Directory to consider the path as relative to + /// + /// true if path refers to an existing directory; + /// false if the directory does not exist or an error occurs when trying to determine if the specified directory exists + /// + bool Exists(string path, string relativeTo); + string[] GetFiles(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly); string[] GetDirectories(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly); bool IsEmpty(string path); @@ -38,6 +61,18 @@ public class DirectoryManager : IDirectoryManager public bool Exists(string path) => IsDirectoryValid(path); + public bool Exists(string path, string relativeTo) + { + if (Path.IsPathRooted(path)) + { + return Exists(path); + } + else + { + return Exists(Path.Combine(relativeTo, path)); + } + } + public string[] GetFiles(string path, string? searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => Directory.GetFiles(path, searchPattern ?? "*", searchOption); diff --git a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs index 2861c315c..aefcf65f6 100644 --- a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs +++ b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs @@ -13,7 +13,7 @@ public interface IOptionSettingHandler /// Due to different validations that could be put in place, access to other services may be needed. /// This method is meant to control access to those services and determine the value to be set. /// - void SetOptionSettingValue(OptionSettingItem optionSettingItem, object value); + void SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value); /// /// This method retrieves the related to a specific . diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index 63ab219a5..d2115c5b4 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -92,7 +92,7 @@ public object GetValue(IDictionary replacementTokens, IDictionar /// Thrown if one or more determine /// is not valid. /// - public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride, IOptionSettingItemValidator[] validators) + public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride, IOptionSettingItemValidator[] validators, Recommendation recommendation) { var isValid = true; var validationFailedMessage = string.Empty; @@ -148,7 +148,7 @@ public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOve { if (deserialized.TryGetValue(childOptionSetting.Id, out var childValueOverride)) { - optionSettingHandler.SetOptionSettingValue(childOptionSetting, childValueOverride); + optionSettingHandler.SetOptionSettingValue(recommendation, childOptionSetting, childValueOverride); } } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index 35c88a0bc..9f7fcd13e 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -40,8 +40,9 @@ public interface IOptionSettingItem /// /// Handler use to set any child option settings /// Value to set - /// /// Validators for this item - void SetValue(IOptionSettingHandler optionSettingHandler, object value, IOptionSettingItemValidator[] validators); + /// Validators for this item + /// Selected recommendation + void SetValue(IOptionSettingHandler optionSettingHandler, object value, IOptionSettingItemValidator[] validators, Recommendation recommendation); } /// diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs index acb293c24..366f0ca72 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs @@ -16,6 +16,18 @@ public enum OptionSettingItemValidatorList /// /// Must be paired with /// - Required + Required, + /// + /// Must be paired with + /// + DirectoryExists, + /// + /// Must be paired with + /// + DockerBuildArgs, + /// + /// Must be paried with + /// + DotnetPublishArgs } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs new file mode 100644 index 000000000..c74523004 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validator that validates if a given directory exists + /// + public class DirectoryExistsValidator : IOptionSettingItemValidator + { + private readonly IDirectoryManager _directoryManager; + + public DirectoryExistsValidator(IDirectoryManager directoryManager) + { + _directoryManager = directoryManager; + } + + /// + /// Validates that the given directory exists. + /// This can be either an absolute path, or a path relative to the project directory. + /// + /// Path to validate + /// Valid if the directory exists, invalid otherwise + public ValidationResult Validate(object input) + { + var executionDirectory = (string)input; + + if (!string.IsNullOrEmpty(executionDirectory) && !_directoryManager.Exists(executionDirectory)) + return ValidationResult.Failed("The specified directory does not exist."); + else + return ValidationResult.Valid(); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs new file mode 100644 index 000000000..9c1f898de --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validator for additional arguments to 'dotnet publish' + /// + public class DotnetPublishArgsValidator : IOptionSettingItemValidator + { + /// + /// Validates that additional 'dotnet publish' arguments do not collide with those used by the deploy tool + /// + /// Additional publish arguments + /// Valid if the arguments don't interfere with the deploy tool, invalid otherwise + public ValidationResult Validate(object input) + { + var publishArgs = Convert.ToString(input); + var errorMessage = string.Empty; + + if (string.IsNullOrEmpty(publishArgs)) + { + return ValidationResult.Valid(); + } + + if (publishArgs.Contains("-o ") || publishArgs.Contains("--output ")) + errorMessage += "You must not include -o/--output as an additional argument as it is used internally." + Environment.NewLine; + + if (publishArgs.Contains("-c ") || publishArgs.Contains("--configuration ")) + errorMessage += "You must not include -c/--configuration as an additional argument. You can set the build configuration in the advanced settings." + Environment.NewLine; + + if (publishArgs.Contains("--self-contained") || publishArgs.Contains("--no-self-contained")) + errorMessage += "You must not include --self-contained/--no-self-contained as an additional argument. You can set the self-contained property in the advanced settings." + Environment.NewLine; + + if (!string.IsNullOrEmpty(errorMessage)) + return ValidationResult.Failed("Invalid value for Dotnet Publish Arguments." + Environment.NewLine + errorMessage.Trim()); + + return ValidationResult.Valid(); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 5c0abd807..8b645836e 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -47,7 +47,10 @@ public ValidatorFactory(IServiceProvider serviceProvider) { { OptionSettingItemValidatorList.Range, typeof(RangeValidator) }, { OptionSettingItemValidatorList.Regex, typeof(RegexValidator) }, - { OptionSettingItemValidatorList.Required, typeof(RequiredValidator) } + { OptionSettingItemValidatorList.Required, typeof(RequiredValidator) }, + { OptionSettingItemValidatorList.DirectoryExists, typeof(DirectoryExistsValidator) }, + { OptionSettingItemValidatorList.DockerBuildArgs, typeof(DockerBuildArgsValidator) }, + { OptionSettingItemValidatorList.DotnetPublishArgs, typeof(DotnetPublishArgsValidator) }, }; private static readonly Dictionary _recipeValidatorTypeMapping = new() diff --git a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs index fb431512f..ff7ab4491 100644 --- a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs +++ b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System; using System.Collections.Generic; using System.Linq; using AWS.Deploy.Common; @@ -26,9 +27,47 @@ public OptionSettingHandler(IValidatorFactory validatorFactory) /// Thrown if one or more determine /// is not valid. /// - public void SetOptionSettingValue(OptionSettingItem optionSettingItem, object value) + public void SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value) { - optionSettingItem.SetValue(this, value, _validatorFactory.BuildValidators(optionSettingItem)); + optionSettingItem.SetValue(this, value, _validatorFactory.BuildValidators(optionSettingItem), recommendation); + + // If the optionSettingItem came from the selected recommendation's deployment bundle, + // set the corresponding property on recommendation.DeploymentBundle + SetDeploymentBundleProperty(recommendation, optionSettingItem, value); + } + + /// + /// Sets the corresponding value in when the + /// corresponding was just set + /// + /// Selected recommendation + /// Option setting that was just set + /// Value that was just set, assumed to be valid + private void SetDeploymentBundleProperty(Recommendation recommendation, OptionSettingItem optionSettingItem, object value) + { + switch (optionSettingItem.Id) + { + case "DockerExecutionDirectory": + recommendation.DeploymentBundle.DockerExecutionDirectory = value.ToString() ?? string.Empty; + break; + case "DockerBuildArgs": + recommendation.DeploymentBundle.DockerBuildArgs = value.ToString() ?? string.Empty; + break; + case "ECRRepositoryName": + recommendation.DeploymentBundle.ECRRepositoryName = value.ToString() ?? string.Empty; + break; + case "DotnetBuildConfiguration": + recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = value.ToString() ?? string.Empty; + break; + case "DotnetPublishArgs": + recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments = value.ToString() ?? string.Empty; + break; + case "SelfContainedBuild": + recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = Convert.ToBoolean(value); + break; + default: + return; + } } /// diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index 0bbdb0d1b..d4735950d 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -157,7 +157,7 @@ public Recommendation ApplyRecommendationPreviousSettings(Recommendation recomme { if (previousSettings.TryGetValue(optionSetting.Id, out var value)) { - _optionSettingHandler.SetOptionSettingValue(optionSetting, value); + _optionSettingHandler.SetOptionSettingValue(recommendationCopy, optionSetting, value); } } @@ -255,6 +255,8 @@ private async Task CreateContainerDeploymentBundle(CloudApplication cloudApplica _dockerEngine.DetermineDockerExecutionDirectory(recommendation); + // Read this from the OptionSetting instead of recommendation.DeploymentBundle. + // When its value comes from a replacement token, it wouldn't have been set back to the DeploymentBundle var respositoryName = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "ECRRepositoryName")); string imageTag; diff --git a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle index d1537328c..91ac45fe9 100644 --- a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle +++ b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle @@ -9,7 +9,12 @@ "TypeHint": "DockerBuildArgs", "DefaultValue": "", "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "DockerBuildArgs" + } + ] }, { "Id": "DockerExecutionDirectory", @@ -19,7 +24,12 @@ "TypeHint": "DockerExecutionDirectory", "DefaultValue": "", "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "DirectoryExists" + } + ] }, { "Id": "ECRRepositoryName", diff --git a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/DotnetPublishZipFile.deploymentbundle b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/DotnetPublishZipFile.deploymentbundle index 9405c6a0e..aae6026a1 100644 --- a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/DotnetPublishZipFile.deploymentbundle +++ b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/DotnetPublishZipFile.deploymentbundle @@ -19,7 +19,12 @@ "TypeHint": "DotnetPublishAdditionalBuildArguments", "DefaultValue": "", "AdvancedSetting": true, - "Updatable": true + "Updatable": true, + "Validators": [ + { + "ValidatorType": "DotnetPublishArgs" + } + ] }, { "Id": "SelfContainedBuild", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json index 4aaa89a88..511fa068a 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -602,7 +602,10 @@ "enum": [ "Range", "Regex", - "Required" + "Required", + "DirectoryExists", + "DockerBuildArgs", + "DotnetPublishArgs" ] } }, diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs index 9fdccc997..ab885adb8 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs @@ -37,6 +37,9 @@ public bool Exists(string path) return CreatedDirectories.Contains(path); } + public bool Exists(string path, string relativeTo) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + 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."); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs index 1b6a9abcd..6d985fc7c 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs @@ -71,7 +71,7 @@ private void Validate(OptionSettingItem optionSettingItem, T value, bool isVa ValidationFailedException exception = null; try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, value); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); } catch (ValidationFailedException e) { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs index 9b07bfd19..1113a90fe 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs @@ -104,6 +104,26 @@ public void ElasticBeanstalkRollingUpdatesPauseTime(string value, bool isValid) Validate(optionSettingItem, value, isValid); } + [Theory] + [InlineData("", true)] + [InlineData("--no-restore --nologo --framework net5.0", true)] + [InlineData("-o dir", false)] // -o or --output is reserved by the deploy tool + [InlineData("--output dir", false)] + [InlineData("-c Release", false)] // -c or --configuration is controlled by DotnetPublishBuildConfiguration instead + [InlineData("--configuration Release", false)] + [InlineData("--self-contained true", false)] // --self-contained is controlled by SelfContainedBuild instead + [InlineData("--no-self-contained", false)] + public void DotnetPublishArgsValidationTest(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.DotnetPublishArgs + }); + + Validate(optionSettingItem, value, isValid); + } + private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) { var regexValidatorConfig = new OptionSettingItemValidatorConfig @@ -137,7 +157,7 @@ private void Validate(OptionSettingItem optionSettingItem, T value, bool isVa try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, value); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); } catch (ValidationFailedException e) { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs index a88a21c0f..54db114e8 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs @@ -64,7 +64,7 @@ public void InvalidInputInMultipleValidatorsThrowsException(string invalidValue) // ACT try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, invalidValue); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); } catch (ValidationFailedException e) { @@ -100,7 +100,7 @@ public void InvalidInputInSingleValidatorThrowsException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, invalidValue); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); } catch (ValidationFailedException e) { @@ -142,7 +142,7 @@ public void ValidInputDoesNotThrowException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, validValue); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, validValue); } catch (ValidationFailedException e) { @@ -186,7 +186,7 @@ public void CustomValidatorMessagePropagatesToValidationException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, invalidValue); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); } catch (ValidationFailedException e) { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs index ee76c1974..df8abbb47 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs @@ -34,6 +34,7 @@ public ValidatorFactoryTests() var mockServiceProvider = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IOptionSettingHandler))).Returns(_optionSettingHandler); + mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(new TestDirectoryManager()); _serviceProvider = mockServiceProvider.Object; _validatorFactory = new ValidatorFactory(_serviceProvider); } diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index 1e612a55d..8aadd5fab 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -89,7 +89,7 @@ public async Task GetOptionSettingTests_GetDisplayableChildren(string optionSett var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var managedActionsEnabled = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, $"{optionSetting}.{childSetting}"); - _optionSettingHandler.SetOptionSettingValue(managedActionsEnabled, childValue); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, managedActionsEnabled, childValue); var elasticBeanstalkManagedPlatformUpdates = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, optionSetting); var elasticBeanstalkManagedPlatformUpdatesValue = _optionSettingHandler.GetOptionSettingValue>(beanstalkRecommendation, elasticBeanstalkManagedPlatformUpdates); @@ -109,8 +109,8 @@ public async Task GetOptionSettingTests_ListType_InvalidValue() var subnets = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.Subnets"); var securityGroups = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.SecurityGroups"); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(subnets, new SortedSet(){ "subnet1" })); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(securityGroups, new SortedSet(){ "securityGroup1" })); + Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet1" })); + Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, securityGroups, new SortedSet(){ "securityGroup1" })); } [Fact] @@ -125,7 +125,7 @@ public async Task GetOptionSettingTests_ListType() var subnets = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.Subnets"); var emptySubnetsValue = _optionSettingHandler.GetOptionSettingValue(appRunnerRecommendation, subnets); - _optionSettingHandler.SetOptionSettingValue(subnets, new SortedSet(){ "subnet-1234abcd" }); + _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet-1234abcd" }); var subnetsValue = _optionSettingHandler.GetOptionSettingValue(appRunnerRecommendation, subnets); var emptySubnetsString = Assert.IsType(emptySubnetsValue); diff --git a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs index bc7414068..71038fc98 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -185,11 +185,11 @@ public async Task ResetOptionSettingValue_Int() var originalDefaultValue = _optionSettingHandler.GetOptionSettingDefaultValue(fargateRecommendation, desiredCountOptionSetting); - _optionSettingHandler.SetOptionSettingValue(desiredCountOptionSetting, 2); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, 2); Assert.Equal(2, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting)); - _optionSettingHandler.SetOptionSettingValue(desiredCountOptionSetting, consoleUtilities.AskUserForValue("Title", "2", true, originalDefaultValue.ToString())); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, consoleUtilities.AskUserForValue("Title", "2", true, originalDefaultValue.ToString())); Assert.Equal(originalDefaultValue, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting)); } @@ -214,11 +214,11 @@ public async Task ResetOptionSettingValue_String() var originalDefaultValue = _optionSettingHandler.GetOptionSettingDefaultValue(fargateRecommendation, ecsServiceNameOptionSetting); - _optionSettingHandler.SetOptionSettingValue(ecsServiceNameOptionSetting, "TestService"); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, "TestService"); Assert.Equal("TestService", _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting)); - _optionSettingHandler.SetOptionSettingValue(ecsServiceNameOptionSetting, consoleUtilities.AskUserForValue("Title", "TestService", true, originalDefaultValue)); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, consoleUtilities.AskUserForValue("Title", "TestService", true, originalDefaultValue)); Assert.Equal(originalDefaultValue, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting)); } @@ -262,7 +262,7 @@ public async Task ValueMappingSetWithValue() var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var environmentTypeOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("EnvironmentType")); - _optionSettingHandler.SetOptionSettingValue(environmentTypeOptionSetting, "LoadBalanced"); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); Assert.Equal("LoadBalanced", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting)); } @@ -276,7 +276,7 @@ public async Task ObjectMappingSetWithValue() var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); - _optionSettingHandler.SetOptionSettingValue(applicationIAMRoleOptionSetting, new IAMRoleTypeHintResponse {CreateNew = false, + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting, new IAMRoleTypeHintResponse {CreateNew = false, RoleArn = "arn:aws:iam::123456789012:group/Developers" }); var iamRoleTypeHintResponse = _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting); @@ -391,7 +391,7 @@ public async Task IsDisplayable_OneDependency() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, loadBalancerTypeOptionSetting)); // Satisfy dependency - _optionSettingHandler.SetOptionSettingValue(environmentTypeOptionSetting, "LoadBalanced"); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); Assert.Equal("LoadBalanced", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting)); // Verify @@ -414,11 +414,11 @@ public async Task IsDisplayable_ManyDependencies() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(fargateRecommendation, vpcIdOptionSetting)); // Satisfy dependencies - _optionSettingHandler.SetOptionSettingValue(isDefaultOptionSetting, false); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, isDefaultOptionSetting, false); Assert.False(_optionSettingHandler.GetOptionSettingValue(fargateRecommendation, isDefaultOptionSetting)); // Default value for Vpc.CreateNew already false, this is to show explicitly setting an override that satisfies Vpc Id option setting - _optionSettingHandler.SetOptionSettingValue(createNewOptionSetting, false); + _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, createNewOptionSetting, false); Assert.False(_optionSettingHandler.GetOptionSettingValue(fargateRecommendation, createNewOptionSetting)); // Verify @@ -441,7 +441,7 @@ public async Task IsDisplayable_NotEmptyOperation() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); // Satisfy dependencies - _optionSettingHandler.SetOptionSettingValue(vpcIdOptionSetting, "vpc-1234abcd"); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); } @@ -463,11 +463,11 @@ public async Task IsDisplayable_NotEmptyOperation_ListType() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); // Satisfy 1 dependency - _optionSettingHandler.SetOptionSettingValue(vpcIdOptionSetting, "vpc-1234abcd"); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); // Satisfy 2 dependencies - _optionSettingHandler.SetOptionSettingValue(subnetsSetting, new SortedSet { "subnet-1234abcd" }); + _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, subnetsSetting, new SortedSet { "subnet-1234abcd" }); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); } diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index df05aabef..2bc7ab0f2 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -29,8 +29,9 @@ public class SetOptionSettingTests public SetOptionSettingTests() { var projectPath = SystemIOUtilities.ResolvePath("WebAppNoDockerFile"); + var directoryManager = new DirectoryManager(); - var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); + var parser = new ProjectDefinitionParser(new FileManager(), directoryManager); var awsCredentials = new Mock(); var session = new OrchestratorSession( parser.Parse(projectPath).Result, @@ -43,7 +44,11 @@ public SetOptionSettingTests() var engine = new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); _recommendations = engine.ComputeRecommendations().GetAwaiter().GetResult(); - _serviceProvider = new Mock().Object; + + var mockServiceProvider = new Mock(); + mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(directoryManager); + _serviceProvider = mockServiceProvider.Object; + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } @@ -57,7 +62,7 @@ public void SetOptionSettingTests_AllowedValues() var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); - _optionSettingHandler.SetOptionSettingValue(optionSetting, optionSetting.AllowedValues.First()); + _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.AllowedValues.First()); Assert.Equal(optionSetting.AllowedValues.First(), _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting)); } @@ -74,7 +79,7 @@ public void SetOptionSettingTests_MappedValues() var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("EnvironmentType")); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(optionSetting, optionSetting.ValueMapping.Values.First())); + Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.ValueMapping.Values.First())); } [Fact] @@ -84,7 +89,7 @@ public void SetOptionSettingTests_KeyValueType() var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); var values = new Dictionary() { { "key", "value" } }; - _optionSettingHandler.SetOptionSettingValue(optionSetting, values); + _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, values); Assert.Equal(values, _optionSettingHandler.GetOptionSettingValue>(recommendation, optionSetting)); } @@ -97,7 +102,7 @@ public void SetOptionSettingTests_KeyValueType_String() var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); var dictionary = new Dictionary() { { "key", "value" } }; var dictionaryString = JsonConvert.SerializeObject(dictionary); - _optionSettingHandler.SetOptionSettingValue(optionSetting, dictionaryString); + _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, dictionaryString); Assert.Equal(dictionary, _optionSettingHandler.GetOptionSettingValue>(recommendation, optionSetting)); } @@ -108,7 +113,46 @@ public void SetOptionSettingTests_KeyValueType_Error() var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); var optionSetting = recommendation.Recipe.OptionSettings.First(x => x.Id.Equals("ElasticBeanstalkEnvironmentVariables")); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(optionSetting, "string")); + Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, "string")); + } + + /// + /// Verifies that calling SetOptionSettingValue for Docker-related settings + /// also sets the corresponding value in recommendation.DeploymentBundle + /// + [Fact] + public void DeploymentBundleWriteThrough_Docker() + { + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_APPRUNNER_ID); + + var dockerExecutionDirectory = SystemIOUtilities.ResolvePath("WebAppNoDockerFile"); + var dockerBuildArgs = "arg1=val1, arg2=val2"; + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DockerExecutionDirectory"), dockerExecutionDirectory); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DockerBuildArgs"), dockerBuildArgs); + + Assert.Equal(dockerExecutionDirectory, recommendation.DeploymentBundle.DockerExecutionDirectory); + Assert.Equal(dockerBuildArgs, recommendation.DeploymentBundle.DockerBuildArgs); + } + + /// + /// Verifies that calling SetOptionSettingValue for dotnet publish settings + /// also sets the corresponding value in recommendation.DeploymentBundle + /// + [Fact] + public void DeploymentBundleWriteThrough_Dotnet() + { + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var dotnetBuildConfiguration = "Debug"; + var dotnetPublishArgs = "--force --nologo"; + var selfContainedBuild = true; + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DotnetBuildConfiguration"), dotnetBuildConfiguration); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DotnetPublishArgs"), dotnetPublishArgs); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "SelfContainedBuild"), selfContainedBuild); + + Assert.Equal(dotnetBuildConfiguration, recommendation.DeploymentBundle.DotnetPublishBuildConfiguration); + Assert.Equal(dotnetPublishArgs, recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments); + Assert.True(recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild); } } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs index 348480577..aa6793395 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs @@ -90,10 +90,10 @@ public async Task GetAdditionSettingsTest_CustomValues() new Mock().Object, _optionSettingHandler); - _optionSettingHandler.SetOptionSettingValue(_optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId), "basic"); - _optionSettingHandler.SetOptionSettingValue(_optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.HealthCheckURLOptionId), "/url"); - _optionSettingHandler.SetOptionSettingValue(_optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.ProxyOptionId), "none"); - _optionSettingHandler.SetOptionSettingValue(_optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.XRayTracingOptionId), "true"); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId), "basic"); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.HealthCheckURLOptionId), "/url"); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.ProxyOptionId), "none"); + _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.XRayTracingOptionId), "true"); // ACT var optionSettings = elasticBeanstalkHandler.GetEnvironmentConfigurationSettings(recommendation); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs index 4e9019f1a..598398bd2 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs @@ -38,6 +38,9 @@ public bool Exists(string path) return CreatedDirectories.Contains(path); } + public bool Exists(string path, string relativeTo) => + throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + 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."); From df4438858f67042a9dbfd6c4c0801f0e8bd75301 Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Wed, 18 May 2022 14:28:43 -0400 Subject: [PATCH 7/8] fix: Rework the "Docker Build Args" option to support appending any options to the docker build command, instead of just `--build-arg` BREAKING CHANGE: If you were previously using "Docker Build Args" to set build-time variables, you must now prepend `--build-arg` for each argument yourself. --- .../TypeHints/DockerBuildArgsCommand.cs | 20 +++---- .../DockerBuildArgsValidator.cs | 42 ++++++++++++++ .../DeploymentBundleHandler.cs | 23 +++----- .../Container.deploymentbundle | 2 +- ...FargateOptionSettingItemValidationTests.cs | 56 ++++++++++++++++++- 5 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs index d68e15799..7d293f3e7 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Generic; -using System.Text.RegularExpressions; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Common.TypeHintData; namespace AWS.Deploy.CLI.Commands.TypeHints @@ -55,20 +55,16 @@ public void OverrideValue(Recommendation recommendation, string dockerBuildArgs) private string ValidateBuildArgs(string buildArgs) { - var argsList = buildArgs.Split(","); - if (argsList.Length == 0) - return ""; + var validationResult = new DockerBuildArgsValidator().Validate(buildArgs); - foreach (var arg in argsList) + if (validationResult.IsValid) { - var keyValue = arg.Split("="); - if (keyValue.Length == 2) - return ""; - else - return "The Docker Build Args must have the following pattern 'arg1=val1,arg2=val2'."; + return string.Empty; + } + else + { + return validationResult.ValidationFailedMessage ?? "Invalid value for additional Docker build options."; } - - return ""; } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs new file mode 100644 index 000000000..371b26e61 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validator for Docker build-time variables, passed via --build-arg + /// + public class DockerBuildArgsValidator : IOptionSettingItemValidator + { + /// + /// Validates that additional Docker build options don't collide + /// with those set by the deploy tool + /// + /// Proposed Docker build args + /// Valid if the options do not contain those set by the deploy tool, invalid otherwise + public ValidationResult Validate(object input) + { + var buildArgs = Convert.ToString(input); + var errorMessage = string.Empty; + + if (string.IsNullOrEmpty(buildArgs)) + { + return ValidationResult.Valid(); + } + + if (buildArgs.Contains("-t ") || buildArgs.Contains("--tag ")) + errorMessage += "You must not include -t/--tag as an additional argument as it is used internally. " + + "You may set the Image Tag property in the advanced settings for some recipes." + Environment.NewLine; + + if (buildArgs.Contains("-f ") || buildArgs.Contains("--file ")) + errorMessage += "You must not include -f/--file as an additional argument as it is used internally." + Environment.NewLine; + + if (!string.IsNullOrEmpty(errorMessage)) + return ValidationResult.Failed("Invalid value for additional Docker build options." + Environment.NewLine + errorMessage.Trim()); + + return ValidationResult.Valid(); + } + } +} diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 68a47975f..827adaf44 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -178,21 +178,16 @@ private string GetDockerFilePath(Recommendation recommendation) private string GetDockerBuildArgs(Recommendation recommendation) { - var buildArgs = string.Empty; - var argsDictionary = recommendation.DeploymentBundle.DockerBuildArgs - .Split(',') - .Where(x => x.Contains("=")) - .ToDictionary( - k => k.Split('=')[0], - v => v.Split('=')[1] - ); - - foreach (var arg in argsDictionary.Keys) - { - buildArgs += $" --build-arg {arg}={argsDictionary[arg]}"; - } + var buildArgs = recommendation.DeploymentBundle.DockerBuildArgs; - return buildArgs; + if (string.IsNullOrEmpty(buildArgs)) + return buildArgs; + + // Ensure it starts with a space so it doesn't collide with the previous option + if (!char.IsWhiteSpace(buildArgs[0])) + return $" {buildArgs}"; + else + return buildArgs; } private async Task InitiateDockerLogin() diff --git a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle index 91ac45fe9..816b68d4c 100644 --- a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle +++ b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle @@ -4,7 +4,7 @@ { "Id": "DockerBuildArgs", "Name": "Docker Build Args", - "Description": "The list of additional docker build args.", + "Description": "The list of additional options to append to the `docker build` command.", "Type": "String", "TypeHint": "DockerBuildArgs", "DefaultValue": "", diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs index ef48efe7a..bb98901dd 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.IO; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using Moq; using Should; using Xunit; -using Xunit.Abstractions; namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation { @@ -17,10 +19,14 @@ public class ECSFargateOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; private readonly IServiceProvider _serviceProvider; + private readonly IDirectoryManager _directoryManager; public ECSFargateOptionSettingItemValidationTests() { - _serviceProvider = new Mock().Object; + _directoryManager = new TestDirectoryManager(); + var mockServiceProvider = new Mock(); + mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(_directoryManager); + _serviceProvider = mockServiceProvider.Object; _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } @@ -149,6 +155,50 @@ public void ECRRepositoryNameValidationTest(string value, bool isValid) Validate(optionSettingItem, value, isValid); } + [Theory] + [InlineData("", true)] + [InlineData("--build-arg arg=val --no-cache", true)] + [InlineData("-t name:tag", false)] + [InlineData("--tag name:tag", false)] + [InlineData("-f file", false)] + [InlineData("--file file", false)] + public void DockerBuildArgsValidationTest(string value, bool isValid) + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.DockerBuildArgs + }); + + Validate(optionSettingItem, value, isValid); + } + + [Fact] + public void DockerExecutionDirectory_AbsoluteExists() + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.DirectoryExists, + }); + + _directoryManager.CreateDirectory(Path.Join("C:", "project")); + + Validate(optionSettingItem, Path.Join("C:", "project"), true); + } + + [Fact] + public void DockerExecutionDirectory_AbsoluteDoesNotExist() + { + var optionSettingItem = new OptionSettingItem("id", "name", "description"); + optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.DirectoryExists, + }); + + Validate(optionSettingItem, Path.Join("C:", "other_project"), false); + } + private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) { var regexValidatorConfig = new OptionSettingItemValidatorConfig @@ -181,7 +231,7 @@ private void Validate(OptionSettingItem optionSettingItem, T value, bool isVa ValidationFailedException exception = null; try { - _optionSettingHandler.SetOptionSettingValue(optionSettingItem, value); + _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); } catch (ValidationFailedException e) { From 79def867fbde2810ae3efe5c942de5f3a8cc6256 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Thu, 19 May 2022 14:12:58 -0400 Subject: [PATCH 8/8] chore: Improve error message for missing NodeJS dependency --- .../Controllers/DeploymentController.cs | 6 +--- .../Models/SystemCapabilitySummary.cs | 10 +++--- .../SystemCapabilities.cs | 28 +++++---------- .../SystemCapabilityEvaluator.cs | 36 ++++++++++--------- src/AWS.Deploy.ServerMode.Client/RestAPI.cs | 8 +---- 5 files changed, 34 insertions(+), 54 deletions(-) diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index d18cdf534..4d2553978 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -464,11 +464,7 @@ public async Task GetCompatibility(string sessionId) var capabilities = await systemCapabilityEvaluator.EvaluateSystemCapabilities(state.SelectedRecommendation); - output.Capabilities = capabilities.Select(x => new SystemCapabilitySummary(x.Name, x.Installed, x.Available) - { - InstallationUrl = x.InstallationUrl, - Message = x.Message - }).ToList(); + output.Capabilities = capabilities.Select(x => new SystemCapabilitySummary(x.Name, x.Message, x.InstallationUrl)); return Ok(output); } diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs index 393ae86b6..a8a0fb55a 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/SystemCapabilitySummary.cs @@ -6,16 +6,14 @@ namespace AWS.Deploy.CLI.ServerMode.Models public class SystemCapabilitySummary { public string Name { get; set; } - public bool Installed { get; set; } - public bool Available { get; set; } - public string? Message { get; set; } + public string Message { get; set; } public string? InstallationUrl { get; set; } - public SystemCapabilitySummary(string name, bool installed, bool available) + public SystemCapabilitySummary(string name, string message, string? installationUrl = null) { Name = name; - Installed = installed; - Available = available; + Message = message; + InstallationUrl = installationUrl; } } } diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilities.cs b/src/AWS.Deploy.Orchestration/SystemCapabilities.cs index 102fe1cf5..b64055725 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilities.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilities.cs @@ -32,32 +32,22 @@ public DockerInfo( public class SystemCapability { - public string Name { get; set; } - public bool Installed { get; set; } - public bool Available { get; set; } - public string? Message { get; set; } - public string? InstallationUrl { get; set; } + public readonly string Name; + public readonly string Message; + public readonly string? InstallationUrl; - public SystemCapability(string name, bool installed, bool available) + public SystemCapability(string name, string message, string? installationUrl = null) { Name = name; - Installed = installed; - Available = available; + Message = message; + InstallationUrl = installationUrl; } public string GetMessage() { - if (!string.IsNullOrEmpty(Message)) - { - if (!string.IsNullOrEmpty(InstallationUrl)) - return $"{Message} {InstallationUrl}"; - else - return Message; - } - - var availabilityMessage = Available ? "and available" : "but not available"; - var installationMessage = Installed ? $"installed {availabilityMessage}" : "not installed"; - return $"The system capability '{Name}' is {installationMessage}"; + return string.IsNullOrEmpty(InstallationUrl) + ? Message + : $"{Message}{Environment.NewLine}You can install the missing {Name} dependency from: {InstallationUrl}"; } } } diff --git a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs index ae7096cda..c582dd7b8 100644 --- a/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs +++ b/src/AWS.Deploy.Orchestration/SystemCapabilityEvaluator.cs @@ -18,6 +18,12 @@ public interface ISystemCapabilityEvaluator public class SystemCapabilityEvaluator : ISystemCapabilityEvaluator { + private const string NODEJS_DEPENDENCY_NAME = "Node.js"; + private const string NODEJS_INSTALLATION_URL = "https://nodejs.org/en/download/"; + + private const string DOCKER_DEPENDENCY_NAME = "Docker"; + private const string DOCKER_INSTALLATION_URL = "https://docs.docker.com/engine/install/"; + private readonly ICommandLineWrapper _commandLineWrapper; private static readonly Version MinimumNodeJSVersion = new Version(10,13,0); @@ -86,21 +92,22 @@ public async Task> EvaluateSystemCapabilities(Recommendat { var capabilities = new List(); var systemCapabilities = await Evaluate(); + string? message; if (selectedRecommendation.Recipe.DeploymentType == DeploymentTypes.CdkProject) { if (systemCapabilities.NodeJsVersion == null) { - capabilities.Add(new SystemCapability("NodeJS", false, false) { - InstallationUrl = "https://nodejs.org/en/download/", - Message = $"The selected deployment uses the AWS CDK, which requires Node.js. AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended." - }); + message = $"The selected deployment uses the AWS CDK, which requires Node.js. AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended. " + + "Please restart your IDE/Shell after installing Node.js."; + + capabilities.Add(new SystemCapability(NODEJS_DEPENDENCY_NAME, message, NODEJS_INSTALLATION_URL)); } else if (systemCapabilities.NodeJsVersion < MinimumNodeJSVersion) { - capabilities.Add(new SystemCapability("NodeJS", false, false) { - InstallationUrl = "https://nodejs.org/en/download/", - Message = $"The selected deployment uses the AWS CDK, which requires a version of Node.js higher than your current installation ({systemCapabilities.NodeJsVersion}). AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended." - }); + message = $"The selected deployment uses the AWS CDK, which requires a version of Node.js higher than your current installation ({systemCapabilities.NodeJsVersion}). " + + $"AWS CDK requires {MinimumNodeJSVersion} of Node.js or later, and the latest LTS version is recommended. Please restart your IDE/Shell after installing Node.js"; + + capabilities.Add(new SystemCapability(NODEJS_DEPENDENCY_NAME, message, NODEJS_INSTALLATION_URL)); } } @@ -108,18 +115,13 @@ public async Task> EvaluateSystemCapabilities(Recommendat { if (!systemCapabilities.DockerInfo.DockerInstalled) { - capabilities.Add(new SystemCapability("Docker", false, false) - { - InstallationUrl = "https://docs.docker.com/engine/install/", - Message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for your OS." - }); + message = "The selected deployment option requires Docker, which was not detected. Please install and start the appropriate version of Docker for your OS."; + capabilities.Add(new SystemCapability(DOCKER_DEPENDENCY_NAME, message, DOCKER_INSTALLATION_URL)); } else if (!systemCapabilities.DockerInfo.DockerContainerType.Equals("linux", StringComparison.OrdinalIgnoreCase)) { - capabilities.Add(new SystemCapability("Docker", true, false) - { - Message = "The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue." - }); + message = "The deployment tool requires Docker to be running in linux mode. Please switch Docker to linux mode to continue."; + capabilities.Add(new SystemCapability(DOCKER_DEPENDENCY_NAME, message)); } } diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index 42a7c80bf..b532e1054 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -1789,7 +1789,7 @@ public partial class CategorySummary [Newtonsoft.Json.JsonProperty("displayName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string DisplayName { get; set; } - /// The order used to sort categories in UI screens. Categories will be showin in sorted descending order. + /// The order used to sort categories in UI screens. Categories will be shown in sorted descending order. [Newtonsoft.Json.JsonProperty("order", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public int Order { get; set; } @@ -2193,12 +2193,6 @@ public partial class SystemCapabilitySummary [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Name { get; set; } - [Newtonsoft.Json.JsonProperty("installed", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public bool Installed { get; set; } - - [Newtonsoft.Json.JsonProperty("available", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public bool Available { get; set; } - [Newtonsoft.Json.JsonProperty("message", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Message { get; set; }