diff --git a/.github/workflows/codebuild-ci.yml b/.github/workflows/codebuild-ci.yml index 75e2f0eec..ea62a1a8a 100644 --- a/.github/workflows/codebuild-ci.yml +++ b/.github/workflows/codebuild-ci.yml @@ -6,6 +6,9 @@ on: - main - dev +permissions: + id-token: write + jobs: build: runs-on: ubuntu-latest @@ -13,10 +16,15 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: - aws-access-key-id: ${{ secrets.CI_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.CI_AWS_ACCESS_KEY_SECRET }} + role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} aws-region: us-west-2 - name: Run CodeBuild + id: codebuild uses: aws-actions/aws-codebuild-run-build@v1.0.3 with: - project-name: aws-dotnet-deploy-ci + project-name: ${{ secrets.CI_AWS_CODE_BUILD_PROJECT_NAME }} + - name: CodeBuild Link + shell: pwsh + run: | + $buildId = "${{ steps.codebuild.outputs.aws-build-id }}" + echo $buildId diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index 6855e3b8c..ed53e3189 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -1,5 +1,3 @@ - - ** AWSSDK.AppRunner; version 3.7.3.11 -- https://www.nuget.org/packages/AWSSDK.AppRunner/ ** AWSSDK.CloudFront; version 3.7.3.10 -- https://www.nuget.org/packages/AWSSDK.CloudFront/ ** AWSSDK.CloudWatchEvents; version 3.7.3.14 -- https://www.nuget.org/packages/AWSSDK.CloudWatchEvents/ @@ -18,10 +16,11 @@ ** AWSSDK.Extensions.NETCore.Setup; version 3.7.1 -- https://www.nuget.org/packages/AWSSDK.Extensions.NETCore.Setup ** AWSSDK.IdentityManagement; version 3.7.2.25 -- https://www.nuget.org/packages/AWSSDK.IdentityManagement ** AWSSDK.SecurityToken; version 3.7.1.35 -- https://www.nuget.org/packages/AWSSDK.SecurityToken -** AWSSDK.SimpleSystemsManagement; version 3.7.16 -- https://www.nuget.org/packages/AWSSDK.SimpleSystemsManagement ** Constructs; version 10.0.0 -- https://www.nuget.org/packages/Constructs ** Amazon.CDK.Lib; version 2.13.0 -- https://www.nuget.org/packages/Amazon.CDK.Lib/ ** Amazon.JSII.Runtime; version 1.54.0 -- https://www.nuget.org/packages/Amazon.JSII.Runtime +** AWSSDK.CloudControlApi; version 3.7.2 -- https://www.nuget.org/packages/AWSSDK.CloudControlApi/ +** AWSSDK.SimpleSystemsManagement; version 3.7.16 -- https://www.nuget.org/packages/AWSSDK.SimpleSystemsManagement/ Apache License Version 2.0, January 2004 @@ -250,6 +249,36 @@ limitations under the License. * For Amazon.JSII.Runtime see also this required NOTICE: AWS Cloud Development Kit (AWS CDK) Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +* For AWSSDK.CloudControlApi see also this required NOTICE: + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* For AWSSDK.SimpleSystemsManagement see also this required NOTICE: + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +------ + +** Material for MkDocs; version 8.2.9 -- https://github.com/squidfunk/mkdocs-material +Copyright (c) 2016-2022 Martin Donath + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------ @@ -351,9 +380,7 @@ Copyright (c) 2016 Richard Morris Copyright (c) 2016 Richard Morris ** Swashbuckle.AspNetCore.SwaggerGen ; version 6.1.2 -- https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerGen/ Copyright (c) 2016 Richard Morris -** mkdocs-material; version 8.2.9 -- https://pypi.org/project/mkdocs-material/ -Copyright (c) 2016-2022 Martin Donath - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to diff --git a/buildtools/README.md b/buildtools/README.md new file mode 100644 index 000000000..a670d05e3 --- /dev/null +++ b/buildtools/README.md @@ -0,0 +1,19 @@ +# Setup + +1. Create CF template using `buildtools/ci.template` +2. Copy output `CodeBuildProjectName` & `OidcRole` output variables. +3. Create `CI_AWS_ROLE_ARN` repository secret with `OidcRole` value and + `CI_AWS_CODE_BUILD_PROJECT_NAME` repository secret with `CodeBuildProjectName` + value. +4. Voila! + +# Troubleshooting + +## thumbprint rotation +``` +Error: OpenIDConnect provider's HTTPS certificate doesn't match configured thumbprint +``` + +This can happen if GitHub has rotated the thumbprint of the certificate. Follow [this guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html) to generate new thumbprint. + +Redeploy the ci.template with the new thumbprint. Additionally, contact https://github.com/aws-actions/configure-aws-credentials/issues for the thumbprint rotation. diff --git a/buildtools/ci.buildspec.yml b/buildtools/ci.buildspec.yml new file mode 100644 index 000000000..f3827bd9a --- /dev/null +++ b/buildtools/ci.buildspec.yml @@ -0,0 +1,37 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 12 + commands: + # install .NET SDK + - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 5.0 + - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 6.0 + - export PATH="$PATH:$HOME/.dotnet" + pre_build: + commands: + - export ORIGINAL_AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID + - export ORIGINAL_AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY + - export ORIGINAL_AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN + - export DOTNET_CLI_TELEMETRY_OPTOUT=1 + - eval $(aws sts assume-role --role-arn arn:aws:iam::610240510716:role/aws-dotnet-deploy-ci-test-runner --role-session-name test | jq -r '.Credentials | "export AWS_ACCESS_KEY_ID=\(.AccessKeyId)\nexport AWS_SECRET_ACCESS_KEY=\(.SecretAccessKey)\nexport AWS_SESSION_TOKEN=\(.SessionToken)\n"') + + build: + commands: + - dotnet build AWS.Deploy.sln -c Release + - dotnet test AWS.Deploy.sln -c Release --no-build --logger trx --results-directory ./testresults + post_build: + commands: + - export AWS_ACCESS_KEY_ID=${ORIGINAL_AWS_ACCESS_KEY_ID} + - export AWS_SECRET_ACCESS_KEY=${ORIGINAL_AWS_SECRET_ACCESS_KEY} + - export AWS_SESSION_TOKEN=${ORIGINAL_AWS_SESSION_TOKEN} + - unset ORIGINAL_AWS_ACCESS_KEY_ID + - unset ORIGINAL_AWS_SECRET_ACCESS_KEY + - unset ORIGINAL_AWS_SESSION_TOKEN +reports: + aws-dotnet-deploy-tests: + file-format: VisualStudioTrx + files: + - '**/*' + base-directory: './testresults' diff --git a/buildtools/ci.template.yml b/buildtools/ci.template.yml new file mode 100644 index 000000000..af25bad0b --- /dev/null +++ b/buildtools/ci.template.yml @@ -0,0 +1,169 @@ +Parameters: + GitHubOrg: + Type: String + Default: "aws" + Description: The GitHub organization to use for the repository. + GitHubRepositoryName: + Description: The name of the GitHub repository to create the role template in and to use for the CodeBuild. + Type: String + Default: "aws-dotnet-deploy" + OIDCProviderArn: + Description: Arn for the GitHub OIDC Provider. Leave blank to create a new one or provide an existing Provider. There can only be one GitHub OIDC Provider per GitHubOrg per AWS Account. Example arn:aws:iam::665544332211:oidc-provider/token.actions.githubusercontent.com + Default: "" + Type: String + CodeBuildProjectName: + Description: Name of the CodeBuild project. + Default: "aws-dotnet-deploy-ci" + Type: String + CodeBuildArtifactsBucketName: + Description: Name of the buckets where the CodeBuild artifacts will be stored. + Default: "aws-dotnet-deploy-codebuild-artifacts" + Type: String + TestRunnerRoleArn: + Description: Role to assume when running tests. This role must already exsit. Role can be a different account. Example arn:aws:iam:112233445566::role/awsdotnet-deploy-ci-test-runner + Default: "" + Type: String + OidcRoleRoleName: + Description: Name of the role to use for the OIDC provider. + Default: "aws-dotnet-deploy-ci-role" + Type: String + + +Conditions: + CreateOIDCProvider: !Equals + - !Ref OIDCProviderArn + - "" + +Resources: + OidcRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Ref OidcRoleRoleName + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Action: sts:AssumeRoleWithWebIdentity + Principal: + Federated: !If + - CreateOIDCProvider + - !Ref GithubOidc + - !Ref OIDCProviderArn + Condition: + StringLike: + token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${GitHubRepositoryName}:* + Policies: + - PolicyName: !Sub "${AWS::StackName}-OIDC-Policy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + Resource: + - !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProjectName} + - Effect: Allow + Action: + - logs:GetLogEvents + Resource: + - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${CodeBuildProjectName}:* + - Effect: Allow + Action: + - s3:GetObject + - s3:GetObjectVersion + Resource: + - !Sub "${CodeBuildArtifactsBucket.Arn}/*" + + GithubOidc: + Type: AWS::IAM::OIDCProvider + Condition: CreateOIDCProvider + Properties: + Url: https://token.actions.githubusercontent.com + ClientIdList: + - sts.amazonaws.com + ThumbprintList: + - 6938fd4d98bab03faadb97b34396831e3780aea1 + + CodeBuildProject: + Type: AWS::CodeBuild::Project + Properties: + ConcurrentBuildLimit: 1 + Name: !Sub ${CodeBuildProjectName} + ServiceRole: !GetAtt CodeBuildProjectRole.Arn + Environment: + PrivilegedMode: true + ComputeType: BUILD_GENERAL1_LARGE + Type: LINUX_CONTAINER + ImagePullCredentialsType: CODEBUILD + Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 + EnvironmentVariables: + - Name: TEST_RUNNER_ROLE_ARN + Type: PLAINTEXT + Value: !Ref TestRunnerRoleArn + Source: + Type: GITHUB + Location: !Sub https://github.com/${GitHubOrg}/${GitHubRepositoryName} + BuildSpec: buildtools/ci.buildspec.yml + Artifacts: + Type: S3 + Packaging: ZIP + Location: !GetAtt CodeBuildArtifactsBucket.Arn + OverrideArtifactName: true + + + CodeBuildProjectRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${CodeBuildProjectName}-codebuild-service-role + AssumeRolePolicyDocument: + Statement: + - Action: ['sts:AssumeRole'] + Effect: Allow + Principal: + Service: [codebuild.amazonaws.com] + Version: '2012-10-17' + Path: / + Policies: + - PolicyName: !Sub "${AWS::StackName}-codebuild-service-role-policy" + PolicyDocument: + Version: '2012-10-17' + Statement: + - Action: + - 'logs:CreateLogGroup' + - 'logs:PutLogEvents' + - 'logs:CreateLogStream' + Effect: Allow + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${CodeBuildProjectName}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${CodeBuildProjectName}:*" + - Action: + - 'sts:AssumeRole' + Effect: Allow + Resource: + - !Ref TestRunnerRoleArn + - Action: + - codebuild:BatchPutTestCases + - codebuild:CreateReport + - codebuild:CreateReportGroup + - codebuild:UpdateReport + - codebuild:UpdateReportGroup + Effect: Allow + Resource: + - !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/* + - Action: + - 's3:PutObject' + Effect: Allow + Resource: + - !Sub "${CodeBuildArtifactsBucket.Arn}/*" + + CodeBuildArtifactsBucket: + Type: 'AWS::S3::Bucket' + DeletionPolicy: Retain + Properties: + BucketName: !Ref CodeBuildArtifactsBucketName + +Outputs: + OidcRole: + Value: !GetAtt OidcRole.Arn + CodeBuildProjectName: + Value: !Sub ${CodeBuildProjectName} diff --git a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs index c85ab5bac..32527926c 100644 --- a/src/AWS.Deploy.CLI/Commands/CommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/CommandFactory.cs @@ -26,6 +26,7 @@ using AWS.Deploy.Orchestration.ServiceHandlers; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.Commands { @@ -70,12 +71,12 @@ public class CommandFactory : ICommandFactory private readonly IDirectoryManager _directoryManager; private readonly IFileManager _fileManager; private readonly IDeploymentManifestEngine _deploymentManifestEngine; - private readonly ICustomRecipeLocator _customRecipeLocator; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly ICDKVersionDetector _cdkVersionDetector; private readonly IAWSServiceHandler _awsServiceHandler; private readonly IOptionSettingHandler _optionSettingHandler; private readonly IValidatorFactory _validatorFactory; + private readonly IRecipeHandler _recipeHandler; public CommandFactory( IServiceProvider serviceProvider, @@ -99,12 +100,12 @@ public CommandFactory( IDirectoryManager directoryManager, IFileManager fileManager, IDeploymentManifestEngine deploymentManifestEngine, - ICustomRecipeLocator customRecipeLocator, ILocalUserSettingsEngine localUserSettingsEngine, ICDKVersionDetector cdkVersionDetector, IAWSServiceHandler awsServiceHandler, IOptionSettingHandler optionSettingHandler, - IValidatorFactory validatorFactory) + IValidatorFactory validatorFactory, + IRecipeHandler recipeHandler) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -127,12 +128,12 @@ public CommandFactory( _directoryManager = directoryManager; _fileManager = fileManager; _deploymentManifestEngine = deploymentManifestEngine; - _customRecipeLocator = customRecipeLocator; _localUserSettingsEngine = localUserSettingsEngine; _cdkVersionDetector = cdkVersionDetector; _awsServiceHandler = awsServiceHandler; _optionSettingHandler = optionSettingHandler; _validatorFactory = validatorFactory; + _recipeHandler = recipeHandler; } public Command BuildRootCommand() @@ -209,7 +210,7 @@ private Command BuildDeployCommand() AWSProfileName = input.Profile ?? userDeploymentSettings?.AWSProfile ?? null }; - var dockerEngine = new DockerEngine.DockerEngine(projectDefinition, _fileManager); + var dockerEngine = new DockerEngine.DockerEngine(projectDefinition, _fileManager, _directoryManager); var deploy = new DeployCommand( _serviceProvider, @@ -228,14 +229,14 @@ private Command BuildDeployCommand() _cloudApplicationNameGenerator, _localUserSettingsEngine, _consoleUtilities, - _customRecipeLocator, _systemCapabilityEvaluator, session, _directoryManager, _fileManager, _awsServiceHandler, _optionSettingHandler, - _validatorFactory); + _validatorFactory, + _recipeHandler); var deploymentProjectPath = input.DeploymentProject ?? string.Empty; if (!string.IsNullOrEmpty(deploymentProjectPath)) @@ -462,6 +463,7 @@ private Command BuildDeploymentProjectCommand() _fileManager, session, _deploymentManifestEngine, + _recipeHandler, targetApplicationFullPath); await generateDeploymentProject.ExecuteAsync(saveDirectory, projectDisplayName); diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 3acb38a3c..2b2564fa4 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -24,6 +24,7 @@ using Newtonsoft.Json; using AWS.Deploy.Orchestration.ServiceHandlers; using Microsoft.Extensions.DependencyInjection; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.Commands { @@ -44,7 +45,6 @@ public class DeployCommand private readonly ICloudApplicationNameGenerator _cloudApplicationNameGenerator; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly IConsoleUtilities _consoleUtilities; - private readonly ICustomRecipeLocator _customRecipeLocator; private readonly ISystemCapabilityEvaluator _systemCapabilityEvaluator; private readonly OrchestratorSession _session; private readonly IDirectoryManager _directoryManager; @@ -53,6 +53,7 @@ public class DeployCommand private readonly IAWSServiceHandler _awsServiceHandler; private readonly IOptionSettingHandler _optionSettingHandler; private readonly IValidatorFactory _validatorFactory; + private readonly IRecipeHandler _recipeHandler; public DeployCommand( IServiceProvider serviceProvider, @@ -71,14 +72,14 @@ public DeployCommand( ICloudApplicationNameGenerator cloudApplicationNameGenerator, ILocalUserSettingsEngine localUserSettingsEngine, IConsoleUtilities consoleUtilities, - ICustomRecipeLocator customRecipeLocator, ISystemCapabilityEvaluator systemCapabilityEvaluator, OrchestratorSession session, IDirectoryManager directoryManager, IFileManager fileManager, IAWSServiceHandler awsServiceHandler, IOptionSettingHandler optionSettingHandler, - IValidatorFactory validatorFactory) + IValidatorFactory validatorFactory, + IRecipeHandler recipeHandler) { _serviceProvider = serviceProvider; _toolInteractiveService = toolInteractiveService; @@ -99,11 +100,11 @@ public DeployCommand( _fileManager = fileManager; _cdkVersionDetector = cdkVersionDetector; _cdkManager = cdkManager; - _customRecipeLocator = customRecipeLocator; _systemCapabilityEvaluator = systemCapabilityEvaluator; _awsServiceHandler = awsServiceHandler; _optionSettingHandler = optionSettingHandler; _validatorFactory = validatorFactory; + _recipeHandler = recipeHandler; } public async Task ExecuteAsync(string applicationName, string deploymentProjectPath, UserDeploymentSettings? userDeploymentSettings = null) @@ -165,8 +166,7 @@ private void DisplayOutputResources(List displayedResourc _deploymentBundleHandler, _localUserSettingsEngine, _dockerEngine, - _customRecipeLocator, - new List { RecipeLocator.FindRecipeDefinitionsPath() }, + _recipeHandler, _fileManager, _directoryManager, _awsServiceHandler, @@ -277,7 +277,7 @@ public async Task ConfigureDeployment(CloudApplication cloudApplication, Orchest if (userDeploymentSettings != null) { - ConfigureDeploymentFromConfigFile(selectedRecommendation, userDeploymentSettings); + await ConfigureDeploymentFromConfigFile(selectedRecommendation, userDeploymentSettings); } if (!_toolInteractiveService.DisableInteractive) @@ -336,7 +336,7 @@ private async Task GetSelectedRecommendationFromPreviousDeployme else previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication); - selectedRecommendation = orchestrator.ApplyRecommendationPreviousSettings(selectedRecommendation, previousSettings); + selectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(selectedRecommendation, previousSettings); var header = $"Loading {deployedApplication.DisplayName} settings:"; @@ -409,7 +409,7 @@ private async Task GetDeploymentProjectRecipeId(string deploymentProject /// /// The selected recommendation settings used for deployment /// The deserialized object from the user provided config file. - private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, UserDeploymentSettings userDeploymentSettings) + private async Task ConfigureDeploymentFromConfigFile(Recommendation recommendation, UserDeploymentSettings userDeploymentSettings) { foreach (var entry in userDeploymentSettings.LeafOptionSettingItems) { @@ -460,13 +460,14 @@ private void ConfigureDeploymentFromConfigFile(Recommendation recommendation, Us throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidValueForOptionSettingItem, $"Invalid value {optionSettingValue} for option setting item {optionSettingJsonPath}"); } - _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, settingValue); + await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, settingValue); } } var validatorFailedResults = _validatorFactory.BuildValidators(recommendation.Recipe) - .Select(validator => validator.Validate(recommendation, _session)) + .Select(async validator => await validator.Validate(recommendation, _session)) + .Select(x => x.Result) .Where(x => !x.IsValid) .ToList(); @@ -710,13 +711,16 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEn // deploy case, nothing more to configure if (string.IsNullOrEmpty(input)) { - var validatorFailedResults = + var settingValidatorFailedResults = _optionSettingHandler.RunOptionSettingValidators(recommendation); + + var recipeValidatorFailedResults = _validatorFactory.BuildValidators(recommendation.Recipe) - .Select(validator => validator.Validate(recommendation, _session)) + .Select(async validator => await validator.Validate(recommendation, _session)) + .Select(x => x.Result) .Where(x => !x.IsValid) .ToList(); - if (!validatorFailedResults.Any()) + if (!settingValidatorFailedResults.Any() && !recipeValidatorFailedResults.Any()) { // validation successful // deployment configured @@ -725,7 +729,9 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, IEn _toolInteractiveService.WriteLine(); _toolInteractiveService.WriteErrorLine("The deployment configuration needs to be adjusted before it can be deployed:"); - foreach (var result in validatorFailedResults) + foreach (var result in settingValidatorFailedResults) + _toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}"); + foreach (var result in recipeValidatorFailedResults) _toolInteractiveService.WriteErrorLine($" - {result.ValidationFailedMessage}"); _toolInteractiveService.WriteLine(); @@ -829,7 +835,7 @@ private async Task ConfigureDeploymentFromCli(Recommendation recommendation, Opt { try { - _optionSettingHandler.SetOptionSettingValue(recommendation, setting, settingValue); + await _optionSettingHandler.SetOptionSettingValue(recommendation, setting, settingValue); } catch (ValidationFailedException ex) { diff --git a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs index 4132e8135..047f8c445 100644 --- a/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/GenerateDeploymentProjectCommand.cs @@ -32,6 +32,7 @@ public class GenerateDeploymentProjectCommand private readonly IFileManager _fileManager; private readonly OrchestratorSession _session; private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IRecipeHandler _recipeHandler; private readonly string _targetApplicationFullPath; public GenerateDeploymentProjectCommand( @@ -43,6 +44,7 @@ public GenerateDeploymentProjectCommand( IFileManager fileManager, OrchestratorSession session, IDeploymentManifestEngine deploymentManifestEngine, + IRecipeHandler recipeHandler, string targetApplicationFullPath) { _toolInteractiveService = toolInteractiveService; @@ -53,6 +55,7 @@ public GenerateDeploymentProjectCommand( _fileManager = fileManager; _session = session; _deploymentManifestEngine = deploymentManifestEngine; + _recipeHandler = recipeHandler; _targetApplicationFullPath = targetApplicationFullPath; } @@ -65,7 +68,7 @@ public GenerateDeploymentProjectCommand( /// public async Task ExecuteAsync(string saveCdkDirectoryPath, string projectDisplayName) { - var orchestrator = new Orchestrator(_session, new[] { RecipeLocator.FindRecipeDefinitionsPath() }); + var orchestrator = new Orchestrator(_session, _recipeHandler); var recommendations = await GenerateRecommendationsToSaveDeploymentProject(orchestrator); var selectedRecommendation = _consoleUtilities.AskToChooseRecommendation(recommendations); diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs index 66b2e052f..626eff895 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkApplicationCommand.cs @@ -2,16 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; -using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs index a75789e74..bd5331725 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/BeanstalkEnvironmentCommand.cs @@ -7,6 +7,7 @@ using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs index 7d293f3e7..fbf97925e 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerBuildArgsCommand.cs @@ -31,7 +31,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", - validators: buildArgs => ValidateBuildArgs(buildArgs)) + validators: async buildArgs => await ValidateBuildArgs(buildArgs, recommendation)) .ToString() .Replace("\"", "\"\""); @@ -45,17 +45,17 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt /// /// The selected recommendation settings used for deployment /// Arguments to be passed when performing a Docker build - public void OverrideValue(Recommendation recommendation, string dockerBuildArgs) + public async Task OverrideValue(Recommendation recommendation, string dockerBuildArgs) { - var resultString = ValidateBuildArgs(dockerBuildArgs); + var resultString = await ValidateBuildArgs(dockerBuildArgs, recommendation); if (!string.IsNullOrEmpty(resultString)) throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidDockerBuildArgs, resultString); recommendation.DeploymentBundle.DockerBuildArgs = dockerBuildArgs; } - private string ValidateBuildArgs(string buildArgs) + private async Task ValidateBuildArgs(string buildArgs, Recommendation recommendation) { - var validationResult = new DockerBuildArgsValidator().Validate(buildArgs); + var validationResult = await new DockerBuildArgsValidator().Validate(buildArgs, recommendation); if (validationResult.IsValid) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs index 390aaa5e1..7e229b858 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DockerExecutionDirectoryCommand.cs @@ -35,7 +35,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", - validators: executionDirectory => ValidateExecutionDirectory(executionDirectory)); + validators: async executionDirectory => await ValidateExecutionDirectory(executionDirectory, recommendation)); recommendation.DeploymentBundle.DockerExecutionDirectory = settingValue; return Task.FromResult(settingValue); @@ -45,19 +45,26 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt /// This method will be invoked to set the Docker execution directory in the deployment bundle /// when it is specified as part of the user provided configuration file. /// - /// The selected recommendation settings used for deployment + /// The selected recommendation used for deployment /// The directory specified for Docker execution. - public void OverrideValue(Recommendation recommendation, string executionDirectory) + public async Task OverrideValue(Recommendation recommendation, string executionDirectory) { - var resultString = ValidateExecutionDirectory(executionDirectory); + var resultString = await ValidateExecutionDirectory(executionDirectory, recommendation); if (!string.IsNullOrEmpty(resultString)) throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidDockerExecutionDirectory, resultString); recommendation.DeploymentBundle.DockerExecutionDirectory = executionDirectory; } - private string ValidateExecutionDirectory(string executionDirectory) + /// + /// Validates that the Docker execution directory exists as either an + /// absolute path or a path relative to the project directory. + /// + /// Proposed Docker execution directory + /// The selected recommendation settings used for deployment + /// Empty string if the directory is valid, an error message if not + private async Task ValidateExecutionDirectory(string executionDirectory, Recommendation recommendation) { - var validationResult = new DirectoryExistsValidator(_directoryManager).Validate(executionDirectory); + var validationResult = await new DirectoryExistsValidator(_directoryManager).Validate(executionDirectory, recommendation); if (validationResult.IsValid) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs index c68cfd2b8..b86676589 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetBeanstalkPlatformArnCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs index 043e2b419..71b6e55ee 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DotnetPublishArgsCommand.cs @@ -31,7 +31,7 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), allowEmpty: true, resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "", - validators: publishArgs => ValidateDotnetPublishArgs(publishArgs)) + validators: async publishArgs => await ValidateDotnetPublishArgs(publishArgs, recommendation)) .ToString() .Replace("\"", "\"\""); @@ -46,17 +46,17 @@ public Task Execute(Recommendation recommendation, OptionSettingItem opt /// /// The selected recommendation settings used for deployment /// The user specified Dotnet build arguments. - public void OverrideValue(Recommendation recommendation, string publishArgs) + public async Task OverrideValue(Recommendation recommendation, string publishArgs) { - var resultString = ValidateDotnetPublishArgs(publishArgs); + var resultString = await ValidateDotnetPublishArgs(publishArgs, recommendation); if (!string.IsNullOrEmpty(resultString)) throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidDotnetPublishArgs, resultString); recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments = publishArgs.Replace("\"", "\"\""); } - private string ValidateDotnetPublishArgs(string publishArgs) + private async Task ValidateDotnetPublishArgs(string publishArgs, Recommendation recommendation) { - var validationResult = new DotnetPublishArgsValidator().Validate(publishArgs); + var validationResult = await new DotnetPublishArgsValidator().Validate(publishArgs, recommendation); if (validationResult.IsValid) { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs index 122ae2526..6e0f8e795 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/DynamoDBTableCommand.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs index 7f5ed75e5..e69812e46 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/EC2KeyPairCommand.cs @@ -7,6 +7,7 @@ using Amazon.EC2.Model; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs index 25ff92a9e..1b371dee7 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECRRepositoryCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.ECR.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs index 82fd18075..ede9338db 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ECSClusterCommand.cs @@ -8,6 +8,7 @@ using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs index 3aa2617f0..97a77f051 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingApplicationLoadBalancerCommand.cs @@ -8,6 +8,7 @@ using Amazon.ElasticLoadBalancingV2; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs index bde85d389..f29f70048 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSecurityGroupsCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.EC2.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs index 2ef30bbe4..cbc9f0fe0 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingSubnetsCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.EC2.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs index 84b2f742c..088aee00d 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.EC2.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs index cbaf988f3..670e52401 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/ExistingVpcConnectorCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.AppRunner.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs new file mode 100644 index 000000000..79e9d28c2 --- /dev/null +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/FilePathCommand.cs @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Threading.Tasks; +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 +{ + /// + /// Typehint that lets the user specify a path to a file. + /// This can either be an absolute path to the file or relative to the project path. + /// + public class FilePathCommand : ITypeHintCommand + { + private readonly IConsoleUtilities _consoleUtilities; + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IFileManager _fileManager; + + public FilePathCommand(IConsoleUtilities consoleUtilities , IOptionSettingHandler optionSettingHandler, IFileManager fileManager) + { + _consoleUtilities = consoleUtilities; + _optionSettingHandler = optionSettingHandler; + _fileManager = fileManager; + } + + /// + /// Not implemented, specific files are not suggested to the user + /// + /// Empty list + public Task?> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult?>(null); + + /// + /// Prompts the user to enter a path to a file + /// + public Task Execute(Recommendation recommendation, OptionSettingItem optionSetting) + { + var typeHintData = optionSetting.GetTypeHintData(); + + var userFilePath = _consoleUtilities + .AskUserForValue( + string.Empty, + _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting), + allowEmpty: typeHintData?.AllowEmpty ?? true, + resetValue: _optionSettingHandler.GetOptionSettingDefaultValue(recommendation, optionSetting) ?? "") ; + + return Task.FromResult(userFilePath); + } + + /// + /// This method will be invoked to set a file path setting in the deployment bundle + /// when it is specified as part of the user provided configuration file. + /// + /// The selected recommendation settings used for deployment + /// File path entered by the user + public async Task OverrideValue(Recommendation recommendation, string filePath) + { + var validator = new FileExistsValidator(_fileManager); + var validationResult = await (validator as IOptionSettingItemValidator).Validate(filePath, recommendation); + + if (!validationResult.IsValid) + throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidFilePath, validationResult.ValidationFailedMessage ?? validator.ValidationFailedMessage); + } + } +} diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs index ebd97bd87..6bbf2ea11 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/IAMRoleCommand.cs @@ -8,6 +8,7 @@ using Amazon.IdentityManagement.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs index b40b2488b..87d67cd67 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/InstanceTypeCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Amazon.EC2.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs index f2eaa9995..b731ea0e1 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/S3BucketNameCommand.cs @@ -10,6 +10,7 @@ using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; using Amazon.S3.Model; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs index a77ca7bbc..c8deb7d0d 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/SNSTopicArnsCommand.cs @@ -9,6 +9,7 @@ using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.Commands.TypeHints { diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs index 53fa002b7..5bcf8303b 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/SQSQueueUrlCommand.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs index 92f003272..8e3948ad4 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/TypeHintCommandFactory.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; @@ -67,7 +68,8 @@ public TypeHintCommandFactory(IServiceProvider serviceProvider, IToolInteractive { OptionSettingTypeHint.ExistingVpcConnector, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ExistingSubnets, ActivatorUtilities.CreateInstance(serviceProvider) }, { OptionSettingTypeHint.ExistingSecurityGroups, ActivatorUtilities.CreateInstance(serviceProvider) }, - { OptionSettingTypeHint.VPCConnector, ActivatorUtilities.CreateInstance(serviceProvider) } + { OptionSettingTypeHint.VPCConnector, ActivatorUtilities.CreateInstance(serviceProvider) }, + { OptionSettingTypeHint.FilePath, ActivatorUtilities.CreateInstance(serviceProvider) }, }; } diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs index 673d757db..a4a5deb9c 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VPCConnectorCommand.cs @@ -8,6 +8,7 @@ using Amazon.EC2.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs index 08721a3e8..29ad3d201 100644 --- a/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/TypeHints/VpcCommand.cs @@ -8,6 +8,7 @@ using Amazon.ECS.Model; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration; diff --git a/src/AWS.Deploy.CLI/ConsoleUtilities.cs b/src/AWS.Deploy.CLI/ConsoleUtilities.cs index 5c51b7ca3..54a910a8e 100644 --- a/src/AWS.Deploy.CLI/ConsoleUtilities.cs +++ b/src/AWS.Deploy.CLI/ConsoleUtilities.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; @@ -28,7 +29,7 @@ T AskUserToChoose(IList options, string title, T defaultValue, string? def void DisplayRow((string, int)[] row); UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, bool askNewName = true, string defaultNewName = "", bool canBeEmpty = false, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null); UserResponse AskUserToChooseOrCreateNew(IEnumerable options, string title, UserInputConfiguration userInputConfiguration, string? defaultChoosePrompt = null, string? defaultCreateNewPrompt = null, string? defaultCreateNewLabel = null); - string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func[] validators); + string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func>[] validators); string AskForEC2KeyPairSaveDirectory(string projectPath); YesNo AskYesNoQuestion(string question, string? defaultValue); YesNo AskYesNoQuestion(string question, YesNo? defaultValue = default); @@ -328,7 +329,7 @@ public UserResponse AskUserToChooseOrCreateNew(IEnumerable options, str }; } - public string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func[] validators) + public string AskUserForValue(string message, string defaultValue, bool allowEmpty, string resetValue = "", string? defaultAskValuePrompt = null, params Func>[] validators) { const string RESET = ""; var prompt = !string.IsNullOrEmpty(defaultAskValuePrompt) ? defaultAskValuePrompt : "Enter value"; @@ -367,7 +368,8 @@ public string AskUserForValue(string message, string defaultValue, bool allowEmp var errorMessages = validators - .Select(v => v(userValue)) + .Select(async v => await v(userValue)) + .Select(v => v.Result) .Where(e => !string.IsNullOrEmpty(e)) .ToList(); diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 770ed5940..464cc7ab8 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -5,6 +5,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; @@ -57,7 +58,6 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDisplayedResourcesHandler), typeof(DisplayedResourcesHandler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IZipFileManager), typeof(ZipFileManager), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDeploymentManifestEngine), typeof(DeploymentManifestEngine), lifetime)); - serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICustomRecipeLocator), typeof(CustomRecipeLocator), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ILocalUserSettingsEngine), typeof(LocalUserSettingsEngine), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandFactory), typeof(CommandFactory), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKVersionDetector), typeof(CDKVersionDetector), lifetime)); @@ -66,6 +66,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, 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)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(IRecipeHandler), typeof(RecipeHandler), 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.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 4d2553978..c7f61b524 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -33,6 +33,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.Common.TypeHintData; using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.ServerMode.Controllers { @@ -197,7 +198,7 @@ private List ListOptionSettingSummary(IOptionSettingHa foreach (var setting in configurableOptionSettings) { - var settingSummary = new OptionSettingItemSummary(setting.Id, setting.Name, setting.Description, setting.Type.ToString()) + var settingSummary = new OptionSettingItemSummary(setting.Id, setting.FullyQualifiedId, setting.Name, setting.Description, setting.Type.ToString()) { Category = setting.Category, TypeHint = setting.TypeHint?.ToString(), @@ -209,6 +210,7 @@ private List ListOptionSettingSummary(IOptionSettingHa SummaryDisplayable = optionSettingHandler.IsSummaryDisplayable(recommendation, setting), AllowedValues = setting.AllowedValues, ValueMapping = setting.ValueMapping, + Validation = setting.Validation, ChildOptionSettings = ListOptionSettingSummary(optionSettingHandler, recommendation, setting.ChildOptionSettings) }; @@ -228,7 +230,7 @@ private List ListOptionSettingSummary(IOptionSettingHa [SwaggerResponse(200, type: typeof(ApplyConfigSettingsOutput))] [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound)] [Authorize] - public IActionResult ApplyConfigSettings(string sessionId, [FromBody] ApplyConfigSettingsInput input) + public async Task ApplyConfigSettings(string sessionId, [FromBody] ApplyConfigSettingsInput input) { var state = _stateServer.Get(sessionId); if (state == null) @@ -251,7 +253,7 @@ public IActionResult ApplyConfigSettings(string sessionId, [FromBody] ApplyConfi try { var setting = optionSettingHandler.GetOptionSetting(state.SelectedRecommendation, updatedSetting.Key); - optionSettingHandler.SetOptionSettingValue(state.SelectedRecommendation, setting, updatedSetting.Value); + await optionSettingHandler.SetOptionSettingValue(state.SelectedRecommendation, setting, updatedSetting.Value); } catch (Exception ex) { @@ -425,7 +427,7 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody else previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment); - state.SelectedRecommendation = orchestrator.ApplyRecommendationPreviousSettings(state.SelectedRecommendation, previousSettings); + state.SelectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(state.SelectedRecommendation, previousSettings); state.ApplicationDetails.Name = existingDeployment.Name; state.ApplicationDetails.UniqueIdentifier = existingDeployment.UniqueIdentifier; @@ -532,6 +534,17 @@ public async Task StartDeployment(string sessionId) if (state.SelectedRecommendation == null) throw new SelectedRecommendationIsNullException("The selected recommendation is null or invalid."); + var optionSettingHandler = serviceProvider.GetRequiredService(); + var settingValidatorFailedResults = optionSettingHandler.RunOptionSettingValidators(state.SelectedRecommendation); + if (settingValidatorFailedResults.Any()) + { + var settingValidationErrorMessage = $"The deployment configuration needs to be adjusted before it can be deployed:{Environment.NewLine}"; + foreach (var result in settingValidatorFailedResults) + settingValidationErrorMessage += $" - {result.ValidationFailedMessage}{Environment.NewLine}{Environment.NewLine}"; + settingValidationErrorMessage += $"{Environment.NewLine}Please adjust your settings"; + return Problem(settingValidationErrorMessage); + } + var systemCapabilityEvaluator = serviceProvider.GetRequiredService(); var capabilities = await systemCapabilityEvaluator.EvaluateSystemCapabilities(state.SelectedRecommendation); @@ -713,9 +726,9 @@ private Orchestrator CreateOrchestrator(SessionState state, IServiceProvider? se serviceProvider.GetRequiredService(), new DockerEngine.DockerEngine( session.ProjectDefinition, - serviceProvider.GetRequiredService()), - serviceProvider.GetRequiredService(), - new List { RecipeLocator.FindRecipeDefinitionsPath() }, + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()), + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs index 71c5de849..b3ade005a 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/RecipeController.cs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AWS.Deploy.CLI.ServerMode.Models; using AWS.Deploy.Common; using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Utilities; +using AWS.Deploy.Recipes; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; @@ -20,12 +23,12 @@ namespace AWS.Deploy.CLI.ServerMode.Controllers [Route("api/v1/[controller]")] public class RecipeController : ControllerBase { - private readonly ICustomRecipeLocator _customRecipeLocator; + private readonly IRecipeHandler _recipeHandler; private readonly IProjectDefinitionParser _projectDefinitionParser; - public RecipeController(ICustomRecipeLocator customRecipeLocator, IProjectDefinitionParser projectDefinitionParser) + public RecipeController(IRecipeHandler recipeHandler, IProjectDefinitionParser projectDefinitionParser) { - _customRecipeLocator = customRecipeLocator; + _recipeHandler = recipeHandler; _projectDefinitionParser = projectDefinitionParser; } @@ -44,11 +47,14 @@ public async Task GetRecipe(string recipeId, [FromQuery] string? } ProjectDefinition? projectDefinition = null; - if(!string.IsNullOrEmpty(projectPath)) + var recipePaths = new HashSet { RecipeLocator.FindRecipeDefinitionsPath() }; + HashSet customRecipePaths = new HashSet(); + if (!string.IsNullOrEmpty(projectPath)) { projectDefinition = await _projectDefinitionParser.Parse(projectPath); + customRecipePaths = await _recipeHandler.LocateCustomRecipePaths(projectDefinition); } - var recipeDefinitions = await RecipeHandler.GetRecipeDefinitions(_customRecipeLocator, projectDefinition); + var recipeDefinitions = await _recipeHandler.GetRecipeDefinitions(recipeDefinitionPaths: recipePaths.Union(customRecipePaths).ToList()); var selectedRecipeDefinition = recipeDefinitions.FirstOrDefault(x => x.Id.Equals(recipeId)); if (selectedRecipeDefinition == null) diff --git a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs index 51138340e..7787f13bd 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Models/OptionSettingItemSummary.cs @@ -3,47 +3,111 @@ using System; using System.Collections.Generic; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.CLI.ServerMode.Models { public class OptionSettingItemSummary { + /// + /// The unique id of setting. This value will be persisted in other config files so its value should never change once a recipe is released. + /// public string Id { get; set; } + /// + /// The fully qualified id of the setting that includes the Id and all of the parent's Ids. + /// This helps easily reference the Option Setting without context of the parent setting. + /// + public string FullyQualifiedId { get; set; } + + /// + /// The display friendly name of the setting. + /// 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. + /// public string Description { get; set; } + /// + /// The value used for the recipe if it is set by the user. + /// public object? Value { get; set; } + /// + /// The type of primitive value expected for this setting. + /// For example String, Int + /// public string Type { get; set; } + /// + /// Hint the the UI what type of setting this is optionally add additional UI features to select a value. + /// For example a value of BeanstalkApplication tells the UI it can display the list of available Beanstalk applications for the user to pick from. + /// public string? TypeHint { get; set; } + /// + /// Type hint additional data required to facilitate handling of the option setting. + /// public Dictionary TypeHintData { get; set; } = new(StringComparer.OrdinalIgnoreCase); + /// + /// UI can use this to reduce the amount of settings to show to the user when confirming the recommendation. This can make it so the user sees only the most important settings + /// they need to know about before deploying. + /// public bool Advanced { get; set; } + /// + /// Indicates whether the setting can be edited + /// public bool ReadOnly { get; set; } + /// + /// Indicates whether the setting is visible/displayed on the UI + /// public bool Visible { get; set; } + /// + /// Indicates whether the setting can be displayed as part of the settings summary of the previous deployment. + /// public bool SummaryDisplayable { get; set; } + /// + /// The allowed values for the setting. + /// public IList AllowedValues { get; set; } = new List(); + /// + /// The value mapping for allowed values. The key of the dictionary is what is sent to services + /// and the value is the display value shown to users. + /// public IDictionary ValueMapping { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Child option settings for value types + /// value depends on the values of + /// public List ChildOptionSettings { get; set; } = new(); - public OptionSettingItemSummary(string id, string name, string description, string type) + /// + /// The validation state of the setting that contains the validation status and message. + /// + public OptionSettingValidation Validation { get; set; } + + public OptionSettingItemSummary(string id, string fullyQualifiedId, string name, string description, string type) { Id = id; + FullyQualifiedId = fullyQualifiedId; Name = name; Description = description; Type = type; + Validation = new OptionSettingValidation(); } } } diff --git a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj index 067479da6..c9c4ce6b8 100644 --- a/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj +++ b/src/AWS.Deploy.Common/AWS.Deploy.Common.csproj @@ -8,10 +8,27 @@ - + + + + + + + + + + + + + + + + + + diff --git a/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs new file mode 100644 index 000000000..0a90da5a1 --- /dev/null +++ b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.AppRunner.Model; +using Amazon.CloudFormation.Model; +using Amazon.CloudFront.Model; +using Amazon.CloudWatchEvents.Model; +using Amazon.EC2.Model; +using Amazon.ECR.Model; +using Amazon.ECS.Model; +using Amazon.ElasticBeanstalk.Model; +using Amazon.ElasticLoadBalancingV2; +using Amazon.IdentityManagement.Model; +using Amazon.S3.Model; +using Amazon.SecurityToken.Model; +using LoadBalancer = Amazon.ElasticLoadBalancingV2.Model.LoadBalancer; +using Listener = Amazon.ElasticLoadBalancingV2.Model.Listener; +using Amazon.CloudControlApi.Model; + +namespace AWS.Deploy.Common.Data +{ + public interface IAWSResourceQueryer + { + Task GetCloudControlApiResource(string type, string identifier); + Task> GetCloudFormationStackEvents(string stackName); + Task> ListOfAvailableInstanceTypes(); + Task DescribeAppRunnerService(string serviceArn); + Task> DescribeCloudFormationResources(string stackName); + Task DescribeElasticBeanstalkEnvironment(string environmentName); + Task DescribeElasticLoadBalancer(string loadBalancerArn); + Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn); + Task DescribeCloudWatchRule(string ruleName); + Task GetS3BucketLocation(string bucketName); + Task GetS3BucketWebSiteConfiguration(string bucketName); + Task> ListOfECSClusters(string? ecsClusterName = null); + Task> ListOfElasticBeanstalkApplications(string? applicationName = null); + Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null, string? environmentName = null); + Task> ListElasticBeanstalkResourceTags(string resourceArn); + Task> ListOfEC2KeyPairs(); + Task CreateEC2KeyPair(string keyName, string saveLocation); + Task> ListOfIAMRoles(string? servicePrincipal); + Task> GetListOfVpcs(); + Task> GetElasticBeanstalkPlatformArns(); + Task GetLatestElasticBeanstalkPlatformArn(); + Task> GetECRAuthorizationToken(); + Task> GetECRRepositories(List? repositoryNames = null); + Task CreateECRRepository(string repositoryName); + Task> GetCloudFormationStacks(); + Task GetCloudFormationStack(string stackName); + Task GetCallerIdentity(string awsRegion); + Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType); + Task GetCloudFrontDistribution(string distributionId); + Task> ListOfDyanmoDBTables(); + Task> ListOfSQSQueuesUrls(); + Task> ListOfSNSTopicArns(); + Task> ListOfS3Buckets(); + Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName); + Task DescribeECRRepository(string respositoryName); + Task> DescribeAppRunnerVpcConnectors(); + Task> DescribeSubnets(string? vpcID = null); + Task> DescribeSecurityGroups(string? vpcID = null); + Task GetParameterStoreTextValue(string parameterName); + } +} diff --git a/src/AWS.Deploy.Common/DeploymentBundles/DeploymentBundle.cs b/src/AWS.Deploy.Common/DeploymentBundles/DeploymentBundle.cs index 3039a220a..73ad669e4 100644 --- a/src/AWS.Deploy.Common/DeploymentBundles/DeploymentBundle.cs +++ b/src/AWS.Deploy.Common/DeploymentBundles/DeploymentBundle.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.IO; + namespace AWS.Deploy.Common { /// @@ -18,6 +20,11 @@ public class DeploymentBundle /// public string DockerBuildArgs { get; set; } = ""; + /// + /// The path to the Dockerfile. This can either be an absolute path or relative to the project directory. + /// + public string DockerfilePath { get; set; } = ""; + /// /// The ECR Repository Name where the docker image will be pushed to. /// diff --git a/src/AWS.Deploy.Common/Exceptions.cs b/src/AWS.Deploy.Common/Exceptions.cs index 3c301e218..074fc39b8 100644 --- a/src/AWS.Deploy.Common/Exceptions.cs +++ b/src/AWS.Deploy.Common/Exceptions.cs @@ -114,7 +114,10 @@ public enum DeployToolErrorCode ResourceQuery = 10009200, FailedToRetrieveStackId = 10009300, FailedToGetECRAuthorizationToken = 10009400, - InvalidCloudApplicationName = 10009500 + InvalidCloudApplicationName = 10009500, + SelectedValueIsNotAllowed = 10009600, + MissingValidatorConfiguration = 10009700, + InvalidFilePath = 10009800 } public class ProjectFileNotFoundException : DeployToolException @@ -211,6 +214,14 @@ public class ValidationFailedException : DeployToolException public ValidationFailedException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + /// + /// Thrown if Option Setting Item Validator has missing or invalid configuration. + /// + public class MissingValidatorConfigurationException : DeployToolException + { + public MissingValidatorConfigurationException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } + /// /// Exception thrown if Project Path contains an invalid path /// @@ -259,6 +270,13 @@ public class ResourceQueryException : DeployToolException public ResourceQueryException(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } } + /// + /// Thrown when an invalid file path is specified as an value + /// + public class InvalidFilePath : DeployToolException + { + public InvalidFilePath(DeployToolErrorCode errorCode, string message, Exception? innerException = null) : base(errorCode, message, innerException) { } + } public static class ExceptionExtensions { diff --git a/src/AWS.Deploy.Common/IO/FileManager.cs b/src/AWS.Deploy.Common/IO/FileManager.cs index de4b9d0e0..df85a5393 100644 --- a/src/AWS.Deploy.Common/IO/FileManager.cs +++ b/src/AWS.Deploy.Common/IO/FileManager.cs @@ -11,7 +11,29 @@ namespace AWS.Deploy.Common.IO { public interface IFileManager { + /// + /// Determines whether the specified file is at a valid path and exists. + /// This can either be an absolute path or relative to the current working directory. + /// + /// The file to check + /// + /// True if the path is valid, the caller has the required permissions, + /// and path contains the name of an existing file + /// bool Exists(string path); + + /// + /// Determines whether the specified file is at a valid path and exists. + /// This can either be an absolute path or relative to the given directory. + /// + /// The file to check + /// Directory to consider the path as relative to + /// + /// True if the path is valid, the caller has the required permissions, + /// and path contains the name of an existing file + /// + bool Exists(string path, string directory); + Task ReadAllTextAsync(string path); Task ReadAllLinesAsync(string path); Task WriteAllTextAsync(string filePath, string contents, CancellationToken cancellationToken = default); @@ -27,6 +49,18 @@ public class FileManager : IFileManager { public bool Exists(string path) => IsFileValid(path); + public bool Exists(string path, string directory) + { + if (Path.IsPathRooted(path)) + { + return Exists(path); + } + else + { + return Exists(Path.Combine(directory, path)); + } + } + public Task ReadAllTextAsync(string path) => File.ReadAllTextAsync(path); public Task ReadAllLinesAsync(string path) => File.ReadAllLinesAsync(path); diff --git a/src/AWS.Deploy.Common/ProjectDefinition.cs b/src/AWS.Deploy.Common/ProjectDefinition.cs index 85ea1edf4..ec6f94b3a 100644 --- a/src/AWS.Deploy.Common/ProjectDefinition.cs +++ b/src/AWS.Deploy.Common/ProjectDefinition.cs @@ -85,7 +85,7 @@ public ProjectDefinition( private bool CheckIfDockerFileExists(string projectPath) { var dir = Directory.GetFiles(new FileInfo(projectPath).DirectoryName ?? - throw new InvalidProjectPathException(DeployToolErrorCode.ProjectPathNotFound, "The project path is invalid."), "Dockerfile"); + throw new InvalidProjectPathException(DeployToolErrorCode.ProjectPathNotFound, "The project path is invalid."), Constants.Docker.DefaultDockerfileName); return dir.Length == 1; } diff --git a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs index aefcf65f6..57cfaeea3 100644 --- a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs +++ b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs @@ -1,6 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Generic; +using System.Threading.Tasks; +using AWS.Deploy.Common.Recipes.Validation; + namespace AWS.Deploy.Common.Recipes { /// @@ -8,18 +12,30 @@ namespace AWS.Deploy.Common.Recipes /// public interface IOptionSettingHandler { + /// + /// This method runs all the option setting validators for the configurable settings. + /// In case of a first time deployment, all settings and validators are run. + /// In case of a redeployment, only the updatable settings are considered. + /// + List RunOptionSettingValidators(Recommendation recommendation, IEnumerable? optionSettings = null); + /// /// This method is used to set values for . /// 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(Recommendation recommendation, OptionSettingItem optionSettingItem, object value); + Task SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value, bool skipValidation = false); /// /// This method retrieves the related to a specific . /// OptionSettingItem GetOptionSetting(Recommendation recommendation, string? jsonPath); + /// + /// This method retrieves the related to a specific . + /// + OptionSettingItem GetOptionSetting(RecipeDefinition recipe, string? jsonPath); + /// /// Retrieve the value for a specific /// This method retrieves the value in a specified type. diff --git a/src/AWS.Deploy.Common/Recipes/IRecipeHandler.cs b/src/AWS.Deploy.Common/Recipes/IRecipeHandler.cs new file mode 100644 index 000000000..1cfe41621 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/IRecipeHandler.cs @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AWS.Deploy.Common.Recipes +{ + public interface IRecipeHandler + { + /// + /// Retrieves all the that are defined by the system as well as any other recipes that are retrieved from an external source. + /// + Task> GetRecipeDefinitions(List? recipeDefinitionPaths = null); + + /// + /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + Task> LocateCustomRecipePaths(ProjectDefinition projectDefinition); + + /// + /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath); + } +} diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs index d2115c5b4..c9999cb0c 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.ValueOverride.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using AWS.Deploy.Common.Recipes.Validation; using Newtonsoft.Json; @@ -88,29 +89,41 @@ public object GetValue(IDictionary replacementTokens, IDictionar /// /// Assigns a value to the OptionSettingItem. /// + /// Value to assign + /// Current deployment recommendation, may be used if the validator needs to consider properties other than itself /// /// Thrown if one or more determine /// is not valid. /// - public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride, IOptionSettingItemValidator[] validators, Recommendation recommendation) + public async Task SetValue(IOptionSettingHandler optionSettingHandler, object valueOverride, IOptionSettingItemValidator[] validators, Recommendation recommendation, bool skipValidation) { - var isValid = true; - var validationFailedMessage = string.Empty; - foreach (var validator in validators) + if (!skipValidation) { - var result = validator.Validate(valueOverride); - if (!result.IsValid) + foreach (var validator in validators) { - isValid = false; - validationFailedMessage += result.ValidationFailedMessage + Environment.NewLine; + var result = await validator.Validate(valueOverride, recommendation); + if (!result.IsValid) + { + Validation.ValidationStatus = ValidationStatus.Invalid; + Validation.ValidationMessage = result.ValidationFailedMessage?.Trim() ?? $"The value '{valueOverride}' is invalid for option setting '{Name}'."; + Validation.InvalidValue = valueOverride; + throw new ValidationFailedException(DeployToolErrorCode.OptionSettingItemValueValidationFailed, Validation.ValidationMessage); + } } } - if (!isValid) - throw new ValidationFailedException(DeployToolErrorCode.OptionSettingItemValueValidationFailed, validationFailedMessage.Trim()); if (AllowedValues != null && AllowedValues.Count > 0 && valueOverride != null && !AllowedValues.Contains(valueOverride.ToString() ?? "")) - throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidValueForOptionSettingItem, $"Invalid value for option setting item '{Name}'"); + { + Validation.ValidationStatus = ValidationStatus.Invalid; + Validation.ValidationMessage = $"Invalid value for option setting item '{Name}'"; + Validation.InvalidValue = valueOverride; + throw new InvalidOverrideValueException(DeployToolErrorCode.InvalidValueForOptionSettingItem, Validation.ValidationMessage); + } + + Validation.ValidationStatus = ValidationStatus.Valid; + Validation.ValidationMessage = string.Empty; + Validation.InvalidValue = null; if (valueOverride is bool || valueOverride is int || valueOverride is long || valueOverride is double || valueOverride is Dictionary || valueOverride is SortedSet) { @@ -148,7 +161,7 @@ public void SetValue(IOptionSettingHandler optionSettingHandler, object valueOve { if (deserialized.TryGetValue(childOptionSetting.Id, out var childValueOverride)) { - optionSettingHandler.SetOptionSettingValue(recommendation, childOptionSetting, childValueOverride); + await optionSettingHandler.SetOptionSettingValue(recommendation, childOptionSetting, childValueOverride, skipValidation: skipValidation); } } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs index 9f7fcd13e..9e4cd2b82 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using AWS.Deploy.Common.Recipes.Validation; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -42,7 +43,8 @@ public interface IOptionSettingItem /// Value to set /// Validators for this item /// Selected recommendation - void SetValue(IOptionSettingHandler optionSettingHandler, object value, IOptionSettingItemValidator[] validators, Recommendation recommendation); + /// Enables or disables running validators specified in + Task SetValue(IOptionSettingHandler optionSettingHandler, object value, IOptionSettingItemValidator[] validators, Recommendation recommendation, bool skipValidation); } /// @@ -55,6 +57,17 @@ public partial class OptionSettingItem : IOptionSettingItem /// public string Id { get; set; } + /// + /// The fully qualified id of the setting that includes the Id and all of the parent's Ids. + /// This helps easily reference the Option Setting without context of the parent setting. + /// + public string FullyQualifiedId { get; set; } + + /// + /// The parent Option Setting Item that allows an option setting item to reference it's parent. + /// + public string? ParentId { get; set; } + /// /// The id of the parent option setting. This is used for tooling that wants to look up the existing resources for a setting based on the TypeHint but needs /// to know the parent AWS resource. For example if listing the available Beanstalk environments the listing should be for the environments of the Beanstalk application. @@ -120,7 +133,7 @@ public partial class OptionSettingItem : IOptionSettingItem /// The allowed values for the setting. /// public IList AllowedValues { get; set; } = new List(); - + /// /// The value mapping for allowed values. The key of the dictionary is what is sent to services /// and the value is the display value shown to users. @@ -143,11 +156,23 @@ public partial class OptionSettingItem : IOptionSettingItem /// public Dictionary TypeHintData { get; set; } = new (); - public OptionSettingItem(string id, string name, string description) + /// + /// Indicates which option setting items need to be validated if a value update occurs. + /// + public List Dependents { get; set; } = new List (); + + /// + /// The validation state of the setting that contains the validation status and message. + /// + public OptionSettingValidation Validation { get; set; } + + public OptionSettingItem(string id, string fullyQualifiedId, string name, string description) { Id = id; + FullyQualifiedId = fullyQualifiedId; Name = name; Description = description; + Validation = new OptionSettingValidation(); } /// diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs index 58cae543d..07435e6e0 100644 --- a/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs @@ -35,6 +35,7 @@ public enum OptionSettingTypeHint ExistingVpcConnector, ExistingSubnets, ExistingSecurityGroups, - VPCConnector + VPCConnector, + FilePath }; } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingValidation.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingValidation.cs new file mode 100644 index 000000000..c38e267be --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingValidation.cs @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Common.Recipes +{ + public enum ValidationStatus + { + Valid, + Invalid + } + + public class OptionSettingValidation + { + /// + /// Determines whether the current value as set by the user is in a valid or invalid state. + /// + public ValidationStatus ValidationStatus { get; set; } = ValidationStatus.Valid; + + /// + /// The validation message in the case where the value set by the user is an invalid one. + /// This is empty in the case where the value set by the user is a valid one. + /// + public string ValidationMessage { get; set; } = string.Empty; + + /// + /// The value last attempted to be set by the user in the case where that value is invalid. + /// This is null in the case where the value is valid. + /// + public object? InvalidValue { get; set; } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/IOptionSettingItemValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/IOptionSettingItemValidator.cs index e51ca964b..989c10b0e 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/IOptionSettingItemValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/IOptionSettingItemValidator.cs @@ -1,15 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { /// - /// This interface outlines the framework for OptionSettingItem validators. + /// This interface outlines the framework for validators. /// Validators such as implement this interface and provide custom validation logic /// on OptionSettingItems /// public interface IOptionSettingItemValidator { - ValidationResult Validate(object input); + /// + /// Validates an override value for an + /// + /// Raw input for an option + /// Selected recommendation, which may be used if the validator needs to consider properties other than itself + /// Whether or not the input is valid + Task Validate(object input, Recommendation recommendation); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs index 8eb7a3c31..e09faa714 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/IRecipeValidator.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { /// @@ -10,6 +12,6 @@ namespace AWS.Deploy.Common.Recipes.Validation /// public interface IRecipeValidator { - ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext); + Task Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs index 366f0ca72..e4b80a4c0 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidatorList.cs @@ -28,6 +28,14 @@ public enum OptionSettingItemValidatorList /// /// Must be paried with /// - DotnetPublishArgs + DotnetPublishArgs, + /// + /// Must be paired with + /// + ExistingResource, + /// + /// Must be paired with + /// + FileExists } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs index c74523004..a695942d4 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DirectoryExistsValidator.cs @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; using AWS.Deploy.Common.IO; namespace AWS.Deploy.Common.Recipes.Validation @@ -23,14 +24,14 @@ public DirectoryExistsValidator(IDirectoryManager directoryManager) /// /// Path to validate /// Valid if the directory exists, invalid otherwise - public ValidationResult Validate(object input) + public Task Validate(object input, Recommendation recommendation) { var executionDirectory = (string)input; if (!string.IsNullOrEmpty(executionDirectory) && !_directoryManager.Exists(executionDirectory)) - return ValidationResult.Failed("The specified directory does not exist."); + return ValidationResult.FailedAsync("The specified directory does not exist."); else - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs index 371b26e61..a4b8509f8 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DockerBuildArgsValidator.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Threading.Tasks; namespace AWS.Deploy.Common.Recipes.Validation { @@ -16,14 +17,14 @@ public class DockerBuildArgsValidator : IOptionSettingItemValidator /// /// Proposed Docker build args /// Valid if the options do not contain those set by the deploy tool, invalid otherwise - public ValidationResult Validate(object input) + public Task Validate(object input, Recommendation recommendation) { var buildArgs = Convert.ToString(input); var errorMessage = string.Empty; if (string.IsNullOrEmpty(buildArgs)) { - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } if (buildArgs.Contains("-t ") || buildArgs.Contains("--tag ")) @@ -34,9 +35,9 @@ public ValidationResult Validate(object input) 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.FailedAsync("Invalid value for additional Docker build options." + Environment.NewLine + errorMessage.Trim()); - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs index 9c1f898de..f8524b7dc 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/DotnetPublishArgsValidator.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Threading.Tasks; namespace AWS.Deploy.Common.Recipes.Validation { @@ -15,14 +16,14 @@ public class DotnetPublishArgsValidator : IOptionSettingItemValidator /// /// Additional publish arguments /// Valid if the arguments don't interfere with the deploy tool, invalid otherwise - public ValidationResult Validate(object input) + public Task Validate(object input, Recommendation recommendation) { var publishArgs = Convert.ToString(input); var errorMessage = string.Empty; if (string.IsNullOrEmpty(publishArgs)) { - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } if (publishArgs.Contains("-o ") || publishArgs.Contains("--output ")) @@ -35,9 +36,9 @@ public ValidationResult Validate(object input) 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.FailedAsync("Invalid value for Dotnet Publish Arguments." + Environment.NewLine + errorMessage.Trim()); - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/ExistingResourceValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/ExistingResourceValidator.cs new file mode 100644 index 000000000..636938fa5 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/ExistingResourceValidator.cs @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Threading.Tasks; +using Amazon.CloudControlApi.Model; +using AWS.Deploy.Common.Data; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + public class ExistingResourceValidator : IOptionSettingItemValidator + { + private readonly IAWSResourceQueryer _awsResourceQueryer; + + /// + /// The Cloud Control API resource type that will be used to query Cloud Control API for the existance of a resource. + /// + public string? ResourceType { get; set; } + + public ExistingResourceValidator(IAWSResourceQueryer awsResourceQueryer) + { + _awsResourceQueryer = awsResourceQueryer; + } + + public async Task Validate(object input, Recommendation recommendation) + { + if (string.IsNullOrEmpty(ResourceType)) + throw new MissingValidatorConfigurationException(DeployToolErrorCode.MissingValidatorConfiguration, $"The validator of type '{typeof(ExistingResourceValidator)}' is missing the configuration property '{nameof(ResourceType)}'."); + var resourceName = input?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(resourceName)) + return ValidationResult.Valid(); + + switch (ResourceType) + { + case "AWS::ElasticBeanstalk::Application": + var beanstalkApplications = await _awsResourceQueryer.ListOfElasticBeanstalkApplications(resourceName); + if (beanstalkApplications.Any(x => x.ApplicationName.Equals(resourceName))) + return ValidationResult.Failed($"An Elastic Beanstalk application already exists with the name '{resourceName}'. Check the AWS Console for more information on the existing resource."); + break; + + case "AWS::ElasticBeanstalk::Environment": + var beanstalkEnvironments = await _awsResourceQueryer.ListOfElasticBeanstalkEnvironments(environmentName: resourceName); + if (beanstalkEnvironments.Any(x => x.EnvironmentName.Equals(resourceName))) + return ValidationResult.Failed($"An Elastic Beanstalk environment already exists with the name '{resourceName}'. Check the AWS Console for more information on the existing resource."); + break; + + default: + try + { + var resource = await _awsResourceQueryer.GetCloudControlApiResource(ResourceType, resourceName); + return ValidationResult.Failed($"A resource of type '{ResourceType}' and name '{resourceName}' already exists. Check the AWS Console for more information on the existing resource."); + } + catch (ResourceQueryException ex) when (ex.InnerException is ResourceNotFoundException) + { + break; + } + } + + return ValidationResult.Valid(); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/FileExistsValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/FileExistsValidator.cs new file mode 100644 index 000000000..c671e05c5 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/FileExistsValidator.cs @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading.Tasks; +using AWS.Deploy.Common.IO; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// Validates that a recipe or deployment bundle option with a FilePath typehint points to an actual file. + /// This can either be an absolute path to the file or relative to the project path + /// + public class FileExistsValidator : IOptionSettingItemValidator + { + private readonly IFileManager _fileManager; + + public FileExistsValidator(IFileManager fileManager) + { + _fileManager = fileManager; + } + + public string ValidationFailedMessage { get; set; } = "The specified file does not exist"; + + /// + /// Whether or not an empty filepath is valid (essentially whether this option is required) + /// + public bool AllowEmptyString { get; set; } = true; + + public Task Validate(object input, Recommendation recommendation) + { + var inputFilePath = input?.ToString() ?? string.Empty; + + if (string.IsNullOrEmpty(inputFilePath)) + { + if (AllowEmptyString) + { + return ValidationResult.ValidAsync(); + } + else + { + return ValidationResult.FailedAsync("A file must be specified"); + } + } + + // Otherwise if there is a value, verify that it points to an actual file + if (_fileManager.Exists(inputFilePath, recommendation.GetProjectDirectory())) + { + return ValidationResult.ValidAsync(); + } + else + { + return ValidationResult.FailedAsync($"The specified file {inputFilePath} does not exist"); + } + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RangeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RangeValidator.cs index 75f61ba4c..c0d42932b 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RangeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RangeValidator.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { /// @@ -22,16 +24,16 @@ public class RangeValidator : IOptionSettingItemValidator public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; public bool AllowEmptyString { get; set; } - public ValidationResult Validate(object input) + public Task Validate(object input, Recommendation recommendation) { if (AllowEmptyString && string.IsNullOrEmpty(input?.ToString())) - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); if (int.TryParse(input?.ToString(), out var result) && result >= Min && result <= Max) { - return ValidationResult.Valid(); + return ValidationResult.ValidAsync(); } var message = @@ -39,7 +41,7 @@ public ValidationResult Validate(object input) .Replace("{{Min}}", Min.ToString()) .Replace("{{Max}}", Max.ToString()); - return ValidationResult.Failed(message); + return ValidationResult.FailedAsync(message); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RegexValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RegexValidator.cs index 85d5359c6..29e58440f 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RegexValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RegexValidator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; +using System.Threading.Tasks; using AWS.Deploy.Common.Extensions; namespace AWS.Deploy.Common.Recipes.Validation @@ -21,7 +22,7 @@ public class RegexValidator : IOptionSettingItemValidator public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; public bool AllowEmptyString { get; set; } - public ValidationResult Validate(object input) + public Task Validate(object input, Recommendation recommendation) { var regex = new Regex(Regex); @@ -34,24 +35,24 @@ public ValidationResult Validate(object input) { var valid = regex.IsMatch(item) || (AllowEmptyString && string.IsNullOrEmpty(item)); if (!valid) - return new ValidationResult + return Task.FromResult(new ValidationResult { IsValid = false, ValidationFailedMessage = message - }; + }); } - return new ValidationResult + return Task.FromResult(new ValidationResult { IsValid = true, ValidationFailedMessage = message - }; + }); } - return new ValidationResult + return Task.FromResult(new ValidationResult { IsValid = regex.IsMatch(input?.ToString() ?? "") || (AllowEmptyString && string.IsNullOrEmpty(input?.ToString())), ValidationFailedMessage = message - }; + }); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RequiredValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RequiredValidator.cs index fa5402f53..fa9813efe 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RequiredValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/OptionSettingItemValidators/RequiredValidator.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { /// @@ -11,11 +13,11 @@ public class RequiredValidator : IOptionSettingItemValidator private static readonly string defaultValidationFailedMessage = "Value can not be empty"; public string ValidationFailedMessage { get; set; } = defaultValidationFailedMessage; - public ValidationResult Validate(object input) => - new() + public Task Validate(object input, Recommendation recommendation) => + Task.FromResult(new() { IsValid = !string.IsNullOrEmpty(input?.ToString()), ValidationFailedMessage = ValidationFailedMessage - }; + }); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs index 1d9230c20..4d4847602 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidatorList.cs @@ -13,6 +13,11 @@ public enum RecipeValidatorList /// /// Must be paired with /// - MinMaxConstraint + MinMaxConstraint, + + /// + /// Must be paired with + /// + ValidDockerfilePath } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/DockerfilePathValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/DockerfilePathValidator.cs new file mode 100644 index 000000000..124338502 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/DockerfilePathValidator.cs @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using System.Threading.Tasks; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Utilities; + +namespace AWS.Deploy.Common.Recipes.Validation +{ + /// + /// This validates that the Dockerfile is within the build context. + /// + /// Per https://docs.docker.com/engine/reference/commandline/build/#text-files + /// "The path must be to a file within the build context." + /// + public class DockerfilePathValidator : IRecipeValidator + { + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + + public DockerfilePathValidator(IDirectoryManager directoryManager, IFileManager fileManager) + { + _directoryManager = directoryManager; + _fileManager = fileManager; + } + + public Task Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) + { + DockerUtilities.TryGetAbsoluteDockerfile(recommendation, _fileManager, _directoryManager, out var absoluteDockerfilePath); + + // Docker execution directory has its own typehint, which sets the value here + var dockerExecutionDirectory = recommendation.DeploymentBundle.DockerExecutionDirectory; + + // We're only checking the interaction here against a user-specified file and execution directory, + // it's still possible that we generate a dockerfile and/or compute the execution directory later. + if (absoluteDockerfilePath == string.Empty || dockerExecutionDirectory == string.Empty) + { + return ValidationResult.ValidAsync(); + } + + // Convert both to absolute paths in case they were specified relative to the project directory + var projectPath = recommendation.GetProjectDirectory(); + + var absoluteDockerExecutionDirectory = Path.IsPathRooted(dockerExecutionDirectory) + ? dockerExecutionDirectory + : _directoryManager.GetAbsolutePath(projectPath, dockerExecutionDirectory); + + if (!_directoryManager.ExistsInsideDirectory(absoluteDockerExecutionDirectory, absoluteDockerfilePath)) + { + return ValidationResult.FailedAsync($"The specified Dockerfile \"{absoluteDockerfilePath}\" is not located within " + + $"the specified Docker execution directory \"{dockerExecutionDirectory}\""); + } + + return ValidationResult.ValidAsync(); + } + } +} diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs index 097291d78..59f75028d 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/FargateTaskCpuMemorySizeValidator.cs @@ -1,8 +1,10 @@ // 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; namespace AWS.Deploy.Common.Recipes.Validation { @@ -58,7 +60,7 @@ private static IEnumerable BuildMemoryArray(int start, int end, int incr public string? InvalidCpuValueValidationFailedMessage { get; set; } /// - public ValidationResult Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) + public Task Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { string cpu; string memory; @@ -70,8 +72,8 @@ public ValidationResult Validate(Recommendation recommendation, IDeployToolValid } catch (OptionSettingItemDoesNotExistException) { - return ValidationResult.Failed("Could not find a valid value for Task CPU or Task Memory " + - "as part of of the ECS Fargate deployment configuration. Please provide a valid value and try again."); + return Task.FromResult(ValidationResult.Failed("Could not find a valid value for Task CPU or Task Memory " + + "as part of of the ECS Fargate deployment configuration. Please provide a valid value and try again.")); } if (!_cpuMemoryMap.ContainsKey(cpu)) @@ -81,14 +83,14 @@ public ValidationResult Validate(Recommendation recommendation, IDeployToolValid // or the UX flow calling in here doesn't enforce AllowedValues. var message = InvalidCpuValueValidationFailedMessage?.Replace("{{cpu}}", cpu); - return ValidationResult.Failed(message?? "Cpu validation failed"); + return Task.FromResult(ValidationResult.Failed(message?? "Cpu validation failed")); } var validMemoryValues = _cpuMemoryMap[cpu]; if (validMemoryValues.Contains(memory)) { - return ValidationResult.Valid(); + return Task.FromResult(ValidationResult.Valid()); } var failed = @@ -97,7 +99,7 @@ public ValidationResult Validate(Recommendation recommendation, IDeployToolValid .Replace("{{memory}}", memory) .Replace("{{memoryList}}", string.Join(", ", validMemoryValues)); - return ValidationResult.Failed(failed); + return Task.FromResult(ValidationResult.Failed(failed)); } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs index 53659f9ad..1b481d559 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/RecipeValidators/MinMaxConstraintValidator.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { /// @@ -21,7 +23,7 @@ public MinMaxConstraintValidator(IOptionSettingHandler optionSettingHandler) 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) + public Task Validate(Recommendation recommendation, IDeployToolValidationContext deployValidationContext) { double minVal; double maxValue; @@ -33,18 +35,18 @@ public ValidationResult Validate(Recommendation recommendation, IDeployToolValid } catch (OptionSettingItemDoesNotExistException) { - return ValidationResult.Failed($"Could not find a valid value for {MinValueOptionSettingsId} or {MaxValueOptionSettingsId}. Please provide a valid value and try again."); + return Task.FromResult(ValidationResult.Failed($"Could not find a valid value for {MinValueOptionSettingsId} or {MaxValueOptionSettingsId}. Please provide a valid value and try again.")); } if (minVal <= maxValue) - return ValidationResult.Valid(); + return Task.FromResult(ValidationResult.Valid()); var failureMessage = ValidationFailedMessage .Replace("{{MinValueOptionSettingsId}}", MinValueOptionSettingsId) .Replace("{{MaxValueOptionSettingsId}}", MaxValueOptionSettingsId); - return ValidationResult.Failed(failureMessage); + return Task.FromResult(ValidationResult.Failed(failureMessage)); } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidationResult.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidationResult.cs index 5850aad41..c546dcab5 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidationResult.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidationResult.cs @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Threading.Tasks; + namespace AWS.Deploy.Common.Recipes.Validation { public class ValidationResult @@ -17,6 +19,11 @@ public static ValidationResult Failed(string message) }; } + public static Task FailedAsync(string message) + { + return Task.FromResult(Failed(message)); + } + public static ValidationResult Valid() { return new ValidationResult @@ -24,5 +31,10 @@ public static ValidationResult Valid() IsValid = true }; } + + public static Task ValidAsync() + { + return Task.FromResult(Valid()); + } } } diff --git a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs index 8b645836e..2595d444e 100644 --- a/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs +++ b/src/AWS.Deploy.Common/Recipes/Validation/ValidatorFactory.cs @@ -20,8 +20,9 @@ public interface IValidatorFactory /// Builds the validators that apply to the given option /// /// Option to validate + /// Applies a filter to the list of validators /// Array of validators for the given option - IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem); + IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem, Func? filter = null); /// /// Builds the validators that apply to the given recipe @@ -51,17 +52,21 @@ public ValidatorFactory(IServiceProvider serviceProvider) { OptionSettingItemValidatorList.DirectoryExists, typeof(DirectoryExistsValidator) }, { OptionSettingItemValidatorList.DockerBuildArgs, typeof(DockerBuildArgsValidator) }, { OptionSettingItemValidatorList.DotnetPublishArgs, typeof(DotnetPublishArgsValidator) }, + { OptionSettingItemValidatorList.ExistingResource, typeof(ExistingResourceValidator) }, + { OptionSettingItemValidatorList.FileExists, typeof(FileExistsValidator) } }; private static readonly Dictionary _recipeValidatorTypeMapping = new() { { RecipeValidatorList.FargateTaskSizeCpuMemoryLimits, typeof(FargateTaskCpuMemorySizeValidator) }, - { RecipeValidatorList.MinMaxConstraint, typeof(MinMaxConstraintValidator) } + { RecipeValidatorList.MinMaxConstraint, typeof(MinMaxConstraintValidator) }, + { RecipeValidatorList.ValidDockerfilePath, typeof(DockerfilePathValidator) } }; - public IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem) + public IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSettingItem, Func? filter = null) { return optionSettingItem.Validators + .Where(validator => filter != null ? filter(validator) : true) .Select(v => Activate(v.ValidatorType, v.Configuration, _optionSettingItemValidatorTypeMapping)) .OfType() .ToArray(); @@ -70,7 +75,7 @@ public IOptionSettingItemValidator[] BuildValidators(OptionSettingItem optionSet public IRecipeValidator[] BuildValidators(RecipeDefinition recipeDefinition) { return recipeDefinition.Validators - .Select(v => Activate(v.ValidatorType, v.Configuration,_recipeValidatorTypeMapping)) + .Select(v => Activate(v.ValidatorType, v.Configuration, _recipeValidatorTypeMapping)) .OfType() .ToArray(); } diff --git a/src/AWS.Deploy.Common/Recommendation.cs b/src/AWS.Deploy.Common/Recommendation.cs index 76a3db6ac..5575f9e24 100644 --- a/src/AWS.Deploy.Common/Recommendation.cs +++ b/src/AWS.Deploy.Common/Recommendation.cs @@ -12,6 +12,9 @@ namespace AWS.Deploy.Common { public class Recommendation : IUserInputOption { + /// + /// Returns the full path to the project file + /// public string ProjectPath => ProjectDefinition.ProjectPath; public ProjectDefinition ProjectDefinition { get; } @@ -113,5 +116,19 @@ public void AddReplacementToken(string key, string value) { ReplacementTokens[key] = value; } + + /// + /// Helper to get the project's directory + /// + /// Full name of directory containing this recommendation's project file + public string GetProjectDirectory() + { + var projectDirectory = new FileInfo(ProjectPath).Directory?.FullName; + + if (string.IsNullOrEmpty(projectDirectory)) + throw new InvalidProjectPathException(DeployToolErrorCode.ProjectPathNotFound, "The project path provided is invalid."); + + return projectDirectory; + } } } diff --git a/src/AWS.Deploy.Common/TypeHintData/FilePathTypeHintData.cs b/src/AWS.Deploy.Common/TypeHintData/FilePathTypeHintData.cs new file mode 100644 index 000000000..1d87c673a --- /dev/null +++ b/src/AWS.Deploy.Common/TypeHintData/FilePathTypeHintData.cs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Common.TypeHintData +{ + /// + /// Additional typehint data for file options + /// + public class FilePathTypeHintData + { + /// + /// Corresponds to a Filter property for a System.Windows.Forms.FileDialog + /// to determine the choices that would appear in the dialog box if a wrapping tool prompted the user for a file path via a UI. + public string Filter { get; set; } = "All files (*.*)|*.*"; + + /// + /// Corresponds to the DefaultExt property for a System.Windows.Forms.FileDialog + /// to specify the default extension used if the user specifies a file name + /// without an extension. + /// + public string DefaultExtension { get; set; } = ""; + + /// + /// Corresponds to the Title property for a System.Windows.Forms.FileDialog + /// to specify the title of the file dialog box. + /// + public string Title { get; set; } = "Open"; + + /// + /// Corresponds to the CheckFileExists property for a System.Windows.Forms.FileDialog + /// to indicate whether the dialog box should display a warning if the user specifies a file that does not exist. + /// + public bool CheckFileExists { get; set; } = true; + + /// + /// Corresponds to the the AllowEmpty parameter for ConsoleUtilities.AskUserForValue + /// This lets a recipe option that uses the FilePathCommand typehint + /// control whether an empty value is allowed during CLI mode + /// + public bool AllowEmpty { get; set; } = true; + } +} diff --git a/src/AWS.Deploy.Common/Utilities/DockerUtilities.cs b/src/AWS.Deploy.Common/Utilities/DockerUtilities.cs new file mode 100644 index 000000000..6999c0705 --- /dev/null +++ b/src/AWS.Deploy.Common/Utilities/DockerUtilities.cs @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.IO; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; + +namespace AWS.Deploy.Common.Utilities +{ + /// + /// Utility methods for working with a recommendation's Docker configuration + /// + public static class DockerUtilities + { + /// + /// Gets the path of a Dockerfile if it exists at the default location: "{ProjectPath}/Dockerfile" + /// + /// The selected recommendation settings used for deployment + /// File manager, used for validating that the Dockerfile exists + /// Path to the Dockerfile, relative to the recommendation's project directory + /// True if the Dockerfile exists at the default location, false otherwise + public static bool TryGetDefaultDockerfile(Recommendation recommendation, IFileManager? fileManager, out string dockerfilePath) + { + if (fileManager == null) + { + fileManager = new FileManager(); + } + + if (fileManager.Exists(Constants.Docker.DefaultDockerfileName, recommendation.GetProjectDirectory())) + { + // Set the default value to the OS-specific ".\Dockerfile" + dockerfilePath = Path.Combine(".", Constants.Docker.DefaultDockerfileName); + return true; + } + else + { + dockerfilePath = string.Empty; + return false; + } + } + + /// + /// Gets the path of a the project's Dockerfile if it exists, from either a user-specified or the default location + /// + /// The selected recommendation settings used for deployment + /// File manager, used for validating that the Dockerfile exists + /// Path to a Dockerfile,which may be absolute or relative + /// True if a Dockerfile is specified for this deployment, false otherwise + public static bool TryGetDockerfile(Recommendation recommendation, IFileManager fileManager, out string dockerfilePath) + { + dockerfilePath = recommendation.DeploymentBundle.DockerfilePath; + + if (!string.IsNullOrEmpty(dockerfilePath)) + { + // Double-check that it still exists in case it was move/deleted after being specified. + if (fileManager.Exists(dockerfilePath, recommendation.GetProjectDirectory())) + { + return true; + } + else + { + throw new InvalidFilePath(DeployToolErrorCode.InvalidFilePath, $"A dockerfile was specified at {dockerfilePath} but does not exist."); + } + } + else + { + // Check the default location again, for the case where a file was NOT specified + // in the option but we generated one in the default location right before calling docker build. + var defaultExists = TryGetDefaultDockerfile(recommendation, fileManager, out dockerfilePath); + return defaultExists; + } + } + + /// + /// Gets the path of a the project's Dockerfile if it exists, from either a user-specified or the default location + /// + /// The selected recommendation settings used for deployment + /// File manager, used for validating that the Dockerfile exists + /// Absolute path to the Dockerfile + /// True if a Dockerfile is specified for this deployment, false otherwise + public static bool TryGetAbsoluteDockerfile(Recommendation recommendation, IFileManager fileManager, IDirectoryManager directoryManager, out string absoluteDockerfilePath) + { + var dockerfileExists = TryGetDockerfile(recommendation, fileManager, out var dockerfilePath); + + if (dockerfileExists) + { + absoluteDockerfilePath = Path.IsPathRooted(dockerfilePath) + ? dockerfilePath + : directoryManager.GetAbsolutePath(recommendation.GetProjectDirectory(), dockerfilePath); + } + else + { + absoluteDockerfilePath = string.Empty; + } + + return dockerfileExists; + } + } +} diff --git a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems index 6c0b2a873..0b50194b9 100644 --- a/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems +++ b/src/AWS.Deploy.Constants/AWS.Deploy.Constants.projitems @@ -12,6 +12,7 @@ + diff --git a/src/AWS.Deploy.Constants/Docker.cs b/src/AWS.Deploy.Constants/Docker.cs new file mode 100644 index 000000000..a076512b8 --- /dev/null +++ b/src/AWS.Deploy.Constants/Docker.cs @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Constants +{ + internal class Docker + { + /// + /// Name of the default Dockerfile that the deployment tool attempts to detect in the project directory + /// + public static readonly string DefaultDockerfileName = "Dockerfile"; + + /// + /// Id for the Docker Execution Directory recipe option + /// + public const string DockerExecutionDirectoryOptionId = "DockerExecutionDirectory"; + + /// + /// Id for the Dockerfile Path recipe option + /// + public const string DockerfileOptionId = "DockerfilePath"; + + /// + /// Id for the Docker Build Args recipe option + /// + public const string DockerBuildArgsOptionId = "DockerBuildArgs"; + + /// + /// Id for the ECR Repository Name recipe option + /// + public const string ECRRepositoryNameOptionId = "ECRRepositoryName"; + + /// + /// Id for the Docker Image Tag recipe option + /// + public const string ImageTagOptionId = "ImageTag"; + } +} diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs index fb79bb90b..e0af22013 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -14,5 +14,21 @@ internal static class RecipeIdentifier public const string REPLACE_TOKEN_LATEST_DOTNET_BEANSTALK_PLATFORM_ARN = "{LatestDotnetBeanstalkPlatformArn}"; public const string REPLACE_TOKEN_ECR_REPOSITORY_NAME = "{DefaultECRRepositoryName}"; public const string REPLACE_TOKEN_ECR_IMAGE_TAG = "{DefaultECRImageTag}"; + public const string REPLACE_TOKEN_DOCKERFILE_PATH = "{DockerfilePath}"; + + /// + /// Id for the 'dotnet publish --configuration' recipe option + /// + public const string DotnetPublishConfigurationOptionId = "DotnetBuildConfiguration"; + + /// + /// Id for the additional args for 'dotnet publish' recipe option + /// + public const string DotnetPublishArgsOptionId = "DotnetPublishArgs"; + + /// + /// Id for the 'dotnet build --self-contained' recipe option + /// + public const string DotnetPublishSelfContainedBuildOptionId = "SelfContainedBuild"; } } diff --git a/src/AWS.Deploy.DockerEngine/DockerEngine.cs b/src/AWS.Deploy.DockerEngine/DockerEngine.cs index d425a13bb..ca863994f 100644 --- a/src/AWS.Deploy.DockerEngine/DockerEngine.cs +++ b/src/AWS.Deploy.DockerEngine/DockerEngine.cs @@ -7,6 +7,8 @@ using System.Linq; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Utilities; using Newtonsoft.Json; namespace AWS.Deploy.DockerEngine @@ -33,9 +35,10 @@ public class DockerEngine : IDockerEngine { private readonly ProjectDefinition _project; private readonly IFileManager _fileManager; + private readonly IDirectoryManager _directoryManager; private readonly string _projectPath; - public DockerEngine(ProjectDefinition project, IFileManager fileManager) + public DockerEngine(ProjectDefinition project, IFileManager fileManager, IDirectoryManager directoryManager) { if (project == null) { @@ -45,6 +48,7 @@ public DockerEngine(ProjectDefinition project, IFileManager fileManager) _project = project; _projectPath = project.ProjectPath; _fileManager = fileManager; + _directoryManager = directoryManager; } /// @@ -158,8 +162,8 @@ public void DetermineDockerExecutionDirectory(Recommendation recommendation) if (string.IsNullOrEmpty(recommendation.DeploymentBundle.DockerExecutionDirectory)) { var projectFilename = Path.GetFileName(recommendation.ProjectPath); - var dockerFilePath = Path.Combine(Path.GetDirectoryName(recommendation.ProjectPath) ?? "", "Dockerfile"); - if (_fileManager.Exists(dockerFilePath)) + + if (DockerUtilities.TryGetAbsoluteDockerfile(recommendation, _fileManager, _directoryManager, out var dockerFilePath)) { using (var stream = File.OpenRead(dockerFilePath)) using (var reader = new StreamReader(stream)) diff --git a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj index 569bbeb42..df4d11cec 100644 --- a/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj +++ b/src/AWS.Deploy.Orchestration/AWS.Deploy.Orchestration.csproj @@ -8,6 +8,7 @@ + diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index 6993eba8e..179130cc8 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Amazon.CloudFormation; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; diff --git a/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs b/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs deleted file mode 100644 index 99321e78b..000000000 --- a/src/AWS.Deploy.Orchestration/CustomRecipeLocator.cs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using AWS.Deploy.Common.DeploymentManifest; -using AWS.Deploy.Common.IO; -using AWS.Deploy.Orchestration.Utilities; - -namespace AWS.Deploy.Orchestration -{ - public interface ICustomRecipeLocator - { - Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath); - } - - /// - /// This class supports the functionality to fetch custom recipe paths from a deployment-manifest file as well as - /// other locations that are monitored by the same source control root as the target application that needs to be deployed. - /// - public class CustomRecipeLocator : ICustomRecipeLocator - { - private readonly string _ignorePathSubstring = Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar; - private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; - private readonly IDeploymentManifestEngine _deploymentManifestEngine; - private readonly IDirectoryManager _directoryManager; - - public CustomRecipeLocator(IDeploymentManifestEngine deploymentManifestEngine, IOrchestratorInteractiveService orchestratorInteractiveService, IDirectoryManager directoryManager) - { - _orchestratorInteractiveService = orchestratorInteractiveService; - _deploymentManifestEngine = deploymentManifestEngine; - _directoryManager = directoryManager; - } - - /// - /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as - /// other locations that are monitored by the same source control root as the target application that needs to be deployed. - /// - /// The absolute path to the csproj or fsproj file of the target application - /// The absolute path of the directory which contains the solution file for the target application - /// A containing absolute paths of directories inside which the custom recipe snapshot is stored - public async Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) - { - var customRecipePaths = new HashSet(); - - foreach (var recipePath in await LocateRecipePathsFromManifestFile(targetApplicationFullPath)) - { - if (ContainsRecipeFile(recipePath)) - { - _orchestratorInteractiveService.LogInfoMessage($"Found custom recipe file at: {recipePath}"); - customRecipePaths.Add(recipePath); - } - } - - foreach (var recipePath in LocateAlternateRecipePaths(targetApplicationFullPath, solutionDirectoryPath)) - { - if (ContainsRecipeFile(recipePath)) - { - _orchestratorInteractiveService.LogInfoMessage($"Found custom recipe file at: {recipePath}"); - customRecipePaths.Add(recipePath); - } - } - - return customRecipePaths; - } - - /// - /// Fetches recipe definition paths by parsing the deployment-manifest file that is associated with the target application. - /// - /// The absolute path to the target application csproj or fsproj file - /// A list containing absolute paths to the saved CDK deployment projects - private async Task> LocateRecipePathsFromManifestFile(string targetApplicationFullPath) - { - try - { - return await _deploymentManifestEngine.GetRecipeDefinitionPaths(targetApplicationFullPath); - } - catch - { - _orchestratorInteractiveService.LogErrorMessage(Environment.NewLine); - _orchestratorInteractiveService.LogErrorMessage("Failed to load custom deployment recommendations " + - "from the deployment-manifest file due to an error while trying to deserialze the file."); - return await Task.FromResult(new List()); - } - } - - /// - /// Fetches custom recipe paths from other locations that are monitored by the same source control root as the target application that needs to be deployed. - /// If the target application is not under source control, then it scans the sub-directories of the solution folder for custom recipes. - /// If source control root directory is equal to the file system root, then it scans the sub-directories of the solution folder for custom recipes. - /// - /// The absolute path to the target application csproj or fsproj file - /// The absolute path of the directory which contains the solution file for the target application - /// A list of recipe definition paths. - private List LocateAlternateRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath ) - { - var targetApplicationDirectoryPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; - var fileSystemRootPath = _directoryManager.GetDirectoryInfo(targetApplicationDirectoryPath).Root.FullName; - var rootDirectoryPath = GetSourceControlRootDirectory(targetApplicationDirectoryPath); - - if (string.IsNullOrEmpty(rootDirectoryPath) || string.Equals(rootDirectoryPath, fileSystemRootPath)) - rootDirectoryPath = solutionDirectoryPath; - - return GetRecipePathsFromRootDirectory(rootDirectoryPath); - } - - /// - /// This method takes a root directory path and recursively searches all its sub-directories for custom recipe paths. - /// However, it ignores any recipe file located inside a "bin" folder. - /// - /// The absolute path of the root directory. - /// A list of recipe definition paths. - private List GetRecipePathsFromRootDirectory(string? rootDirectoryPath) - { - var recipePaths = new List(); - - if (!string.IsNullOrEmpty(rootDirectoryPath) && _directoryManager.Exists(rootDirectoryPath)) - { - var recipePathList = new List(); - try - { - recipePathList = _directoryManager.GetFiles(rootDirectoryPath, "*.recipe", SearchOption.AllDirectories).ToList(); - } - catch (Exception e) - { - _orchestratorInteractiveService.LogInfoMessage($"Failed to find custom recipe paths starting from {rootDirectoryPath}. Encountered the following exception: {e.GetType()}"); - } - - foreach (var recipeFilePath in recipePathList) - { - if (recipeFilePath.Contains(_ignorePathSubstring)) - continue; - recipePaths.Add(_directoryManager.GetDirectoryInfo(recipeFilePath).Parent.FullName); - } - } - return recipePaths; - } - - /// - /// Helper method to find the source control root directory of the current directory path. - /// If the current directory is not monitored by any source control system, then it returns string.Empty - /// - /// An absolute directory path. - /// First parent directory path that contains a ".git" folder or string.Empty if cannot find any - private string GetSourceControlRootDirectory(string? directoryPath) - { - var currentDir = directoryPath; - while(currentDir != null) - { - if(_directoryManager.GetDirectories(currentDir, ".git").Any()) - { - var sourceControlRootDirectory = _directoryManager.GetDirectoryInfo(currentDir).FullName; - _orchestratorInteractiveService.LogDebugMessage($"Source control root directory found at: {sourceControlRootDirectory}"); - return sourceControlRootDirectory; - } - - currentDir = _directoryManager.GetDirectoryInfo(currentDir).Parent?.FullName; - } - - _orchestratorInteractiveService.LogDebugMessage($"Could not find any source control root directory"); - return string.Empty; - } - - /// - /// This method determines if the given directory contains any recipe files - /// - /// The path of the directory that needs to be validated - /// A bool indicating the presence of a recipe file inside the directory. - private bool ContainsRecipeFile(string directoryPath) - { - var directoryName = _directoryManager.GetDirectoryInfo(directoryPath).Name; - var recipeFilePaths = _directoryManager.GetFiles(directoryPath, "*.recipe"); - if (!recipeFilePaths.Any()) - { - return false; - } - - return recipeFilePaths.All(filePath => Path.GetFileNameWithoutExtension(filePath).Equals(directoryName, StringComparison.Ordinal)); - } - } -} diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 7ee20d7f8..0031c152d 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Amazon; using Amazon.AppRunner.Model; +using Amazon.CloudControlApi; +using Amazon.CloudControlApi.Model; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; using Amazon.CloudFront; @@ -40,51 +42,10 @@ using Amazon.SQS; using Amazon.SQS.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.Orchestration.Data { - public interface IAWSResourceQueryer - { - Task> GetCloudFormationStackEvents(string stackName); - Task> ListOfAvailableInstanceTypes(); - Task DescribeAppRunnerService(string serviceArn); - Task> DescribeCloudFormationResources(string stackName); - Task DescribeElasticBeanstalkEnvironment(string environmentName); - Task DescribeElasticLoadBalancer(string loadBalancerArn); - Task> DescribeElasticLoadBalancerListeners(string loadBalancerArn); - Task DescribeCloudWatchRule(string ruleName); - Task GetS3BucketLocation(string bucketName); - Task GetS3BucketWebSiteConfiguration(string bucketName); - Task> ListOfECSClusters(); - Task> ListOfElasticBeanstalkApplications(); - Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null); - Task> ListElasticBeanstalkResourceTags(string resourceArn); - Task> ListOfEC2KeyPairs(); - Task CreateEC2KeyPair(string keyName, string saveLocation); - Task> ListOfIAMRoles(string? servicePrincipal); - Task> GetListOfVpcs(); - Task> GetElasticBeanstalkPlatformArns(); - Task GetLatestElasticBeanstalkPlatformArn(); - Task> GetECRAuthorizationToken(); - Task> GetECRRepositories(List? repositoryNames = null); - Task CreateECRRepository(string repositoryName); - Task> GetCloudFormationStacks(); - Task GetCloudFormationStack(string stackName); - Task GetCallerIdentity(string awsRegion); - Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType); - Task GetCloudFrontDistribution(string distributionId); - Task> ListOfDyanmoDBTables(); - Task> ListOfSQSQueuesUrls(); - Task> ListOfSNSTopicArns(); - Task> ListOfS3Buckets(); - Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName); - Task DescribeECRRepository(string respositoryName); - Task> DescribeAppRunnerVpcConnectors(); - Task> DescribeSubnets(string? vpcID = null); - Task> DescribeSecurityGroups(string? vpcID = null); - Task GetParameterStoreTextValue(string parameterName); - } - public class AWSResourceQueryer : IAWSResourceQueryer { private readonly IAWSClientFactory _awsClientFactory; @@ -94,6 +55,21 @@ public AWSResourceQueryer(IAWSClientFactory awsClientFactory) _awsClientFactory = awsClientFactory; } + public async Task GetCloudControlApiResource(string type, string identifier) + { + var cloudControlApiClient = _awsClientFactory.GetAWSClient(); + var request = new GetResourceRequest + { + TypeName = type, + Identifier = identifier + }; + + return await HandleException(async () => { + var resource = await cloudControlApiClient.GetResourceAsync(request); + return resource.ResourceDescription; + }); + } + /// /// List the available subnets /// If is specified, the list of subnets is filtered by the VPC. @@ -342,34 +318,46 @@ public async Task GetS3BucketLocation(string bucketName) return response.WebsiteConfiguration; } - public async Task> ListOfECSClusters() + public async Task> ListOfECSClusters(string? ecsClusterName = null) { var ecsClient = _awsClientFactory.GetAWSClient(); var clusters = await HandleException(async () => { - var clusterArns = await ecsClient.Paginators - .ListClusters(new ListClustersRequest()) - .ClusterArns - .ToListAsync(); + var request = new DescribeClustersRequest(); + if (string.IsNullOrEmpty(ecsClusterName)) + { + var clusterArns = await ecsClient.Paginators + .ListClusters(new ListClustersRequest()) + .ClusterArns + .ToListAsync(); - return await ecsClient.DescribeClustersAsync(new DescribeClustersRequest + request.Clusters = clusterArns; + } + else { - Clusters = clusterArns - }); + request.Clusters = new List { ecsClusterName }; + } + + + return await ecsClient.DescribeClustersAsync(request); }); return clusters.Clusters; } - public async Task> ListOfElasticBeanstalkApplications() + public async Task> ListOfElasticBeanstalkApplications(string? applicationName = null) { var beanstalkClient = _awsClientFactory.GetAWSClient(); - var applications = await HandleException(async () => await beanstalkClient.DescribeApplicationsAsync()); + var request = new DescribeApplicationsRequest(); + if (!string.IsNullOrEmpty(applicationName)) + request.ApplicationNames = new List { applicationName }; + + var applications = await HandleException(async () => await beanstalkClient.DescribeApplicationsAsync(request)); return applications.Applications; } - public async Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null) + public async Task> ListOfElasticBeanstalkEnvironments(string? applicationName = null, string? environmentName = null) { var beanstalkClient = _awsClientFactory.GetAWSClient(); @@ -378,6 +366,9 @@ public async Task> ListOfElasticBeanstalkEnvironmen ApplicationName = applicationName }; + if (!string.IsNullOrEmpty(environmentName)) + request.EnvironmentNames = new List { environmentName }; + return await HandleException(async () => { var environments = new List(); diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 827adaf44..c2f5e5b87 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -9,7 +9,10 @@ using System.Threading.Tasks; using Amazon.ECR.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Utilities; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; @@ -29,19 +32,22 @@ public class DeploymentBundleHandler : IDeploymentBundleHandler private readonly IOrchestratorInteractiveService _interactiveService; private readonly IDirectoryManager _directoryManager; private readonly IZipFileManager _zipFileManager; + private readonly IFileManager _fileManager; public DeploymentBundleHandler( ICommandLineWrapper commandLineWrapper, IAWSResourceQueryer awsResourceQueryer, IOrchestratorInteractiveService interactiveService, IDirectoryManager directoryManager, - IZipFileManager zipFileManager) + IZipFileManager zipFileManager, + IFileManager fileManager) { _commandLineWrapper = commandLineWrapper; _awsResourceQueryer = awsResourceQueryer; _interactiveService = interactiveService; _directoryManager = directoryManager; _zipFileManager = zipFileManager; + _fileManager = fileManager; } public async Task BuildDockerImage(CloudApplication cloudApplication, Recommendation recommendation, string imageTag) @@ -50,13 +56,14 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Recommenda _interactiveService.LogInfoMessage("Building the docker image..."); var dockerExecutionDirectory = GetDockerExecutionDirectory(recommendation); - var dockerFile = GetDockerFilePath(recommendation); var buildArgs = GetDockerBuildArgs(recommendation); + DockerUtilities.TryGetAbsoluteDockerfile(recommendation, _fileManager, _directoryManager, out var dockerFile); var dockerBuildCommand = $"docker build -t {imageTag} -f \"{dockerFile}\"{buildArgs} ."; _interactiveService.LogInfoMessage($"Docker Execution Directory: {Path.GetFullPath(dockerExecutionDirectory)}"); _interactiveService.LogInfoMessage($"Docker Build Command: {dockerBuildCommand}"); + recommendation.DeploymentBundle.DockerfilePath = dockerFile; recommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; var result = await _commandLineWrapper.TryRunWithResult(dockerBuildCommand, dockerExecutionDirectory, streamOutputToInteractiveService: true); @@ -139,23 +146,23 @@ public async Task CreateDotnetPublishZip(Recommendation recommendation) /// /// Determines the appropriate docker execution directory for the project. - /// By default, the docker execution directory is at solution level. - /// If no solution is available, the dockerfile directory is used. + /// In order of precedence: + /// 1. DeploymentBundle.DockerExecutionDirectory, if already set + /// 2. The solution level if ProjectDefinition.ProjectSolutionPath is set + /// 3. The project directory /// /// private string GetDockerExecutionDirectory(Recommendation recommendation) { var dockerExecutionDirectory = recommendation.DeploymentBundle.DockerExecutionDirectory; - var dockerFileDirectory = new FileInfo(recommendation.ProjectPath).Directory?.FullName; - if (dockerFileDirectory == null) - throw new InvalidProjectPathException(DeployToolErrorCode.ProjectPathNotFound, "The project path is invalid."); + var projectDirectory = recommendation.GetProjectDirectory(); var projectSolutionPath = recommendation.ProjectDefinition.ProjectSolutionPath; if (string.IsNullOrEmpty(dockerExecutionDirectory)) { if (string.IsNullOrEmpty(projectSolutionPath)) { - dockerExecutionDirectory = new FileInfo(dockerFileDirectory).FullName; + dockerExecutionDirectory = new FileInfo(projectDirectory).FullName; } else { @@ -167,15 +174,6 @@ private string GetDockerExecutionDirectory(Recommendation recommendation) return dockerExecutionDirectory; } - private string GetDockerFilePath(Recommendation recommendation) - { - var dockerFileDirectory = new FileInfo(recommendation.ProjectPath).Directory?.FullName; - if (dockerFileDirectory == null) - throw new InvalidProjectPathException(DeployToolErrorCode.ProjectPathNotFound, "The project path is invalid."); - - return Path.Combine(dockerFileDirectory, "Dockerfile"); - } - private string GetDockerBuildArgs(Recommendation recommendation) { var buildArgs = recommendation.DeploymentBundle.DockerBuildArgs; diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/AppRunnerServiceResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/AppRunnerServiceResource.cs index 7d5561275..f11b84c5a 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/AppRunnerServiceResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/AppRunnerServiceResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/CloudFrontDistributionResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/CloudFrontDistributionResource.cs index dcc0b2d19..8db9390c3 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/CloudFrontDistributionResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/CloudFrontDistributionResource.cs @@ -8,6 +8,7 @@ using AWS.Deploy.Orchestration.Data; using Amazon.CloudFront.Model; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.Orchestration.DisplayedResources { diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/CloudWatchEventResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/CloudWatchEventResource.cs index 21afb75b2..bf69c04a9 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/CloudWatchEventResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/CloudWatchEventResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourceCommandFactory.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourceCommandFactory.cs index b275f763a..b190ebf9a 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourceCommandFactory.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourceCommandFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs index 32ee133eb..01cfefd5b 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/DisplayedResourcesHandler.cs @@ -7,6 +7,7 @@ using AWS.Deploy.Orchestration.Data; using System.Linq; using AWS.Deploy.Orchestration.DeploymentCommands; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.Orchestration.DisplayedResources { diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticBeanstalkEnvironmentResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticBeanstalkEnvironmentResource.cs index 726e3458d..1869e20fc 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticBeanstalkEnvironmentResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticBeanstalkEnvironmentResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticLoadBalancerResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticLoadBalancerResource.cs index 90fd18407..45ed00fd1 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticLoadBalancerResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/ElasticLoadBalancerResource.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Amazon.ElasticLoadBalancingV2; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/DisplayedResources/S3BucketResource.cs b/src/AWS.Deploy.Orchestration/DisplayedResources/S3BucketResource.cs index a7954f74f..165d40e43 100644 --- a/src/AWS.Deploy.Orchestration/DisplayedResources/S3BucketResource.cs +++ b/src/AWS.Deploy.Orchestration/DisplayedResources/S3BucketResource.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.Orchestration.DisplayedResources diff --git a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs index ff7ab4491..4b171895c 100644 --- a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs +++ b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.Recipes; @@ -20,6 +21,59 @@ public OptionSettingHandler(IValidatorFactory validatorFactory) _validatorFactory = validatorFactory; } + /// + /// This method runs all the option setting validators for the configurable settings. + /// In case of a first time deployment, all settings and validators are run. + /// In case of a redeployment, only the updatable settings are considered. + /// + public List RunOptionSettingValidators(Recommendation recommendation, IEnumerable? optionSettings = null) + { + if (optionSettings == null) + optionSettings = recommendation.GetConfigurableOptionSettingItems().Where(x => !recommendation.IsExistingCloudApplication || x.Updatable); + + List settingValidatorFailedResults = new List(); + foreach (var optionSetting in optionSettings) + { + if (!IsOptionSettingDisplayable(recommendation, optionSetting)) + { + optionSetting.Validation.ValidationStatus = ValidationStatus.Valid; + optionSetting.Validation.ValidationMessage = string.Empty; + optionSetting.Validation.InvalidValue = null; + continue; + } + + var optionSettingValue = GetOptionSettingValue(recommendation, optionSetting); + settingValidatorFailedResults.AddRange(_validatorFactory.BuildValidators(optionSetting) + .Select(async validator => await validator.Validate(optionSettingValue, recommendation)) + .Select(x => x.Result) + .Where(x => !x.IsValid) + .ToList()); + + // Only update the validation object if there is no InvalidValue set. + // In the case where a user tries to set an Invalid Value, this is the value that will be on the UI. + // We don't want to update that value in the background if it is triggered by a dependent setting validation + // since it won't be reflected on the UI. + if (optionSetting.Validation.InvalidValue == null) + { + if (settingValidatorFailedResults.Any()) + { + optionSetting.Validation.ValidationStatus = ValidationStatus.Invalid; + optionSetting.Validation.ValidationMessage = string.Join(Environment.NewLine, settingValidatorFailedResults.Select(x => x.ValidationFailedMessage)).Trim(); + optionSetting.Validation.InvalidValue = optionSettingValue; + } + else + { + optionSetting.Validation.ValidationStatus = ValidationStatus.Valid; + optionSetting.Validation.ValidationMessage = string.Empty; + } + } + + settingValidatorFailedResults.AddRange(RunOptionSettingValidators(recommendation, optionSetting.ChildOptionSettings)); + } + + return settingValidatorFailedResults; + } + /// /// Assigns a value to the OptionSettingItem. /// @@ -27,10 +81,17 @@ public OptionSettingHandler(IValidatorFactory validatorFactory) /// Thrown if one or more determine /// is not valid. /// - public void SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value) + public async Task SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value, bool skipValidation = false) { - optionSettingItem.SetValue(this, value, _validatorFactory.BuildValidators(optionSettingItem), recommendation); + IOptionSettingItemValidator[] validators = new IOptionSettingItemValidator[0]; + if (!skipValidation && IsOptionSettingDisplayable(recommendation, optionSettingItem)) + validators = _validatorFactory.BuildValidators(optionSettingItem); + + await optionSettingItem.SetValue(this, value, validators, recommendation, skipValidation); + if (!skipValidation) + RunOptionSettingValidators(recommendation, optionSettingItem.Dependents.Select(x => GetOptionSetting(recommendation, x))); + // If the optionSettingItem came from the selected recommendation's deployment bundle, // set the corresponding property on recommendation.DeploymentBundle SetDeploymentBundleProperty(recommendation, optionSettingItem, value); @@ -47,22 +108,25 @@ private void SetDeploymentBundleProperty(Recommendation recommendation, OptionSe { switch (optionSettingItem.Id) { - case "DockerExecutionDirectory": + case Constants.Docker.DockerExecutionDirectoryOptionId: recommendation.DeploymentBundle.DockerExecutionDirectory = value.ToString() ?? string.Empty; break; - case "DockerBuildArgs": + case Constants.Docker.DockerfileOptionId: + recommendation.DeploymentBundle.DockerfilePath = value.ToString() ?? string.Empty; + break; + case Constants.Docker.DockerBuildArgsOptionId: recommendation.DeploymentBundle.DockerBuildArgs = value.ToString() ?? string.Empty; break; - case "ECRRepositoryName": + case Constants.Docker.ECRRepositoryNameOptionId: recommendation.DeploymentBundle.ECRRepositoryName = value.ToString() ?? string.Empty; break; - case "DotnetBuildConfiguration": + case Constants.RecipeIdentifier.DotnetPublishConfigurationOptionId: recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = value.ToString() ?? string.Empty; break; - case "DotnetPublishArgs": + case Constants.RecipeIdentifier.DotnetPublishArgsOptionId: recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments = value.ToString() ?? string.Empty; break; - case "SelfContainedBuild": + case Constants.RecipeIdentifier.DotnetPublishSelfContainedBuildOptionId: recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = Convert.ToBoolean(value); break; default: @@ -108,6 +172,44 @@ public OptionSettingItem GetOptionSetting(Recommendation recommendation, string? return optionSetting!; } + /// + /// Interactively traverses given json path and returns target option setting. + /// Throws exception if there is no that matches /> + /// In case an option setting of type is encountered, + /// that can have the key value pair name as the leaf node with the option setting Id as the node before that. + /// + /// + /// Dot (.) separated key values string pointing to an option setting. + /// Read more + /// + /// Option setting at the json path. Throws if there doesn't exist an option setting. + public OptionSettingItem GetOptionSetting(RecipeDefinition recipe, string? jsonPath) + { + if (string.IsNullOrEmpty(jsonPath)) + throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"An option setting item with the specified fully qualified Id '{jsonPath}' cannot be found in the" + + $" '{recipe.Name}' recipe."); + + var ids = jsonPath.Split('.'); + OptionSettingItem? optionSetting = null; + + for (int i = 0; i < ids.Length; i++) + { + var optionSettings = optionSetting?.ChildOptionSettings ?? recipe.OptionSettings; + optionSetting = optionSettings.FirstOrDefault(os => os.Id.Equals(ids[i])); + if (optionSetting == null) + { + throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"An option setting item with the specified fully qualified Id '{jsonPath}' cannot be found in the" + + $" '{recipe.Name}' recipe."); + } + if (optionSetting.Type.Equals(OptionSettingValueType.KeyValue)) + { + return optionSetting; + } + } + + return optionSetting!; + } + /// /// Retrieves the value of the Option Setting Item in a given recommendation. /// diff --git a/src/AWS.Deploy.Orchestration/Orchestrator.cs b/src/AWS.Deploy.Orchestration/Orchestrator.cs index d4735950d..84d88fd91 100644 --- a/src/AWS.Deploy.Orchestration/Orchestrator.cs +++ b/src/AWS.Deploy.Orchestration/Orchestrator.cs @@ -7,9 +7,11 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Utilities; using AWS.Deploy.DockerEngine; using AWS.Deploy.Orchestration.CDK; using AWS.Deploy.Orchestration.Data; @@ -17,6 +19,7 @@ using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.ServiceHandlers; using AWS.Deploy.Orchestration.Utilities; +using AWS.Deploy.Recipes; namespace AWS.Deploy.Orchestration { @@ -35,10 +38,9 @@ public class Orchestrator internal readonly IDeploymentBundleHandler? _deploymentBundleHandler; internal readonly ILocalUserSettingsEngine? _localUserSettingsEngine; internal readonly IDockerEngine? _dockerEngine; - internal readonly IList? _recipeDefinitionPaths; + internal readonly IRecipeHandler? _recipeHandler; internal readonly IFileManager? _fileManager; internal readonly IDirectoryManager? _directoryManager; - internal readonly ICustomRecipeLocator? _customRecipeLocator; internal readonly OrchestratorSession? _session; internal readonly IAWSServiceHandler? _awsServiceHandler; private readonly IOptionSettingHandler? _optionSettingHandler; @@ -53,8 +55,7 @@ public Orchestrator( IDeploymentBundleHandler deploymentBundleHandler, ILocalUserSettingsEngine localUserSettingsEngine, IDockerEngine dockerEngine, - ICustomRecipeLocator customRecipeLocator, - IList recipeDefinitionPaths, + IRecipeHandler recipeHandler, IFileManager fileManager, IDirectoryManager directoryManager, IAWSServiceHandler awsServiceHandler, @@ -68,8 +69,7 @@ public Orchestrator( _awsResourceQueryer = awsResourceQueryer; _deploymentBundleHandler = deploymentBundleHandler; _dockerEngine = dockerEngine; - _customRecipeLocator = customRecipeLocator; - _recipeDefinitionPaths = recipeDefinitionPaths; + _recipeHandler = recipeHandler; _localUserSettingsEngine = localUserSettingsEngine; _fileManager = fileManager; _directoryManager = directoryManager; @@ -77,10 +77,10 @@ public Orchestrator( _optionSettingHandler = optionSettingHandler; } - public Orchestrator(OrchestratorSession session, IList recipeDefinitionPaths) + public Orchestrator(OrchestratorSession session, IRecipeHandler recipeHandler) { _session = session; - _recipeDefinitionPaths = recipeDefinitionPaths; + _recipeHandler = recipeHandler; } /// @@ -90,18 +90,15 @@ public Orchestrator(OrchestratorSession session, IList recipeDefinitionP /// public async Task> GenerateDeploymentRecommendations() { - if (_recipeDefinitionPaths == null) - throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); if (_session == null) throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); - - var targetApplicationFullPath = new DirectoryInfo(_session.ProjectDefinition.ProjectPath).FullName; - var solutionDirectoryPath = !string.IsNullOrEmpty(_session.ProjectDefinition.ProjectSolutionPath) ? - new DirectoryInfo(_session.ProjectDefinition.ProjectSolutionPath).Parent.FullName : string.Empty; - - var customRecipePaths = await LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath); - var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths.Union(customRecipePaths), _session); - return await engine.ComputeRecommendations(); + if (_recipeHandler == null) + throw new InvalidOperationException($"{nameof(_recipeHandler)} is null as part of the orchestartor object"); + + var engine = new RecommendationEngine.RecommendationEngine(_session, _recipeHandler); + var recipePaths = new HashSet { RecipeLocator.FindRecipeDefinitionsPath() }; + var customRecipePaths = await _recipeHandler.LocateCustomRecipePaths(_session.ProjectDefinition); + return await engine.ComputeRecommendations(recipeDefinitionPaths: recipePaths.Union(customRecipePaths).ToList()); } /// @@ -111,12 +108,12 @@ public async Task> GenerateDeploymentRecommendations() /// public async Task> GenerateRecommendationsToSaveDeploymentProject() { - if (_recipeDefinitionPaths == null) - throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); if (_session == null) throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); - - var engine = new RecommendationEngine.RecommendationEngine(_recipeDefinitionPaths, _session); + if (_recipeHandler == null) + throw new InvalidOperationException($"{nameof(_recipeHandler)} is null as part of the orchestartor object"); + + var engine = new RecommendationEngine.RecommendationEngine(_session, _recipeHandler); var compatibleRecommendations = await engine.ComputeRecommendations(); var cdkRecommendations = compatibleRecommendations.Where(x => x.Recipe.DeploymentType == DeploymentTypes.CdkProject).ToList(); return cdkRecommendations; @@ -133,19 +130,21 @@ public async Task> GenerateRecommendationsFromSavedDeployme { if (_session == null) throw new InvalidOperationException($"{nameof(_session)} is null as part of the orchestartor object"); + if (_recipeHandler == null) + throw new InvalidOperationException($"{nameof(_recipeHandler)} is null as part of the orchestartor object"); if (_directoryManager == null) throw new InvalidOperationException($"{nameof(_directoryManager)} is null as part of the orchestartor object"); if (!_directoryManager.Exists(deploymentProjectPath)) throw new InvalidCliArgumentException(DeployToolErrorCode.DeploymentProjectPathNotFound, $"The path '{deploymentProjectPath}' does not exists on the file system. Please provide a valid deployment project path and try again."); - var engine = new RecommendationEngine.RecommendationEngine(new List { deploymentProjectPath }, _session); - return await engine.ComputeRecommendations(); + var engine = new RecommendationEngine.RecommendationEngine(_session, _recipeHandler); + return await engine.ComputeRecommendations(recipeDefinitionPaths: new List { deploymentProjectPath }); } /// /// Creates a deep copy of the recommendation object and applies the previous settings to that recommendation. /// - public Recommendation ApplyRecommendationPreviousSettings(Recommendation recommendation, IDictionary previousSettings) + public async Task ApplyRecommendationPreviousSettings(Recommendation recommendation, IDictionary previousSettings) { if (_optionSettingHandler == null) throw new InvalidOperationException($"{nameof(_optionSettingHandler)} is null as part of the orchestartor object"); @@ -157,7 +156,7 @@ public Recommendation ApplyRecommendationPreviousSettings(Recommendation recomme { if (previousSettings.TryGetValue(optionSetting.Id, out var value)) { - _optionSettingHandler.SetOptionSettingValue(recommendationCopy, optionSetting, value); + await _optionSettingHandler.SetOptionSettingValue(recommendationCopy, optionSetting, value, skipValidation: true); } } @@ -187,6 +186,13 @@ public async Task ApplyAllReplacementTokens(Recommendation recommendation, strin { recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_ECR_IMAGE_TAG, DateTime.UtcNow.Ticks.ToString()); } + if (recommendation.ReplacementTokens.ContainsKey(Constants.RecipeIdentifier.REPLACE_TOKEN_DOCKERFILE_PATH)) + { + if (_deploymentBundleHandler != null && DockerUtilities.TryGetDefaultDockerfile(recommendation, _fileManager, out var defaultDockerfilePath)) + { + recommendation.AddReplacementToken(Constants.RecipeIdentifier.REPLACE_TOKEN_DOCKERFILE_PATH, defaultDockerfilePath); + } + } } public async Task DeployRecommendation(CloudApplication cloudApplication, Recommendation recommendation) @@ -198,7 +204,7 @@ public async Task DeployRecommendation(CloudApplication cloudApplication, Recomm public async Task CreateDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) { if (_interactiveService == null) - throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); + throw new InvalidOperationException($"{nameof(_interactiveService)} is null as part of the orchestrator object"); if (recommendation.Recipe.DeploymentBundle == DeploymentBundleTypes.Container) { @@ -231,15 +237,17 @@ public async Task CreateDeploymentBundle(CloudApplication cloudApplication, Reco private async Task CreateContainerDeploymentBundle(CloudApplication cloudApplication, Recommendation recommendation) { if (_interactiveService == null) - throw new InvalidOperationException($"{nameof(_recipeDefinitionPaths)} is null as part of the orchestartor object"); + throw new InvalidOperationException($"{nameof(_interactiveService)} is null as part of the orchestartor object"); if (_dockerEngine == null) throw new InvalidOperationException($"{nameof(_dockerEngine)} is null as part of the orchestartor object"); if (_deploymentBundleHandler == null) - throw new InvalidOperationException($"{nameof(_deploymentBundleHandler)} is null as part of the orchestartor object"); + throw new InvalidOperationException($"{nameof(_deploymentBundleHandler)} is null as part of the orchestrator object"); if (_optionSettingHandler == null) - throw new InvalidOperationException($"{nameof(_optionSettingHandler)} is null as part of the orchestartor object"); + throw new InvalidOperationException($"{nameof(_optionSettingHandler)} is null as part of the orchestrator object"); + if (_fileManager == null) + throw new InvalidOperationException($"{nameof(_fileManager)} is null as part of the orchestrator object"); - if (!recommendation.ProjectDefinition.HasDockerFile) + if (!DockerUtilities.TryGetDockerfile(recommendation, _fileManager, out _)) { _interactiveService.LogInfoMessage("Generating Dockerfile..."); try @@ -257,12 +265,12 @@ private async Task CreateContainerDeploymentBundle(CloudApplication cloudApplica // 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")); + var respositoryName = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.Docker.ECRRepositoryNameOptionId)); string imageTag; try { - var tagSuffix = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "ImageTag")); + var tagSuffix = _optionSettingHandler.GetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.Docker.ImageTagOptionId)); imageTag = $"{respositoryName}:{tagSuffix}"; } catch (OptionSettingItemDoesNotExistException) @@ -304,18 +312,5 @@ public CloudApplicationResourceType GetCloudApplicationResourceType(DeploymentTy throw new FailedToFindCloudApplicationResourceType(DeployToolErrorCode.FailedToFindCloudApplicationResourceType, errorMessage); } } - - private async Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) - { - if (_customRecipeLocator == null) - throw new InvalidOperationException($"{nameof(_customRecipeLocator)} is null as part of the orchestartor object"); - - var customRecipePaths = new List(); - foreach (var customRecipePath in await _customRecipeLocator.LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath)) - { - customRecipePaths.Add(customRecipePath); - } - return customRecipePaths; - } } } diff --git a/src/AWS.Deploy.Orchestration/RecipeHandler.cs b/src/AWS.Deploy.Orchestration/RecipeHandler.cs index 1dd26821a..1fc16433f 100644 --- a/src/AWS.Deploy.Orchestration/RecipeHandler.cs +++ b/src/AWS.Deploy.Orchestration/RecipeHandler.cs @@ -7,42 +7,64 @@ using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Recipes; using Newtonsoft.Json; namespace AWS.Deploy.Orchestration { - public class RecipeHandler + public class RecipeHandler : IRecipeHandler { - public static async Task> GetRecipeDefinitions(ICustomRecipeLocator customRecipeLocator, ProjectDefinition? projectDefinition) + private readonly string _ignorePathSubstring = Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IOptionSettingHandler _optionSettingHandler; + + public RecipeHandler(IDeploymentManifestEngine deploymentManifestEngine, IOrchestratorInteractiveService orchestratorInteractiveService, IDirectoryManager directoryManager, IFileManager fileManager, IOptionSettingHandler optionSettingHandler) { - IEnumerable recipeDefinitionsPaths = new List { RecipeLocator.FindRecipeDefinitionsPath() }; - if(projectDefinition != null) - { - var targetApplicationFullPath = new DirectoryInfo(projectDefinition.ProjectPath).FullName; - var solutionDirectoryPath = !string.IsNullOrEmpty(projectDefinition.ProjectSolutionPath) ? - new DirectoryInfo(projectDefinition.ProjectSolutionPath).Parent.FullName : string.Empty; + _orchestratorInteractiveService = orchestratorInteractiveService; + _deploymentManifestEngine = deploymentManifestEngine; + _directoryManager = directoryManager; + _fileManager = fileManager; + _optionSettingHandler = optionSettingHandler; + } - var customPaths = await customRecipeLocator.LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath); - recipeDefinitionsPaths = recipeDefinitionsPaths.Union(customPaths); - } + public async Task> GetRecipeDefinitions(List? recipeDefinitionPaths = null) + { + recipeDefinitionPaths ??= new List { RecipeLocator.FindRecipeDefinitionsPath() }; var recipeDefinitions = new List(); + var uniqueRecipeId = new HashSet(); try { - foreach(var recipeDefinitionsPath in recipeDefinitionsPaths) + foreach(var recipeDefinitionsPath in recipeDefinitionPaths) { - foreach (var recipeDefinitionFile in Directory.GetFiles(recipeDefinitionsPath, "*.recipe", SearchOption.TopDirectoryOnly)) + foreach (var recipeDefinitionFile in _directoryManager.GetFiles(recipeDefinitionsPath, "*.recipe", SearchOption.TopDirectoryOnly)) { try { - var content = File.ReadAllText(recipeDefinitionFile); + var content = await _fileManager.ReadAllTextAsync(recipeDefinitionFile); var definition = JsonConvert.DeserializeObject(content); if (definition == null) throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeRecipe, $"Failed to Deserialize Recipe Definition [{recipeDefinitionFile}]"); - recipeDefinitions.Add(definition); + definition.RecipePath = recipeDefinitionFile; + if (!uniqueRecipeId.Contains(definition.Id)) + { + var dependencyTree = new Dictionary>(); + BuildDependencyTree(definition, definition.OptionSettings, dependencyTree); + foreach (var dependee in dependencyTree.Keys) + { + var optionSetting = _optionSettingHandler.GetOptionSetting(definition, dependee); + optionSetting.Dependents = dependencyTree[dependee]; + } + recipeDefinitions.Add(definition); + uniqueRecipeId.Add(definition.Id); + } } catch (Exception e) { @@ -58,5 +80,194 @@ public static async Task> GetRecipeDefinitions(ICustomRec return recipeDefinitions; } + + /// + /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + /// The of the application to be deployed. + /// A containing absolute paths of directories inside which the custom recipe snapshot is stored + public async Task> LocateCustomRecipePaths(ProjectDefinition projectDefinition) + { + var targetApplicationFullPath = new DirectoryInfo(projectDefinition.ProjectPath).FullName; + var solutionDirectoryPath = !string.IsNullOrEmpty(projectDefinition.ProjectSolutionPath) ? + new DirectoryInfo(projectDefinition.ProjectSolutionPath).Parent.FullName : string.Empty; + + return await LocateCustomRecipePaths(targetApplicationFullPath, solutionDirectoryPath); + } + + /// + /// Wrapper method to fetch custom recipe definition paths from a deployment-manifest file as well as + /// other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// + /// The absolute path to the csproj or fsproj file of the target application + /// The absolute path of the directory which contains the solution file for the target application + /// A containing absolute paths of directories inside which the custom recipe snapshot is stored + public async Task> LocateCustomRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) + { + var customRecipePaths = new HashSet(); + + foreach (var recipePath in await LocateRecipePathsFromManifestFile(targetApplicationFullPath)) + { + if (ContainsRecipeFile(recipePath)) + { + _orchestratorInteractiveService.LogInfoMessage($"Found custom recipe file at: {recipePath}"); + customRecipePaths.Add(recipePath); + } + } + + foreach (var recipePath in LocateAlternateRecipePaths(targetApplicationFullPath, solutionDirectoryPath)) + { + if (ContainsRecipeFile(recipePath)) + { + _orchestratorInteractiveService.LogInfoMessage($"Found custom recipe file at: {recipePath}"); + customRecipePaths.Add(recipePath); + } + } + + return customRecipePaths; + } + + /// + /// Fetches recipe definition paths by parsing the deployment-manifest file that is associated with the target application. + /// + /// The absolute path to the target application csproj or fsproj file + /// A list containing absolute paths to the saved CDK deployment projects + private async Task> LocateRecipePathsFromManifestFile(string targetApplicationFullPath) + { + try + { + return await _deploymentManifestEngine.GetRecipeDefinitionPaths(targetApplicationFullPath); + } + catch + { + _orchestratorInteractiveService.LogErrorMessage(Environment.NewLine); + _orchestratorInteractiveService.LogErrorMessage("Failed to load custom deployment recommendations " + + "from the deployment-manifest file due to an error while trying to deserialze the file."); + return await Task.FromResult(new List()); + } + } + + /// + /// Fetches custom recipe paths from other locations that are monitored by the same source control root as the target application that needs to be deployed. + /// If the target application is not under source control, then it scans the sub-directories of the solution folder for custom recipes. + /// If source control root directory is equal to the file system root, then it scans the sub-directories of the solution folder for custom recipes. + /// + /// The absolute path to the target application csproj or fsproj file + /// The absolute path of the directory which contains the solution file for the target application + /// A list of recipe definition paths. + private List LocateAlternateRecipePaths(string targetApplicationFullPath, string solutionDirectoryPath) + { + var targetApplicationDirectoryPath = _directoryManager.GetDirectoryInfo(targetApplicationFullPath).Parent.FullName; + var fileSystemRootPath = _directoryManager.GetDirectoryInfo(targetApplicationDirectoryPath).Root.FullName; + var rootDirectoryPath = GetSourceControlRootDirectory(targetApplicationDirectoryPath); + + if (string.IsNullOrEmpty(rootDirectoryPath) || string.Equals(rootDirectoryPath, fileSystemRootPath)) + rootDirectoryPath = solutionDirectoryPath; + + return GetRecipePathsFromRootDirectory(rootDirectoryPath); + } + + /// + /// This method takes a root directory path and recursively searches all its sub-directories for custom recipe paths. + /// However, it ignores any recipe file located inside a "bin" folder. + /// + /// The absolute path of the root directory. + /// A list of recipe definition paths. + private List GetRecipePathsFromRootDirectory(string? rootDirectoryPath) + { + var recipePaths = new List(); + + if (!string.IsNullOrEmpty(rootDirectoryPath) && _directoryManager.Exists(rootDirectoryPath)) + { + var recipePathList = new List(); + try + { + recipePathList = _directoryManager.GetFiles(rootDirectoryPath, "*.recipe", SearchOption.AllDirectories).ToList(); + } + catch (Exception e) + { + _orchestratorInteractiveService.LogInfoMessage($"Failed to find custom recipe paths starting from {rootDirectoryPath}. Encountered the following exception: {e.GetType()}"); + } + + foreach (var recipeFilePath in recipePathList) + { + if (recipeFilePath.Contains(_ignorePathSubstring)) + continue; + recipePaths.Add(_directoryManager.GetDirectoryInfo(recipeFilePath).Parent.FullName); + } + } + return recipePaths; + } + + /// + /// Helper method to find the source control root directory of the current directory path. + /// If the current directory is not monitored by any source control system, then it returns string.Empty + /// + /// An absolute directory path. + /// First parent directory path that contains a ".git" folder or string.Empty if cannot find any + private string GetSourceControlRootDirectory(string? directoryPath) + { + var currentDir = directoryPath; + while (currentDir != null) + { + if (_directoryManager.GetDirectories(currentDir, ".git").Any()) + { + var sourceControlRootDirectory = _directoryManager.GetDirectoryInfo(currentDir).FullName; + _orchestratorInteractiveService.LogDebugMessage($"Source control root directory found at: {sourceControlRootDirectory}"); + return sourceControlRootDirectory; + } + + currentDir = _directoryManager.GetDirectoryInfo(currentDir).Parent?.FullName; + } + + _orchestratorInteractiveService.LogDebugMessage($"Could not find any source control root directory"); + return string.Empty; + } + + /// + /// This method determines if the given directory contains any recipe files + /// + /// The path of the directory that needs to be validated + /// A bool indicating the presence of a recipe file inside the directory. + private bool ContainsRecipeFile(string directoryPath) + { + var directoryName = _directoryManager.GetDirectoryInfo(directoryPath).Name; + var recipeFilePaths = _directoryManager.GetFiles(directoryPath, "*.recipe"); + if (!recipeFilePaths.Any()) + { + return false; + } + + return recipeFilePaths.All(filePath => Path.GetFileNameWithoutExtension(filePath).Equals(directoryName, StringComparison.Ordinal)); + } + + /// Creates an option setting item dependency tree that indicates + /// which option setting items need to be validated if a value update occurs. + /// The function recursively goes through all the settings and their children to build this tree. + /// This method also creates a Fully Qualified Id which will help reference . + /// + private void BuildDependencyTree(RecipeDefinition recipe, List optionSettingItems, Dictionary> dependencyTree, string parentFullyQualifiedId = "") + { + foreach (var optionSettingItem in optionSettingItems) + { + optionSettingItem.FullyQualifiedId = string.IsNullOrEmpty(parentFullyQualifiedId) + ? optionSettingItem.Id + : $"{parentFullyQualifiedId}.{optionSettingItem.Id}"; + optionSettingItem.ParentId = parentFullyQualifiedId; + foreach (var dependency in optionSettingItem.DependsOn) + { + if (dependencyTree.ContainsKey(dependency.Id)) + { + dependencyTree[dependency.Id].Add(optionSettingItem.FullyQualifiedId); + } + else + { + dependencyTree[dependency.Id] = new List { optionSettingItem.FullyQualifiedId }; + } + } + BuildDependencyTree(recipe, optionSettingItem.ChildOptionSettings, dependencyTree, optionSettingItem.FullyQualifiedId); + } + } } } diff --git a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs index 96f360723..2e9e9567d 100644 --- a/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs +++ b/src/AWS.Deploy.Orchestration/RecommendationEngine/RecommendationEngine.cs @@ -15,49 +15,22 @@ namespace AWS.Deploy.Orchestration.RecommendationEngine { public class RecommendationEngine { - private readonly IList _availableRecommendations = new List(); private readonly OrchestratorSession _orchestratorSession; + private readonly IRecipeHandler _recipeHandler; - public RecommendationEngine(IEnumerable recipeDefinitionPaths, OrchestratorSession orchestratorSession) + public RecommendationEngine(OrchestratorSession orchestratorSession, IRecipeHandler recipeHandler) { _orchestratorSession = orchestratorSession; - - recipeDefinitionPaths ??= new List(); - - var uniqueRecipeId = new HashSet(); - - foreach (var recommendationPath in recipeDefinitionPaths) - { - foreach (var recipeFile in Directory.GetFiles(recommendationPath, "*.recipe", SearchOption.TopDirectoryOnly)) - { - try - { - var content = File.ReadAllText(recipeFile); - var definition = JsonConvert.DeserializeObject(content); - if (definition == null) - throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeRecipe, $"Failed to Deserialize Recipe [{recipeFile}]"); - definition.RecipePath = recipeFile; - if (!uniqueRecipeId.Contains(definition.Id)) - { - _availableRecommendations.Add(definition); - uniqueRecipeId.Add(definition.Id); - } - } - catch (Exception e) - { - throw new FailedToDeserializeException(DeployToolErrorCode.FailedToDeserializeRecipe, $"Failed to Deserialize Recipe [{recipeFile}]: {e.Message}", e); - } - } - } + _recipeHandler = recipeHandler; } - public async Task> ComputeRecommendations(Dictionary? additionalReplacements = null) + public async Task> ComputeRecommendations(List? recipeDefinitionPaths = null, Dictionary? additionalReplacements = null) { additionalReplacements ??= new Dictionary(); var recommendations = new List(); - - foreach (var potentialRecipe in _availableRecommendations) + var availableRecommendations = await _recipeHandler.GetRecipeDefinitions(recipeDefinitionPaths); + foreach (var potentialRecipe in availableRecommendations) { var results = await EvaluateRules(potentialRecipe.RecommendationRules); if(!results.Include) diff --git a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs index 49d0a418d..44717674e 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs @@ -9,6 +9,7 @@ using Amazon.ElasticBeanstalk; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Data; diff --git a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle index 816b68d4c..1e74cd16a 100644 --- a/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle +++ b/src/AWS.Deploy.Recipes/DeploymentBundleDefinitions/Container.deploymentbundle @@ -16,6 +16,26 @@ } ] }, + { + "Id": "DockerfilePath", + "Name": "Dockerfile Path", + "Description": "Specify a path to a Dockerfile as either an absolute path or a path relative to the project.", + "Type": "String", + "TypeHint": "FilePath", + "TypeHintData": { + "Filter": "All files (*.*)|*.*", + "CheckFileExists": true, + "Title": "Select a Dockerfile" + }, + "DefaultValue": "{DockerfilePath}", + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "FileExists" + } + ] + }, { "Id": "DockerExecutionDirectory", "Name": "Docker Execution Directory", diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 782380894..dc661ffb0 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -51,7 +51,6 @@ "Fail": { "Include": true } } } - ], "Categories": [ { @@ -85,6 +84,11 @@ "Order": 60 } ], + "Validators": [ + { + "ValidatorType": "ValidDockerfilePath" + } + ], "OptionSettings": [ { "Id": "ServiceName", @@ -172,7 +176,6 @@ "ValidatorType": "Regex", "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" } } @@ -224,7 +227,6 @@ "ValidatorType": "Regex", "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" } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index f2d7c8b6f..a183c75e9 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -72,6 +72,9 @@ "MinValueOptionSettingsId": "AutoScaling.MinCapacity", "MaxValueOptionSettingsId": "AutoScaling.MaxCapacity" } + }, + { + "ValidatorType": "ValidDockerfilePath" } ], "Categories": [ @@ -144,7 +147,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "arn:[^:]+:ecs:[^:]*:[0-9]{12}:cluster/.+", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster Arn. The ARN should contain the arn:[PARTITION]:ecs namespace, followed by the Region of the cluster, the AWS account ID of the cluster owner, the cluster namespace, and then the cluster name. For example, arn:aws:ecs:region:012345678910:cluster/test. For more information visit https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Cluster.html" } } @@ -169,9 +171,14 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "^([A-Za-z0-9_-]{1,255})$", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ECS::Cluster" + } } ], "DependsOn": [ @@ -261,7 +268,6 @@ "ValidatorType": "Regex", "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" } } @@ -323,7 +329,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "^vpc-([0-9a-f]{8}|[0-9a-f]{17})$", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid VPC ID. The VPC ID must start with the \"vpc-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example vpc-abc88de9 is a valid VPC ID." } } @@ -477,7 +482,6 @@ "ValidatorType": "Regex", "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" } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe index 36011703a..7d0dd60e2 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalk.recipe @@ -125,12 +125,13 @@ "Order": 100 } ], + "OptionSettings": [ { "Id": "BeanstalkApplication", - "Name": "Application Name", + "Name": "Elastic Beanstalk Application", "Category": "General", - "Description": "The Elastic Beanstalk application name.", + "Description": "The Elastic Beanstalk application.", "Type": "Object", "TypeHint": "BeanstalkApplication", "AdvancedSetting": false, @@ -167,6 +168,12 @@ "AllowEmptyString": true, "ValidationFailedMessage": "Invalid Application Name. The Application name can contain up to 100 Unicode characters, not including forward slash (/)." } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ElasticBeanstalk::Application" + } } ] }, @@ -224,6 +231,12 @@ "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." } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ElasticBeanstalk::Environment" + } } ] } @@ -327,7 +340,6 @@ "ValidatorType": "Regex", "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" } } @@ -379,7 +391,6 @@ "ValidatorType": "Regex", "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" } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index 0e423efd3..611f1c9c5 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -93,6 +93,9 @@ "Validators": [ { "ValidatorType": "FargateTaskSizeCpuMemoryLimits" + }, + { + "ValidatorType": "ValidDockerfilePath" } ], "Categories": [ @@ -155,7 +158,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "arn:[^:]+:ecs:[^:]*:[0-9]{12}:cluster/.+", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster Arn. The ARN should contain the arn:[PARTITION]:ecs namespace, followed by the Region of the cluster, the AWS account ID of the cluster owner, the cluster namespace, and then the cluster name. For example, arn:aws:ecs:region:012345678910:cluster/test. For more information visit https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Cluster.html" } } @@ -180,7 +182,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "^([A-Za-z0-9_-]{1,255})$", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } } @@ -232,7 +233,6 @@ "ValidatorType": "Regex", "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" } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 39628aed6..fb97b0e24 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -135,6 +135,9 @@ "MinValueOptionSettingsId": "AutoScaling.MinCapacity", "MaxValueOptionSettingsId": "AutoScaling.MaxCapacity" } + }, + { + "ValidatorType": "ValidDockerfilePath" } ], "Categories": [ @@ -202,7 +205,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "arn:[^:]+:ecs:[^:]*:[0-9]{12}:cluster/.+", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster Arn. The ARN should contain the arn:[PARTITION]:ecs namespace, followed by the Region of the cluster, the AWS account ID of the cluster owner, the cluster namespace, and then the cluster name. For example, arn:aws:ecs:region:012345678910:cluster/test. For more information visit https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Cluster.html" } } @@ -227,7 +229,6 @@ "ValidatorType": "Regex", "Configuration": { "Regex": "^([A-Za-z0-9_-]{1,255})$", - "AllowEmptyString": true, "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." } } @@ -319,7 +320,6 @@ "ValidatorType": "Regex", "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" } } diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe index 30d24f5b1..ab102ea22 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe @@ -26,6 +26,11 @@ } } ], + "Validators": [ + { + "ValidatorType": "ValidDockerfilePath" + } + ], "Categories": [ { "Id": "General", 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 511fa068a..edd758783 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -352,7 +352,8 @@ "type": "string", "enum": [ "FargateTaskSizeCpuMemoryLimits", - "MinMaxConstraint" + "MinMaxConstraint", + "ValidDockerfilePath" ] } }, @@ -448,6 +449,7 @@ "properties": { "Id": { "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", "title": "Unique ID for the setting", "description": "The unqiue id for the setting. This value should never been change once the recipe is released because it will be stored in user config files.", "minLength": 1 @@ -605,7 +607,9 @@ "Required", "DirectoryExists", "DockerBuildArgs", - "DotnetPublishArgs" + "DotnetPublishArgs", + "ExistingResource", + "FileExists" ] } }, diff --git a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs index b532e1054..f33840a10 100644 --- a/src/AWS.Deploy.ServerMode.Client/RestAPI.cs +++ b/src/AWS.Deploy.ServerMode.Client/RestAPI.cs @@ -2006,51 +2006,96 @@ public partial class HealthStatusOutput [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] public partial class OptionSettingItemSummary { + /// The unique id of setting. This value will be persisted in other config files so its value should never change once a recipe is released. [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Id { get; set; } + /// The fully qualified id of the setting that includes the Id and all of the parent's Ids. + /// This helps easily reference the Option Setting without context of the parent setting. + [Newtonsoft.Json.JsonProperty("fullyQualifiedId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string FullyQualifiedId { get; set; } + + /// The display friendly name of the setting. [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Name { get; set; } + /// The category for the setting. This value must match an id field in the list of categories. [Newtonsoft.Json.JsonProperty("category", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Category { get; set; } + /// The description of what the setting is used for. [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Description { get; set; } + /// The value used for the recipe if it is set by the user. [Newtonsoft.Json.JsonProperty("value", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public object Value { get; set; } + /// The type of primitive value expected for this setting. + /// For example String, Int [Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Type { get; set; } + /// Hint the the UI what type of setting this is optionally add additional UI features to select a value. + /// For example a value of BeanstalkApplication tells the UI it can display the list of available Beanstalk applications for the user to pick from. [Newtonsoft.Json.JsonProperty("typeHint", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string TypeHint { get; set; } + /// Type hint additional data required to facilitate handling of the option setting. [Newtonsoft.Json.JsonProperty("typeHintData", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.IDictionary TypeHintData { get; set; } + /// UI can use this to reduce the amount of settings to show to the user when confirming the recommendation. This can make it so the user sees only the most important settings + /// they need to know about before deploying. [Newtonsoft.Json.JsonProperty("advanced", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool Advanced { get; set; } + /// Indicates whether the setting can be edited [Newtonsoft.Json.JsonProperty("readOnly", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool ReadOnly { get; set; } + /// Indicates whether the setting is visible/displayed on the UI [Newtonsoft.Json.JsonProperty("visible", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool Visible { get; set; } + /// Indicates whether the setting can be displayed as part of the settings summary of the previous deployment. [Newtonsoft.Json.JsonProperty("summaryDisplayable", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool SummaryDisplayable { get; set; } + /// The allowed values for the setting. [Newtonsoft.Json.JsonProperty("allowedValues", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.ICollection AllowedValues { get; set; } + /// The value mapping for allowed values. The key of the dictionary is what is sent to services + /// and the value is the display value shown to users. [Newtonsoft.Json.JsonProperty("valueMapping", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.IDictionary ValueMapping { get; set; } + /// Child option settings for AWS.Deploy.Common.Recipes.OptionSettingValueType.Object value types + /// AWS.Deploy.CLI.ServerMode.Models.OptionSettingItemSummary.ChildOptionSettings value depends on the values of AWS.Deploy.CLI.ServerMode.Models.OptionSettingItemSummary.ChildOptionSettings [Newtonsoft.Json.JsonProperty("childOptionSettings", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.ICollection ChildOptionSettings { get; set; } + /// The validation state of the setting that contains the validation status and message. + [Newtonsoft.Json.JsonProperty("validation", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public OptionSettingValidation Validation { get; set; } + + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] + public partial class OptionSettingValidation + { + [Newtonsoft.Json.JsonProperty("validationStatus", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public ValidationStatus ValidationStatus { get; set; } + + [Newtonsoft.Json.JsonProperty("validationMessage", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string ValidationMessage { get; set; } + + [Newtonsoft.Json.JsonProperty("invalidValue", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public object InvalidValue { get; set; } + } @@ -2223,6 +2268,17 @@ public partial class TypeHintResourceSummary public string DisplayName { get; set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.1.0 (Newtonsoft.Json v13.0.0.0)")] + public enum ValidationStatus + { + [System.Runtime.Serialization.EnumMember(Value = @"Valid")] + Valid = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Invalid")] + Invalid = 1, + } [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v13.0.0.0))")] diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs index ab885adb8..d1c736586 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestDirectoryManager.cs @@ -5,12 +5,14 @@ using System.Collections.Generic; using System.IO; using AWS.Deploy.Common.IO; +using System.Linq; namespace AWS.Deploy.CLI.Common.UnitTests.IO { public class TestDirectoryManager : IDirectoryManager { public readonly HashSet CreatedDirectories = new(); + public readonly Dictionary> AddedFiles = new(); public DirectoryInfo CreateDirectory(string path) { @@ -29,8 +31,7 @@ public string[] GetProjFiles(string path) => public void Delete(string path, bool recursive = false) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); - public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) => - throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + public bool ExistsInsideDirectory(string parentDirectoryPath, string childPath) => childPath.Contains(parentDirectoryPath + Path.DirectorySeparatorChar, StringComparison.InvariantCulture); public bool Exists(string path) { @@ -41,10 +42,10 @@ 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."); + CreatedDirectories.ToArray(); public string[] GetFiles(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => - throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + AddedFiles.ContainsKey(path) ? AddedFiles[path].ToArray() : new string[0]; public bool IsEmpty(string path) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs index 87fd484a1..26d80d0b5 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/IO/TestFileManager.cs @@ -40,6 +40,17 @@ public Task WriteAllTextAsync(string filePath, string contents, CancellationToke public FileStream OpenRead(string filePath) => throw new NotImplementedException(); public string GetExtension(string filePath) => throw new NotImplementedException(); public long GetSizeInBytes(string filePath) => throw new NotImplementedException(); + public bool Exists(string path, string directory) + { + if (Path.IsPathRooted(path)) + { + return Exists(path); + } + else + { + return Exists(Path.Combine(directory, path)); + } + } } public static class TestFileManagerExtensions 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 6d985fc7c..eaddda83f 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/AppRunnerOptionSettingItemValidationTests.cs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; +using AWS.Deploy.Orchestration.Data; using Moq; using Should; using Xunit; @@ -15,12 +18,17 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation public class AppRunnerOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public AppRunnerOptionSettingItemValidationTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Theory] @@ -30,12 +38,12 @@ public AppRunnerOptionSettingItemValidationTests() [InlineData("abc_@1323", false)] //invalid character "@" [InlineData("123*&$_abc_", false)] //invalid characters [InlineData("-abc123def45", false)] // does not start with a letter or a number - public void AppRunnerServiceNameValidationTests(string value, bool isValid) + public async Task AppRunnerServiceNameValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); // 4 to 40 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. optionSettingItem.Validators.Add(GetRegexValidatorConfig("^([A-Za-z0-9][A-Za-z0-9_-]{3,39})$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -45,11 +53,11 @@ public void AppRunnerServiceNameValidationTests(string value, bool isValid) [InlineData("arn:aws:iam::1234567890124354:role/S3Access", false)] //invalid account ID [InlineData("arn:aws-new:iam::123456789012:role/S3Access", false)] // invalid aws partition [InlineData("arn:aws:iam::123456789012:role", false)] // missing resorce path - public void RoleArnValidationTests(string value, bool isValid) + public async Task RoleArnValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:(aws|aws-us-gov|aws-cn|aws-iso|aws-iso-b):iam::[0-9]{12}:(role|role/service-role)/[\\w+=,.@\\-/]{1,1000}")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -59,19 +67,19 @@ public void RoleArnValidationTests(string value, bool isValid) [InlineData("arn:aws:kms:us-east-1:11112222:key/1234abcd-12ab-34cd-56ef-1234567890ab", false)] // invalid account ID [InlineData("arn:aws:kms:us-west-2:111122223333:key", false)] // missing resource path [InlineData("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab", false)] // invalid key-id structure - public void KmsKeyArnValidationTests(string value, bool isValid) + public async Task KmsKeyArnValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("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}")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } - private void Validate(OptionSettingItem optionSettingItem, T value, bool isValid) + private async Task Validate(OptionSettingItem optionSettingItem, T value, bool isValid) { ValidationFailedException exception = null; try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); + await _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); } catch (ValidationFailedException e) { diff --git a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs new file mode 100644 index 000000000..1bf28c7ff --- /dev/null +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/DockerfilePathValidationTests.cs @@ -0,0 +1,106 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using AWS.Deploy.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 Xunit; + +namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation +{ + /// + /// Tests for the recipe-level validation between the Dockerfile path and + /// docker execution recipe options, + /// + public class DockerfilePathValidationTests + { + private readonly IServiceProvider _serviceProvider; + private readonly RecipeDefinition _recipeDefinition; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + + public DockerfilePathValidationTests() + { + _serviceProvider = new Mock().Object; + _directoryManager = new TestDirectoryManager(); + _fileManager = new TestFileManager(); + + _recipeDefinition = new Mock( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()).Object; + } + + public static IEnumerable DockerfilePathTestData => new List() + { + // Dockerfile path | Docker execution directory | expected to be valid? + + // We generate a Dockerfile later if one isn't specified, so not invalid at this point + new object[] { "", Path.Combine("C:", "project"), true }, + + // We compute the execution directory later if one isn't specified, so not invalid at this point + new object[] { Path.Combine("C:", "project", "Dockerfile"), "", true }, + + // Dockerfile is in the execution directory, with absolute paths + new object[] { Path.Combine("C:", "project", "Dockerfile"), Path.Combine("C:", "project"), true }, + + // Dockerfile is in the execution directory, with relative paths + new object[] { Path.Combine(".", "Dockerfile"), Path.Combine("."), true }, + + // Dockerfile is further down in execution directory, with absolute paths + new object[] { Path.Combine("C:", "project", "child", "Dockerfile"), Path.Combine("C:", "project"), true }, + + // Dockerfile is further down in execution directory, with relative paths + new object[] { Path.Combine(".", "child", "Dockerfile"), Path.Combine("."), true }, + + // Dockerfile is outside of the execution directory, which is invalid + new object[] { Path.Combine("C:", "project", "Dockerfile"), Path.Combine("C:", "foo"), false } + }; + + /// + /// Tests for , which validates the relationship + /// between a Dockerfile path and the Docker execution directory + /// + [Theory] + [MemberData(nameof(DockerfilePathTestData))] + public async Task DockerfilePathValidationHelperAsync(string dockerfilePath, string dockerExecutionDirectory, bool expectedToBeValid) + { + var projectPath = Path.Combine("C:", "project", "test.csproj"); + var options = new List() + { + new OptionSettingItem("DockerfilePath", "", "", "") + }; + var projectDefintion = new ProjectDefinition(null, projectPath, "", ""); + var recommendation = new Recommendation(_recipeDefinition, projectDefintion, options, 100, new Dictionary()); + var validator = new DockerfilePathValidator(_directoryManager, _fileManager); + + recommendation.DeploymentBundle.DockerExecutionDirectory = dockerExecutionDirectory; + recommendation.DeploymentBundle.DockerfilePath = dockerfilePath; + + // "Write" to the TestFileManager so that "Exists" returns true + if (Path.IsPathRooted(dockerfilePath)) + await _fileManager.WriteAllTextAsync(dockerfilePath, ""); + else + await _fileManager.WriteAllTextAsync(Path.Combine(recommendation.GetProjectDirectory(), dockerfilePath), ""); + + var validationResult = await validator.Validate(recommendation, null); + + Assert.Equal(expectedToBeValid, validationResult.IsValid); + } + } +} 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 bb98901dd..6520ec178 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ECSFargateOptionSettingItemValidationTests.cs @@ -6,12 +6,16 @@ using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using Amazon.CloudControlApi.Model; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using Moq; using Should; using Xunit; +using ResourceNotFoundException = Amazon.CloudControlApi.Model.ResourceNotFoundException; +using Task = System.Threading.Tasks.Task; namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation { @@ -20,12 +24,17 @@ public class ECSFargateOptionSettingItemValidationTests private readonly IOptionSettingHandler _optionSettingHandler; private readonly IServiceProvider _serviceProvider; private readonly IDirectoryManager _directoryManager; + private readonly Mock _awsResourceQueryer; public ECSFargateOptionSettingItemValidationTests() { + _awsResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); var mockServiceProvider = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(_directoryManager); + mockServiceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); _serviceProvider = mockServiceProvider.Object; _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } @@ -37,11 +46,11 @@ public ECSFargateOptionSettingItemValidationTests() [InlineData("arn:aws:ecs:us-east-1:01234567891:cluster/test", false)] //invalid account ID [InlineData("arn:aws:ecs:us-east-1:012345678910:cluster", false)] //no cluster name [InlineData("arn:aws:ecs:us-east-1:012345678910:fluster/test", false)] //fluster instead of cluster - public void ClusterArnValidationTests(string value, bool isValid) + public async Task ClusterArnValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:[^:]+:ecs:[^:]*:[0-9]{12}:cluster/.+")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -50,12 +59,12 @@ public void ClusterArnValidationTests(string value, bool isValid) [InlineData("abc12-34-56-XZ", true)] [InlineData("abc_@1323", false)] //invalid characters [InlineData("123*&$abc", false)] //invalid characters - public void NewClusterNameValidationTests(string value, bool isValid) + public async Task NewClusterNameValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); //up to 255 letters(uppercase and lowercase), numbers, underscores, and hyphens are allowed. optionSettingItem.Validators.Add(GetRegexValidatorConfig("^([A-Za-z0-9-]{1,255})$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -64,12 +73,12 @@ public void NewClusterNameValidationTests(string value, bool isValid) [InlineData("abc12-34-56_XZ", true)] [InlineData("abc_@1323", false)] //invalid character "@" [InlineData("123*&$_abc_", false)] //invalid characters - public void ECSServiceNameValidationTests(string value, bool isValid) + public async Task ECSServiceNameValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); // Up to 255 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. optionSettingItem.Validators.Add(GetRegexValidatorConfig("^([A-Za-z0-9_-]{1,255})$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -78,11 +87,11 @@ public void ECSServiceNameValidationTests(string value, bool isValid) [InlineData(-1, false)] [InlineData(6000, false)] [InlineData(1000, true)] - public void DesiredCountValidationTests(int value, bool isValid) + public async Task DesiredCountValidationTests(int value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRangeValidatorConfig(1, 5000)); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -92,11 +101,11 @@ public void DesiredCountValidationTests(int value, bool isValid) [InlineData("arn:aws:iam::123456789012:role/S3Access", true)] [InlineData("arn:aws:IAM::123456789012:role/S3Access", false)] //invalid uppercase IAM [InlineData("arn:aws:iam::1234567890124354:role/S3Access", false)] //invalid account ID - public void RoleArnValidationTests(string value, bool isValid) + public async Task RoleArnValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:.+:iam::[0-9]{12}:.+")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -107,13 +116,13 @@ public void RoleArnValidationTests(string value, bool isValid) [InlineData("ipc-456678", false)] //invalid prefix [InlineData("vpc-zzzzzzzz", false)] //invalid character z [InlineData("vpc-ffffffffaaaabbbb12", false)] //suffix length greater than 17 - public void VpcIdValidationTests(string value, bool isValid) + public async Task VpcIdValidationTests(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); //must start with the \"vpc-\" prefix, //followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. optionSettingItem.Validators.Add(GetRegexValidatorConfig("^vpc-([0-9a-f]{8}|[0-9a-f]{17})$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -122,11 +131,11 @@ public void VpcIdValidationTests(string value, bool isValid) [InlineData("arn:aws:elasticloadbalancing:012345678910:elasticloadbalancing:loadbalancer/my-load-balancer", false)] //missing region [InlineData("arn:aws:elasticloadbalancing:012345678910:elasticloadbalancing:loadbalancer", false)] //missing resource path [InlineData("arn:aws:elasticloadbalancing:01234567891:elasticloadbalancing:loadbalancer", false)] //11 digit account ID - public void LoadBalancerArnValidationTest(string value, bool isValid) + public async Task LoadBalancerArnValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:[^:]+:elasticloadbalancing:[^:]*:[0-9]{12}:loadbalancer/.+")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -135,11 +144,11 @@ public void LoadBalancerArnValidationTest(string value, bool isValid) [InlineData("/Api/Path/&*$-/@", true)] [InlineData("Api/Path", false)] // does not start with '/' [InlineData("/Api/Path/", false)] // contains invalid character '<' and '>' - public void ListenerConditionPathPatternValidationTest(string value, bool isValid) + public async Task ListenerConditionPathPatternValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("^/[a-zA-Z0-9*?&_\\-.$/~\"'@:+]{0,127}$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -148,11 +157,30 @@ public void ListenerConditionPathPatternValidationTest(string value, bool isVali [InlineData("MyRepo", false)] // cannot contain uppercase letters [InlineData("myrepo123@", false)] // cannot contain @ [InlineData("myrepo123.a//b", false)] // cannot contain consecutive slashes. - public void ECRRepositoryNameValidationTest(string value, bool isValid) + public async Task ECRRepositoryNameValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); + } + + [Fact] + public async Task ECSClusterNameValidationTest_Valid() + { + _awsResourceQueryer.Setup(x => x.GetCloudControlApiResource(It.IsAny(), It.IsAny())).Throws(new ResourceQueryException(DeployToolErrorCode.ResourceQuery, "", new ResourceNotFoundException(""))); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ECS::Cluster")); + await Validate(optionSettingItem, "WebApp1", true); + } + + [Fact] + public async Task ECSClusterNameValidationTest_Invalid() + { + var resource = new ResourceDescription { Identifier = "WebApp1" }; + _awsResourceQueryer.Setup(x => x.GetCloudControlApiResource(It.IsAny(), It.IsAny())).ReturnsAsync(resource); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ECS::Cluster")); + await Validate(optionSettingItem, "WebApp1", false); } [Theory] @@ -162,21 +190,21 @@ public void ECRRepositoryNameValidationTest(string value, bool isValid) [InlineData("--tag name:tag", false)] [InlineData("-f file", false)] [InlineData("--file file", false)] - public void DockerBuildArgsValidationTest(string value, bool isValid) + public async Task DockerBuildArgsValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig { ValidatorType = OptionSettingItemValidatorList.DockerBuildArgs }); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Fact] - public void DockerExecutionDirectory_AbsoluteExists() + public async Task DockerExecutionDirectory_AbsoluteExists() { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig { ValidatorType = OptionSettingItemValidatorList.DirectoryExists, @@ -184,19 +212,19 @@ public void DockerExecutionDirectory_AbsoluteExists() _directoryManager.CreateDirectory(Path.Join("C:", "project")); - Validate(optionSettingItem, Path.Join("C:", "project"), true); + await Validate(optionSettingItem, Path.Join("C:", "project"), true); } [Fact] - public void DockerExecutionDirectory_AbsoluteDoesNotExist() + public async Task DockerExecutionDirectory_AbsoluteDoesNotExist() { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig { ValidatorType = OptionSettingItemValidatorList.DirectoryExists, }); - Validate(optionSettingItem, Path.Join("C:", "other_project"), false); + await Validate(optionSettingItem, Path.Join("C:", "other_project"), false); } private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) @@ -212,6 +240,19 @@ private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) return regexValidatorConfig; } + private OptionSettingItemValidatorConfig GetExistingResourceValidatorConfig(string type) + { + var existingResourceValidatorConfig = new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.ExistingResource, + Configuration = new ExistingResourceValidator(_awsResourceQueryer.Object) + { + ResourceType = type + } + }; + return existingResourceValidatorConfig; + } + private OptionSettingItemValidatorConfig GetRangeValidatorConfig(int min, int max) { var rangeValidatorConfig = new OptionSettingItemValidatorConfig @@ -226,12 +267,12 @@ private OptionSettingItemValidatorConfig GetRangeValidatorConfig(int min, int ma return rangeValidatorConfig; } - private void Validate(OptionSettingItem optionSettingItem, T value, bool isValid) + private async Task Validate(OptionSettingItem optionSettingItem, T value, bool isValid) { ValidationFailedException exception = null; try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); + await _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 1113a90fe..ad7c0ace9 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ElasticBeanStalkOptionSettingItemValidationTests.cs @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; @@ -15,24 +19,29 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Recipes.Validation public class ElasticBeanStalkOptionSettingItemValidationTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public ElasticBeanStalkOptionSettingItemValidationTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Theory] [InlineData("12345sas", true)] [InlineData("435&*abc@3123", true)] [InlineData("abc/123/#", false)] // invalid character forward slash(/) - public void ApplicationNameValidationTest(string value, bool isValid) + public async Task ApplicationNameValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); //can contain up to 100 Unicode characters, not including forward slash (/). optionSettingItem.Validators.Add(GetRegexValidatorConfig("^[^/]{1,100}$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -40,13 +49,13 @@ public void ApplicationNameValidationTest(string value, bool isValid) [InlineData("abc-ABC-123-xyz", true)] [InlineData("abc", false)] // invalid length less than 4 characters. [InlineData("-12-abc", false)] // invalid character leading hyphen (-) - public void EnvironmentNameValidationTest(string value, bool isValid) + public async Task EnvironmentNameValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); // 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. optionSettingItem.Validators.Add(GetRegexValidatorConfig("^[a-zA-Z0-9][a-zA-Z0-9-]{2,38}[a-zA-Z0-9]$")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -56,11 +65,11 @@ public void EnvironmentNameValidationTest(string value, bool isValid) [InlineData("arn:aws:iam::123456789012:role/S3Access", true)] [InlineData("arn:aws:IAM::123456789012:role/S3Access", false)] //invalid uppercase IAM [InlineData("arn:aws:iam::1234567890124354:role/S3Access", false)] //invalid account ID - public void IAMRoleArnValidationTest(string value, bool isValid) + public async Task IAMRoleArnValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(GetRegexValidatorConfig("arn:.+:iam::[0-9]{12}:.+")); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } [Theory] @@ -68,12 +77,12 @@ public void IAMRoleArnValidationTest(string value, bool isValid) [InlineData("abc 1234 xyz", true)] [InlineData(" abc 123-xyz", false)] //leading space [InlineData(" 123 abc-456 ", false)] //leading and trailing space - public void EC2KeyPairValidationTest(string value, bool isValid) + public async Task EC2KeyPairValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); // It allows all ASCII characters but without leading and trailing spaces optionSettingItem.Validators.Add(GetRegexValidatorConfig("^(?! ).+(? x.ListOfElasticBeanstalkApplications(It.IsAny())).ReturnsAsync(new List { }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ElasticBeanstalk::Application")); + await Validate(optionSettingItem, "WebApp1", true); + } + + [Fact] + public async Task ExistingApplicationNameValidationTest_Invalid() + { + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkApplications(It.IsAny())).ReturnsAsync(new List { new ApplicationDescription { ApplicationName = "WebApp1" } }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ElasticBeanstalk::Application")); + await Validate(optionSettingItem, "WebApp1", false); + } + + [Fact] + public async Task ExistingEnvironmentNameValidationTest_Valid() + { + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())).ReturnsAsync(new List { }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ElasticBeanstalk::Environment")); + await Validate(optionSettingItem, "WebApp1", true); + } + + [Fact] + public async Task ExistingEnvironmentNameValidationTest_Invalid() + { + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())).ReturnsAsync(new List { new EnvironmentDescription { EnvironmentName = "WebApp1" } }); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); + optionSettingItem.Validators.Add(GetExistingResourceValidatorConfig("AWS::ElasticBeanstalk::Environment")); + await Validate(optionSettingItem, "WebApp1", false); + } + + private OptionSettingItemValidatorConfig GetExistingResourceValidatorConfig(string type) + { + var existingResourceValidatorConfig = new OptionSettingItemValidatorConfig + { + ValidatorType = OptionSettingItemValidatorList.ExistingResource, + Configuration = new ExistingResourceValidator(_awsResourceQueryer.Object) + { + ResourceType = type + } + }; + return existingResourceValidatorConfig; } [Theory] @@ -113,15 +171,15 @@ public void ElasticBeanstalkRollingUpdatesPauseTime(string value, bool isValid) [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) + public async Task DotnetPublishArgsValidationTest(string value, bool isValid) { - var optionSettingItem = new OptionSettingItem("id", "name", "description"); + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description"); optionSettingItem.Validators.Add(new OptionSettingItemValidatorConfig { ValidatorType = OptionSettingItemValidatorList.DotnetPublishArgs }); - Validate(optionSettingItem, value, isValid); + await Validate(optionSettingItem, value, isValid); } private OptionSettingItemValidatorConfig GetRegexValidatorConfig(string regex) @@ -151,13 +209,13 @@ private OptionSettingItemValidatorConfig GetRangeValidatorConfig(int min, int ma return rangeValidatorConfig; } - private void Validate(OptionSettingItem optionSettingItem, T value, bool isValid) + private async Task Validate(OptionSettingItem optionSettingItem, T value, bool isValid) { ValidationFailedException exception = null; try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, value); + await _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 54db114e8..d178266a5 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/OptionSettingsItemValidationTests.cs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Threading.Tasks; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; @@ -24,22 +26,27 @@ public class OptionSettingsItemValidationTests { private readonly ITestOutputHelper _output; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public OptionSettingsItemValidationTests(ITestOutputHelper output) { _output = output; - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Theory] [InlineData("")] [InlineData("-10")] [InlineData("100")] - public void InvalidInputInMultipleValidatorsThrowsException(string invalidValue) + public async Task InvalidInputInMultipleValidatorsThrowsException(string invalidValue) { - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Validators = new() { @@ -64,7 +71,7 @@ public void InvalidInputInMultipleValidatorsThrowsException(string invalidValue) // ACT try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); + await _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); } catch (ValidationFailedException e) { @@ -77,10 +84,10 @@ public void InvalidInputInMultipleValidatorsThrowsException(string invalidValue) } [Fact] - public void InvalidInputInSingleValidatorThrowsException() + public async Task InvalidInputInSingleValidatorThrowsException() { var invalidValue = "lowercase_only"; - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Validators = new() { @@ -100,7 +107,7 @@ public void InvalidInputInSingleValidatorThrowsException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); + await _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); } catch (ValidationFailedException e) { @@ -113,11 +120,11 @@ public void InvalidInputInSingleValidatorThrowsException() } [Fact] - public void ValidInputDoesNotThrowException() + public async Task ValidInputDoesNotThrowException() { var validValue = 8; - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Validators = new() { @@ -142,7 +149,7 @@ public void ValidInputDoesNotThrowException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, validValue); + await _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, validValue); } catch (ValidationFailedException e) { @@ -158,13 +165,13 @@ public void ValidInputDoesNotThrowException() /// helps tests several important concepts. /// [Fact] - public void CustomValidatorMessagePropagatesToValidationException() + public async Task CustomValidatorMessagePropagatesToValidationException() { // ARRANGE var customValidationMessage = "Custom Validation Message: Testing!"; var invalidValue = 100; - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Validators = new() { @@ -186,7 +193,7 @@ public void CustomValidatorMessagePropagatesToValidationException() // ACT try { - _optionSettingHandler.SetOptionSettingValue(null, optionSettingItem, invalidValue); + await _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 df8abbb47..9d3dc86c1 100644 --- a/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs +++ b/test/AWS.Deploy.CLI.Common.UnitTests/Recipes/Validation/ValidatorFactoryTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Amazon.Runtime.Internal; using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -27,16 +28,25 @@ public class ValidatorFactoryTests private readonly IOptionSettingHandler _optionSettingHandler; private readonly IServiceProvider _serviceProvider; private readonly IValidatorFactory _validatorFactory; + private readonly Mock _awsResourceQueryer; public ValidatorFactoryTests() { + _awsResourceQueryer = new Mock(); _optionSettingHandler = new Mock().Object; var mockServiceProvider = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IOptionSettingHandler))).Returns(_optionSettingHandler); mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(new TestDirectoryManager()); + mockServiceProvider.Setup(x => x.GetService(typeof(IFileManager))).Returns(new TestFileManager()); _serviceProvider = mockServiceProvider.Object; + mockServiceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); _validatorFactory = new ValidatorFactory(_serviceProvider); + mockServiceProvider + .Setup(x => x.GetService(typeof(IValidatorFactory))) + .Returns(_validatorFactory); } [Fact] @@ -45,7 +55,7 @@ public void HasABindingForAllOptionSettingItemValidators() // ARRANGE var allValidators = Enum.GetValues(typeof(OptionSettingItemValidatorList)); - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Validators = allValidators @@ -106,7 +116,7 @@ public void CanBuildRehydratedOptionSettingsItem() ValidationFailedMessage = "Custom Test Message" }; - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Name = "Test Item", Validators = new List @@ -146,7 +156,7 @@ public void CanBuildRehydratedOptionSettingsItem() public void WhenValidatorTypeAndConfigurationHaveAMismatchThenValidatorTypeWins() { // ARRANGE - var optionSettingItem = new OptionSettingItem("id", "name", "description") + var optionSettingItem = new OptionSettingItem("id", "fullyQualifiedId", "name", "description") { Name = "Test Item", Validators = new List diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs index bbd7ef588..cae778e87 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs @@ -13,6 +13,7 @@ using AWS.Deploy.CLI.IntegrationTests.Helpers; using AWS.Deploy.CLI.IntegrationTests.Services; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.Utilities; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs index 0bcfbabfc..e97bd7e6b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs @@ -11,6 +11,7 @@ using Amazon.ElasticBeanstalk.Model; using Amazon.S3; using Amazon.S3.Transfer; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration.Data; diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs index 37301310f..fad782368 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Amazon.IdentityManagement; using Amazon.IdentityManagement.Model; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.IntegrationTests.Helpers diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs index 437bed3f0..c325d0cc5 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/CustomRecipeLocatorTests.cs @@ -12,6 +12,9 @@ using Should; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.IntegrationTests.Services; +using AWS.Deploy.Common.Recipes; +using Moq; +using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.CLI.IntegrationTests.SaveCdkDeploymentProject { @@ -32,7 +35,7 @@ public async Task LocateCustomRecipePathsWithManifestFile() var webAppWithDockerFilePath = Path.Combine(tempDirectoryPath, "testapps", "WebAppWithDockerFile"); var webAppWithDockerCsproj = Path.Combine(webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); var solutionDirectoryPath = tempDirectoryPath; - var customRecipeLocator = BuildCustomRecipeLocator(); + var recipeHandler = BuildRecipeHandler(); await _commandLineWrapper.Run("git init", tempDirectoryPath); // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot @@ -40,7 +43,7 @@ public async Task LocateCustomRecipePathsWithManifestFile() await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp2")); // ACT - Fetch custom recipes corresponding to the same target application that has a deployment-manifest file. - var customRecipePaths = await customRecipeLocator.LocateCustomRecipePaths(webAppWithDockerCsproj, solutionDirectoryPath); + var customRecipePaths = await recipeHandler.LocateCustomRecipePaths(webAppWithDockerCsproj, solutionDirectoryPath); // ASSERT File.Exists(Path.Combine(webAppWithDockerFilePath, "aws-deployments.json")).ShouldBeTrue(); @@ -58,7 +61,7 @@ public async Task LocateCustomRecipePathsWithoutManifestFile() var webAppWithDockerCsproj = Path.Combine(webAppWithDockerFilePath, "WebAppWithDockerFile.csproj"); var webAppNoDockerCsproj = Path.Combine(webAppNoDockerFilePath, "WebAppNoDockerFile.csproj"); var solutionDirectoryPath = tempDirectoryPath; - var customRecipeLocator = BuildCustomRecipeLocator(); + var recipeHandler = BuildRecipeHandler(); await _commandLineWrapper.Run("git init", tempDirectoryPath); // ARRANGE - Create 2 CDK deployment projects that contain the custom recipe snapshot @@ -66,7 +69,7 @@ public async Task LocateCustomRecipePathsWithoutManifestFile() await Utilities.CreateCDKDeploymentProject(webAppWithDockerFilePath, Path.Combine(tempDirectoryPath, "MyCdkApp2")); // ACT - Fetch custom recipes corresponding to a different target application (under source control) without a deployment-manifest file. - var customRecipePaths = await customRecipeLocator.LocateCustomRecipePaths(webAppNoDockerCsproj, solutionDirectoryPath); + var customRecipePaths = await recipeHandler.LocateCustomRecipePaths(webAppNoDockerCsproj, solutionDirectoryPath); // ASSERT File.Exists(Path.Combine(webAppNoDockerFilePath, "aws-deployments.json")).ShouldBeFalse(); @@ -75,12 +78,15 @@ public async Task LocateCustomRecipePathsWithoutManifestFile() customRecipePaths.ShouldContain(Path.Combine(tempDirectoryPath, "MyCdkApp1")); } - private ICustomRecipeLocator BuildCustomRecipeLocator() + private IRecipeHandler BuildRecipeHandler() { var directoryManager = new DirectoryManager(); var fileManager = new FileManager(); var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); - return new CustomRecipeLocator(deploymentManifestEngine, _inMemoryInteractiveService, directoryManager); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + return new RecipeHandler(deploymentManifestEngine, _inMemoryInteractiveService, directoryManager, fileManager, optionSettingHandler); } protected virtual void Dispose(bool disposing) diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 9272450b5..0eab218db 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -229,12 +229,15 @@ public async Task GenerateRecommendationsFromIncompatibleDeploymentProject() private async Task GetOrchestrator(string targetApplicationProjectPath) { + var awsResourceQueryer = new TestToolAWSResourceQueryer(); var directoryManager = new DirectoryManager(); var fileManager = new FileManager(); var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); var localUserSettingsEngine = new LocalUserSettingsEngine(fileManager, directoryManager); - var customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, _inMemoryInteractiveService, directoryManager); - + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + var recipeHandler = new RecipeHandler(deploymentManifestEngine, _inMemoryInteractiveService, directoryManager, fileManager, optionSettingHandler); var projectDefinition = await new ProjectDefinitionParser(fileManager, directoryManager).Parse(targetApplicationProjectPath); var session = new OrchestratorSession(projectDefinition); @@ -243,12 +246,11 @@ private async Task GetOrchestrator(string targetApplicationProject new Mock().Object, new Mock().Object, new Mock().Object, - new TestToolAWSResourceQueryer(), + awsResourceQueryer, new Mock().Object, localUserSettingsEngine, new Mock().Object, - customRecipeLocator, - new List { RecipeLocator.FindRecipeDefinitionsPath() }, + recipeHandler, fileManager, directoryManager, new Mock().Object, diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs new file mode 100644 index 000000000..389bef013 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs @@ -0,0 +1,249 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime; +using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.Common.UnitTests.IO; +using AWS.Deploy.CLI.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Extensions; +using AWS.Deploy.CLI.IntegrationTests.Utilities; +using AWS.Deploy.ServerMode.Client; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.ServerMode +{ + public class DependencyValidationOptionSettings : IDisposable + { + private bool _isDisposed; + private string _stackName; + private readonly IServiceProvider _serviceProvider; + + private readonly string _awsRegion; + private readonly TestAppManager _testAppManager; + + public DependencyValidationOptionSettings() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + + _awsRegion = "us-west-2"; + + _testAppManager = new TestAppManager(); + } + + public Task ResolveCredentials() + { + var testCredentials = FallbackCredentialsFactory.GetCredentials(); + return Task.FromResult(testCredentials); + } + + [Fact] + public async Task DependentOptionSettingsGetInvalidated() + { + _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; + + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var portNumber = 4022; + 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 baseUrl = $"http://localhost:{portNumber}/"; + var restClient = new RestAPIClient(baseUrl, httpClient); + + await restClient.WaitTillServerModeReady(); + + var sessionId = await restClient.StartDeploymentSession(projectPath, _awsRegion); + + var logOutput = new StringBuilder(); + await ServerModeExtensions.SetupSignalRConnection(baseUrl, sessionId, logOutput); + + var beanstalkRecommendation = await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppElasticBeanstalkLinux", _stackName); + + var applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Equal(ValidationStatus.Valid, applicationIAMRole.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(applicationIAMRole.Validation.ValidationMessage)); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + var validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Equal(ValidationStatus.Valid, validCreateNew.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(validCreateNew.Validation.ValidationMessage)); + Assert.Null(validCreateNew.Validation.InvalidValue); + var validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Equal(ValidationStatus.Valid, validRoleArn.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(validRoleArn.Validation.ValidationMessage)); + Assert.Null(validRoleArn.Validation.InvalidValue); + + var applyConfigResponse = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"ApplicationIAMRole.CreateNew", "false"} + } + }); + + applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Equal(ValidationStatus.Valid, applicationIAMRole.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(applicationIAMRole.Validation.ValidationMessage)); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Equal(ValidationStatus.Valid, validCreateNew.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(validCreateNew.Validation.ValidationMessage)); + Assert.Null(validCreateNew.Validation.InvalidValue); + validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Equal(ValidationStatus.Invalid, validRoleArn.Validation.ValidationStatus); + Assert.Equal("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", + validRoleArn.Validation.ValidationMessage); + Assert.NotNull(validRoleArn.Validation.InvalidValue); + Assert.True(string.IsNullOrEmpty(validRoleArn.Validation.InvalidValue.ToString())); + + applyConfigResponse = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"ApplicationIAMRole.CreateNew", "true"} + } + }); + + applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Equal(ValidationStatus.Valid, applicationIAMRole.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(applicationIAMRole.Validation.ValidationMessage)); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Equal(ValidationStatus.Valid, validCreateNew.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(validCreateNew.Validation.ValidationMessage)); + Assert.Null(validCreateNew.Validation.InvalidValue); + validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Equal(ValidationStatus.Valid, validRoleArn.Validation.ValidationStatus); + Assert.True(string.IsNullOrEmpty(validRoleArn.Validation.ValidationMessage)); + Assert.Null(validRoleArn.Validation.InvalidValue); + } + finally + { + cancelSource.Cancel(); + _stackName = null; + } + } + + [Fact] + public async Task SettingInvalidValue() + { + _stackName = $"ServerModeWebAppRunner{Guid.NewGuid().ToString().Split('-').Last()}"; + + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var portNumber = 4022; + 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 baseUrl = $"http://localhost:{portNumber}/"; + var restClient = new RestAPIClient(baseUrl, httpClient); + + await restClient.WaitTillServerModeReady(); + + var sessionId = await restClient.StartDeploymentSession(projectPath, _awsRegion); + + var logOutput = new StringBuilder(); + await ServerModeExtensions.SetupSignalRConnection(baseUrl, sessionId, logOutput); + + var beanstalkRecommendation = await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppElasticBeanstalkLinux", _stackName); + + var applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + var validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Null(validCreateNew.Validation.InvalidValue); + var validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Null(validRoleArn.Validation.InvalidValue); + + var applyConfigResponse = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"ApplicationIAMRole.CreateNew", "false"}, + {"ApplicationIAMRole.RoleArn", "fakeArn"} + } + }); + + applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Null(validCreateNew.Validation.InvalidValue); + validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.NotNull(validRoleArn.Validation.InvalidValue); + Assert.Equal("fakeArn", validRoleArn.Validation.InvalidValue); + + applyConfigResponse = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"ApplicationIAMRole.CreateNew", "true"} + } + }); + + applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Null(validCreateNew.Validation.InvalidValue); + validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Null(validRoleArn.Validation.InvalidValue); + + applyConfigResponse = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"ApplicationIAMRole.RoleArn", "fakeArn"} + } + }); + + applicationIAMRole = (await restClient.GetConfigSettingsAsync(sessionId)).OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.Null(applicationIAMRole.Validation.InvalidValue); + validCreateNew = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.Null(validCreateNew.Validation.InvalidValue); + validRoleArn = applicationIAMRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.Null(validRoleArn.Validation.InvalidValue); + } + finally + { + cancelSource.Cancel(); + _stackName = null; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) return; + + _isDisposed = true; + } + + ~DependencyValidationOptionSettings() + { + Dispose(false); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index 8f049ac20..24cf5b159 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -9,22 +9,19 @@ using System.Threading; using System.Threading.Tasks; using Amazon.CloudFormation; -using Amazon.CloudFormation.Model; -using Amazon.Runtime; using AWS.Deploy.CLI.Commands; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.Extensions; using AWS.Deploy.CLI.IntegrationTests.Extensions; -using AWS.Deploy.CLI.IntegrationTests.Helpers; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.Common; -using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; using AWS.Deploy.Common.TypeHintData; using Microsoft.Extensions.DependencyInjection; using Moq; using Newtonsoft.Json; using Xunit; +using AWS.Deploy.CLI.IntegrationTests.Utilities; namespace AWS.Deploy.CLI.IntegrationTests.ServerMode { @@ -57,22 +54,6 @@ public GetApplyOptionSettings() _testAppManager = new TestAppManager(); } - public TemplateMetadataReader GetTemplateMetadataReader(string templateBody) - { - var templateMetadataReader = new TemplateMetadataReader(_mockAWSClientFactory.Object); - var cfResponse = new GetTemplateResponse(); - cfResponse.TemplateBody = templateBody; - _mockAWSClientFactory.Setup(x => x.GetAWSClient(It.IsAny())).Returns(_mockCFClient.Object); - _mockCFClient.Setup(x => x.GetTemplateAsync(It.IsAny(), It.IsAny())).ReturnsAsync(cfResponse); - return templateMetadataReader; - } - - public Task ResolveCredentials() - { - var testCredentials = FallbackCredentialsFactory.GetCredentials(); - return Task.FromResult(testCredentials); - } - [Fact] public async Task GetAndApplyAppRunnerSettings_VPCConnector() { @@ -80,7 +61,7 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4021; - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); var cancelSource = new CancellationTokenSource(); @@ -91,14 +72,14 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); - var sessionId = await StartDeploymentSession(restClient, projectPath); + var sessionId = await restClient.StartDeploymentSession(projectPath, _awsRegion); var logOutput = new StringBuilder(); - await SetupSignalRConnection(baseUrl, sessionId, logOutput); + await ServerModeExtensions.SetupSignalRConnection(baseUrl, sessionId, logOutput); - var appRunnerRecommendation = await GetRecommendationsAndSelectAppRunner(restClient, sessionId); + var appRunnerRecommendation = await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppAppRunner", _stackName); var vpcResources = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.VpcId"); var subnetsResourcesEmpty = await restClient.GetConfigSettingResourcesAsync(sessionId, "VPCConnector.Subnets"); @@ -135,7 +116,7 @@ public async Task GetAndApplyAppRunnerSettings_VPCConnector() var generateCloudFormationTemplateResponse = await restClient.GenerateCloudFormationTemplateAsync(sessionId); - var metadata = await GetAppSettingsFromCFTemplate(generateCloudFormationTemplateResponse.CloudFormationTemplate, _stackName); + var metadata = await ServerModeExtensions.GetAppSettingsFromCFTemplate(_mockAWSClientFactory, _mockCFClient, generateCloudFormationTemplateResponse.CloudFormationTemplate, _stackName); Assert.True(metadata.Settings.ContainsKey("VPCConnector")); var vpcConnector = JsonConvert.DeserializeObject(metadata.Settings["VPCConnector"].ToString()); @@ -158,7 +139,7 @@ public async Task GetAppRunnerConfigSettings_TypeHintData() var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4002; - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); var cancelSource = new CancellationTokenSource(); @@ -169,14 +150,14 @@ public async Task GetAppRunnerConfigSettings_TypeHintData() var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); - var sessionId = await StartDeploymentSession(restClient, projectPath); + var sessionId = await restClient.StartDeploymentSession(projectPath, _awsRegion); var logOutput = new StringBuilder(); - await SetupSignalRConnection(baseUrl, sessionId, logOutput); + await ServerModeExtensions.SetupSignalRConnection(baseUrl, sessionId, logOutput); - await GetRecommendationsAndSelectAppRunner(restClient, sessionId); + await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppAppRunner", _stackName); var configSettings = restClient.GetConfigSettingsAsync(sessionId); Assert.NotEmpty(configSettings.Result.OptionSettings); @@ -191,68 +172,6 @@ public async Task GetAppRunnerConfigSettings_TypeHintData() } } - private async Task WaitTillServerModeReady(RestAPIClient restApiClient) - { - await WaitUntilHelper.WaitUntil(async () => - { - SystemStatus status = SystemStatus.Error; - try - { - status = (await restApiClient.HealthAsync()).Status; - } - catch (Exception) - { - } - - return status == SystemStatus.Ready; - }, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10)); - } - - private async Task GetAppSettingsFromCFTemplate(string cloudFormationTemplate, string stackName) - { - var templateMetadataReader = GetTemplateMetadataReader(cloudFormationTemplate); - return await templateMetadataReader.LoadCloudApplicationMetadata(stackName); - } - - private async Task StartDeploymentSession(RestAPIClient restClient, string projectPath) - { - var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput - { - AwsRegion = _awsRegion, - ProjectPath = projectPath - }); - - var sessionId = startSessionOutput.SessionId; - Assert.NotNull(sessionId); - return sessionId; - } - - private static async Task SetupSignalRConnection(string baseUrl, string sessionId, StringBuilder logOutput) - { - var signalRClient = new DeploymentCommunicationClient(baseUrl); - await signalRClient.JoinSession(sessionId); - - AWS.Deploy.CLI.IntegrationTests.ServerModeTests.RegisterSignalRMessageCallbacks(signalRClient, logOutput); - } - - private async Task GetRecommendationsAndSelectAppRunner(RestAPIClient restClient, string sessionId) - { - var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); - Assert.NotEmpty(getRecommendationOutput.Recommendations); - - var appRunnerRecommendation = - getRecommendationOutput.Recommendations.FirstOrDefault(x => string.Equals(x.RecipeId, "AspNetAppAppRunner")); - Assert.NotNull(appRunnerRecommendation); - - await restClient.SetDeploymentTargetAsync(sessionId, - new SetDeploymentTargetInput - { - NewDeploymentName = _stackName, - NewDeploymentRecipeId = appRunnerRecommendation.RecipeId - }); - return appRunnerRecommendation; - } - public void Dispose() { Dispose(true); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs new file mode 100644 index 000000000..204ab3b49 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; +using Amazon.Runtime; +using AWS.Deploy.Common; +using AWS.Deploy.Orchestration.Utilities; +using AWS.Deploy.ServerMode.Client; +using Moq; +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.Utilities +{ + public static class ServerModeExtensions + { + public static async Task WaitTillServerModeReady(this RestAPIClient restApiClient) + { + await WaitUntilHelper.WaitUntil(async () => + { + SystemStatus status = SystemStatus.Error; + try + { + status = (await restApiClient.HealthAsync()).Status; + } + catch (Exception) + { + } + + return status == SystemStatus.Ready; + }, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10)); + } + + public static async Task StartDeploymentSession(this RestAPIClient restClient, string projectPath, string awsRegion) + { + var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput + { + AwsRegion = awsRegion, + ProjectPath = projectPath + }); + + var sessionId = startSessionOutput.SessionId; + Assert.NotNull(sessionId); + return sessionId; + } + + public static async Task SetupSignalRConnection(string baseUrl, string sessionId, StringBuilder logOutput) + { + var signalRClient = new DeploymentCommunicationClient(baseUrl); + await signalRClient.JoinSession(sessionId); + + ServerModeTests.RegisterSignalRMessageCallbacks(signalRClient, logOutput); + } + + public static async Task GetRecommendationsAndSetDeploymentTarget(this RestAPIClient restClient, string sessionId, string recipeId, string stackName) + { + var getRecommendationOutput = await restClient.GetRecommendationsAsync(sessionId); + Assert.NotEmpty(getRecommendationOutput.Recommendations); + + var beanstalkRecommendation = + getRecommendationOutput.Recommendations.FirstOrDefault(x => string.Equals(x.RecipeId, recipeId)); + Assert.NotNull(beanstalkRecommendation); + + await restClient.SetDeploymentTargetAsync(sessionId, + new SetDeploymentTargetInput + { + NewDeploymentName = stackName, + NewDeploymentRecipeId = beanstalkRecommendation.RecipeId + }); + return beanstalkRecommendation; + } + + public static async Task GetAppSettingsFromCFTemplate(Mock mockAWSClientFactory, Mock mockCFClient, string cloudFormationTemplate, string stackName) + { + var templateMetadataReader = GetTemplateMetadataReader(mockAWSClientFactory, mockCFClient, cloudFormationTemplate); + return await templateMetadataReader.LoadCloudApplicationMetadata(stackName); + } + + public static TemplateMetadataReader GetTemplateMetadataReader(Mock mockAWSClientFactory, Mock mockCFClient, string templateBody) + { + var templateMetadataReader = new TemplateMetadataReader(mockAWSClientFactory.Object); + var cfResponse = new GetTemplateResponse(); + cfResponse.TemplateBody = templateBody; + mockAWSClientFactory.Setup(x => x.GetAWSClient(It.IsAny())).Returns(mockCFClient.Object); + mockCFClient.Setup(x => x.GetTemplateAsync(It.IsAny(), It.IsAny())).ReturnsAsync(cfResponse); + return templateMetadataReader; + } + + public static Task ResolveCredentials() + { + var testCredentials = FallbackCredentialsFactory.GetCredentials(); + return Task.FromResult(testCredentials); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index abd4ba040..c536f8d4d 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Amazon.AppRunner.Model; +using Amazon.CloudControlApi.Model; using Amazon.CloudFormation.Model; using Amazon.CloudFront.Model; using Amazon.CloudWatchEvents.Model; @@ -17,6 +18,7 @@ using Amazon.IdentityManagement.Model; using Amazon.Runtime; using Amazon.SecurityToken.Model; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration.Data; namespace AWS.Deploy.CLI.IntegrationTests.Utilities @@ -44,9 +46,9 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task GetS3BucketLocation(string bucketName) => throw new NotImplementedException(); public Task GetS3BucketWebSiteConfiguration(string bucketName) => throw new NotImplementedException(); public Task> ListOfEC2KeyPairs() => throw new NotImplementedException(); - public Task> ListOfECSClusters() => throw new NotImplementedException(); - public Task> ListOfElasticBeanstalkApplications() => throw new NotImplementedException(); - public Task> ListOfElasticBeanstalkEnvironments(string applicationName) => throw new NotImplementedException(); + public Task> ListOfECSClusters(string ecsClusterName) => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkApplications(string applicationName) => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkEnvironments(string applicationName, string environmentName) => throw new NotImplementedException(); public Task> ListOfIAMRoles(string servicePrincipal) => throw new NotImplementedException(); public Task DescribeAppRunnerService(string serviceArn) => throw new NotImplementedException(); public Task> ListOfLoadBalancers(LoadBalancerTypeEnum loadBalancerType) => throw new NotImplementedException(); @@ -65,5 +67,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> DescribeSubnets(string vpcID = null) => throw new NotImplementedException(); public Task> DescribeSecurityGroups(string vpcID = null) => throw new NotImplementedException(); public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); + public Task GetCloudControlApiResource(string type, string identifier) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs index a92f47ffb..30d8f0bd5 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ApplyPreviousSettingsTests.cs @@ -19,28 +19,50 @@ using Assert = Should.Core.Assertions.Assert; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Common.Data; +using Amazon.CloudControlApi.Model; +using Amazon.ElasticBeanstalk.Model; +using AWS.Deploy.Common.DeploymentManifest; namespace AWS.Deploy.CLI.UnitTests { public class ApplyPreviousSettingsTests { + private readonly Mock _awsResourceQueryer; private readonly IOptionSettingHandler _optionSettingHandler; private readonly Orchestrator _orchestrator; - private readonly IServiceProvider _serviceProvider; - + private readonly Mock _serviceProvider; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public ApplyPreviousSettingsTests() { - _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); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _orchestrator = new Orchestrator(null, null, null, null, null, null, null, null, null, null, null, null, null, _optionSettingHandler); } private async Task BuildRecommendationEngine(string testProjectName) { var fullPath = SystemIOUtilities.ResolvePath(testProjectName); - var parser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); + var parser = new ProjectDefinitionParser(_fileManager, _directoryManager); var awsCredentials = new Mock(); var session = new OrchestratorSession( await parser.Parse(fullPath), @@ -51,7 +73,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + return new RecommendationEngine(session, _recipeHandler); } [Theory] @@ -77,7 +99,7 @@ public async Task ApplyApplicationIAMRolePreviousSettings(bool createNew, string var settings = JsonConvert.DeserializeObject>(serializedSettings); - beanstalkRecommendation = _orchestrator.ApplyRecommendationPreviousSettings(beanstalkRecommendation, settings); + beanstalkRecommendation = await _orchestrator.ApplyRecommendationPreviousSettings(beanstalkRecommendation, settings); var applicationIAMRoleOptionSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationIAMRole")); var typeHintResponse = _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting); @@ -111,7 +133,7 @@ public async Task ApplyVpcPreviousSettings(bool isDefault, bool createNew, strin var settings = JsonConvert.DeserializeObject>(serializedSettings); - fargateRecommendation = _orchestrator.ApplyRecommendationPreviousSettings(fargateRecommendation, settings); + fargateRecommendation = await _orchestrator.ApplyRecommendationPreviousSettings(fargateRecommendation, settings); var vpcOptionSetting = fargateRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("Vpc")); @@ -119,5 +141,87 @@ public async Task ApplyVpcPreviousSettings(bool isDefault, bool createNew, strin Assert.Equal(createNew, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, vpcOptionSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("CreateNew")))); Assert.Equal(vpcId, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, vpcOptionSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("VpcId")))); } + + [Fact] + public async Task ApplyECSClusterNamePreviousSettings() + { + _awsResourceQueryer.Setup(x => x.GetCloudControlApiResource(It.IsAny(), It.IsAny())).ReturnsAsync(new ResourceDescription { Identifier = "WebApp" }); + var engine = await BuildRecommendationEngine("WebAppWithDockerFile"); + + var recommendations = await engine.ComputeRecommendations(); + + var fargateRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_ASPNET_CORE_FARGATE_RECIPE_ID); + + var serializedSettings = @$" + {{ + ""ECSCluster"": {{ + ""CreateNew"": true, + ""NewClusterName"": ""WebApp"" + }} + }}"; + + var settings = JsonConvert.DeserializeObject>(serializedSettings); + + fargateRecommendation = await _orchestrator.ApplyRecommendationPreviousSettings(fargateRecommendation, settings); + + var ecsClusterSetting = fargateRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("ECSCluster")); + + Assert.Equal(true, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsClusterSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("CreateNew")))); + Assert.Equal("WebApp", _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsClusterSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("NewClusterName")))); + } + + [Fact] + public async Task ApplyBeanstalkApplicationNamePreviousSettings() + { + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkApplications(It.IsAny())).ReturnsAsync(new List { new ApplicationDescription { ApplicationName = "WebApp" } }); + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var serializedSettings = @$" + {{ + ""BeanstalkApplication"": {{ + ""CreateNew"": true, + ""ApplicationName"": ""WebApp"" + }} + }}"; + + var settings = JsonConvert.DeserializeObject>(serializedSettings); + + beanstalkRecommendation = await _orchestrator.ApplyRecommendationPreviousSettings(beanstalkRecommendation, settings); + + var applicationSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("BeanstalkApplication")); + + Assert.Equal(true, _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("CreateNew")))); + Assert.Equal("WebApp", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("ApplicationName")))); + } + + [Fact] + public async Task ApplyBeanstalkEnvironmentNamePreviousSettings() + { + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())).ReturnsAsync(new List { new EnvironmentDescription { EnvironmentName = "WebApp" } }); + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + + var recommendations = await engine.ComputeRecommendations(); + + var beanstalkRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var serializedSettings = @$" + {{ + ""BeanstalkEnvironment"": {{ + ""EnvironmentName"": ""WebApp"" + }} + }}"; + + var settings = JsonConvert.DeserializeObject>(serializedSettings); + + beanstalkRecommendation = await _orchestrator.ApplyRecommendationPreviousSettings(beanstalkRecommendation, settings); + + var environmentSetting = beanstalkRecommendation.Recipe.OptionSettings.First(optionSetting => optionSetting.Id.Equals("BeanstalkEnvironment")); + + Assert.Equal("WebApp", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentSetting.ChildOptionSettings.First(optionSetting => optionSetting.Id.Equals("EnvironmentName")))); + } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs index cff537b3a..61af3ce8e 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ConsoleUtilitiesTests.cs @@ -17,6 +17,7 @@ using Moq; using Should; using Xunit; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.UnitTests { @@ -24,13 +25,18 @@ public class ConsoleUtilitiesTests { private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public ConsoleUtilitiesTests() { - _serviceProvider = new Mock().Object; _directoryManager = new TestDirectoryManager(); - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } private readonly List _options = new List diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index 3d1570249..61a19a61f 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -10,8 +10,10 @@ using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; @@ -27,18 +29,33 @@ public class DeploymentBundleHandlerTests private readonly TestDirectoryManager _directoryManager; private readonly ProjectDefinitionParser _projectDefinitionParser; private readonly RecipeDefinition _recipeDefinition; + private readonly TestFileManager _fileManager; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IRecipeHandler _recipeHandler; public DeploymentBundleHandlerTests() { var awsResourceQueryer = new TestToolAWSResourceQueryer(); var interactiveService = new TestToolOrchestratorInteractiveService(); var zipFileManager = new TestZipFileManager(); + var serviceProvider = new Mock().Object; _commandLineWrapper = new TestToolCommandLineWrapper(); + _fileManager = new TestFileManager(); _directoryManager = new TestDirectoryManager(); + var recipeFiles = Directory.GetFiles(RecipeLocator.FindRecipeDefinitionsPath(), "*.recipe", SearchOption.TopDirectoryOnly); + _directoryManager.AddedFiles.Add(RecipeLocator.FindRecipeDefinitionsPath(), new HashSet (recipeFiles)); + foreach (var recipeFile in recipeFiles) + _fileManager.InMemoryStore.Add(recipeFile, File.ReadAllText(recipeFile)); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var validatorFactory = new ValidatorFactory(serviceProvider); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); _projectDefinitionParser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); - _deploymentBundleHandler = new DeploymentBundleHandler(_commandLineWrapper, awsResourceQueryer, interactiveService, _directoryManager, zipFileManager); + _deploymentBundleHandler = new DeploymentBundleHandler(_commandLineWrapper, awsResourceQueryer, interactiveService, _directoryManager, zipFileManager, new FileManager()); _recipeDefinition = new Mock( It.IsAny(), @@ -58,17 +75,20 @@ public async Task BuildDockerImage_DockerExecutionDirectoryNotSet() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - - var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); + var options = new List() + { + new OptionSettingItem("DockerfilePath", "", "", "") + }; + var recommendation = new Recommendation(_recipeDefinition, project, options, 100, new Dictionary()); var cloudApplication = new CloudApplication("ConsoleAppTask", String.Empty, CloudApplicationResourceType.CloudFormationStack, string.Empty); var imageTag = "imageTag"; await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation, imageTag); - var dockerFile = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(recommendation.ProjectPath)), "Dockerfile"); + var expectedDockerFile = Path.GetFullPath(Path.Combine(".", "Dockerfile"), recommendation.GetProjectDirectory()); var dockerExecutionDirectory = Directory.GetParent(Path.GetFullPath(recommendation.ProjectPath)).Parent.Parent; - Assert.Equal($"docker build -t {imageTag} -f \"{dockerFile}\" .", + Assert.Equal($"docker build -t {imageTag} -f \"{expectedDockerFile}\" .", _commandLineWrapper.CommandsToExecute.First().Command); Assert.Equal(dockerExecutionDirectory.FullName, _commandLineWrapper.CommandsToExecute.First().WorkingDirectory); @@ -79,7 +99,11 @@ public async Task BuildDockerImage_DockerExecutionDirectorySet() { var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); var project = await _projectDefinitionParser.Parse(projectPath); - var recommendation = new Recommendation(_recipeDefinition, project, new List(), 100, new Dictionary()); + var options = new List() + { + new OptionSettingItem("DockerfilePath", "", "", "") + }; + var recommendation = new Recommendation(_recipeDefinition, project, options, 100, new Dictionary()); recommendation.DeploymentBundle.DockerExecutionDirectory = projectPath; @@ -87,14 +111,43 @@ public async Task BuildDockerImage_DockerExecutionDirectorySet() var imageTag = "imageTag"; await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation, imageTag); - var dockerFile = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(recommendation.ProjectPath)), "Dockerfile"); + var expectedDockerFile = Path.GetFullPath(Path.Combine(".", "Dockerfile"), recommendation.GetProjectDirectory()); - Assert.Equal($"docker build -t {imageTag} -f \"{dockerFile}\" .", + Assert.Equal($"docker build -t {imageTag} -f \"{expectedDockerFile}\" .", _commandLineWrapper.CommandsToExecute.First().Command); Assert.Equal(projectPath, _commandLineWrapper.CommandsToExecute.First().WorkingDirectory); } + /// + /// Tests the Dockerfile being located in a subfolder instead of the project root + /// + [Fact] + public async Task BuildDockerImage_AlternativeDockerfilePathSet() + { + var projectPath = SystemIOUtilities.ResolvePath("ConsoleAppTask"); + var project = await _projectDefinitionParser.Parse(projectPath); + var options = new List() + { + new OptionSettingItem("DockerfilePath", "", "", "") + }; + var recommendation = new Recommendation(_recipeDefinition, project, options, 100, new Dictionary()); + + var dockerfilePath = Path.Combine(projectPath, "Docker", "Dockerfile"); + var expectedDockerExecutionDirectory = Directory.GetParent(Path.GetFullPath(recommendation.ProjectPath)).Parent.Parent; + + recommendation.DeploymentBundle.DockerfilePath = dockerfilePath; + + var cloudApplication = new CloudApplication("ConsoleAppTask", string.Empty, CloudApplicationResourceType.CloudFormationStack, recommendation.Recipe.Id); + var imageTag = "imageTag"; + await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation, imageTag); + + Assert.Equal($"docker build -t {imageTag} -f \"{dockerfilePath}\" .", + _commandLineWrapper.CommandsToExecute.First().Command); + Assert.Equal(expectedDockerExecutionDirectory.FullName, + _commandLineWrapper.CommandsToExecute.First().WorkingDirectory); + } + [Fact] public async Task PushDockerImage_RepositoryNameCheck() { @@ -170,7 +223,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + return new RecommendationEngine(session, _recipeHandler); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/DockerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DockerTests.cs index 79534e6a8..8adac6d15 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DockerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DockerTests.cs @@ -5,9 +5,13 @@ using System.IO; using System.Reflection; using System.Threading.Tasks; +using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; using AWS.Deploy.DockerEngine; +using AWS.Deploy.Orchestration; +using Moq; using Should; using Xunit; @@ -32,7 +36,7 @@ public async Task DockerGenerate(string topLevelFolder, string projectName) var project = await new ProjectDefinitionParser(fileManager, new DirectoryManager()).Parse(projectPath); - var engine = new DockerEngine.DockerEngine(project, fileManager); + var engine = new DockerEngine.DockerEngine(project, fileManager, new TestDirectoryManager()); engine.GenerateDockerFile(); diff --git a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs index 8aadd5fab..86356b49b 100644 --- a/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingTests.cs @@ -8,6 +8,8 @@ using Amazon.Runtime; using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -22,12 +24,31 @@ namespace AWS.Deploy.CLI.UnitTests public class GetOptionSettingTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public GetOptionSettingTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } private async Task BuildRecommendationEngine(string testProjectName) @@ -45,7 +66,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + return new RecommendationEngine(session, _recipeHandler); } [Theory] @@ -89,7 +110,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(beanstalkRecommendation, managedActionsEnabled, childValue); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, managedActionsEnabled, childValue); var elasticBeanstalkManagedPlatformUpdates = _optionSettingHandler.GetOptionSetting(beanstalkRecommendation, optionSetting); var elasticBeanstalkManagedPlatformUpdatesValue = _optionSettingHandler.GetOptionSettingValue>(beanstalkRecommendation, elasticBeanstalkManagedPlatformUpdates); @@ -106,11 +127,13 @@ public async Task GetOptionSettingTests_ListType_InvalidValue() var appRunnerRecommendation = recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_APPRUNNER_ID); + var createNew = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.CreateNew"); var subnets = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.Subnets"); var securityGroups = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.SecurityGroups"); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet1" })); - Assert.Throws(() => _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, securityGroups, new SortedSet(){ "securityGroup1" })); + await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, createNew, true); + await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet1" })); + await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, securityGroups, new SortedSet(){ "securityGroup1" })); } [Fact] @@ -125,7 +148,7 @@ public async Task GetOptionSettingTests_ListType() var subnets = _optionSettingHandler.GetOptionSetting(appRunnerRecommendation, "VPCConnector.Subnets"); var emptySubnetsValue = _optionSettingHandler.GetOptionSettingValue(appRunnerRecommendation, subnets); - _optionSettingHandler.SetOptionSettingValue(appRunnerRecommendation, subnets, new SortedSet(){ "subnet-1234abcd" }); + await _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 71038fc98..07ac4872a 100644 --- a/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/RecommendationTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Amazon.Runtime; @@ -10,6 +11,8 @@ using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -25,15 +28,37 @@ namespace AWS.Deploy.CLI.UnitTests public class RecommendationTests { private OrchestratorSession _session; - private readonly IDirectoryManager _directoryManager; + private readonly TestDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly TestFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public RecommendationTests() { _directoryManager = new TestDirectoryManager(); - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _directoryManager = new TestDirectoryManager(); + _fileManager = new TestFileManager(); + var recipeFiles = Directory.GetFiles(RecipeLocator.FindRecipeDefinitionsPath(), "*.recipe", SearchOption.TopDirectoryOnly); + _directoryManager.AddedFiles.Add(RecipeLocator.FindRecipeDefinitionsPath(), new HashSet(recipeFiles)); + foreach (var recipeFile in recipeFiles) + _fileManager.InMemoryStore.Add(recipeFile, File.ReadAllText(recipeFile)); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } private async Task BuildRecommendationEngine(string testProjectName) @@ -51,7 +76,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, _session); + return new RecommendationEngine(_session, _recipeHandler); } [Fact] @@ -185,11 +210,11 @@ public async Task ResetOptionSettingValue_Int() var originalDefaultValue = _optionSettingHandler.GetOptionSettingDefaultValue(fargateRecommendation, desiredCountOptionSetting); - _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, 2); + await _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, 2); Assert.Equal(2, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting)); - _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, consoleUtilities.AskUserForValue("Title", "2", true, originalDefaultValue.ToString())); + await _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting, consoleUtilities.AskUserForValue("Title", "2", true, originalDefaultValue.ToString())); Assert.Equal(originalDefaultValue, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, desiredCountOptionSetting)); } @@ -214,11 +239,11 @@ public async Task ResetOptionSettingValue_String() var originalDefaultValue = _optionSettingHandler.GetOptionSettingDefaultValue(fargateRecommendation, ecsServiceNameOptionSetting); - _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, "TestService"); + await _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, "TestService"); Assert.Equal("TestService", _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting)); - _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, consoleUtilities.AskUserForValue("Title", "TestService", true, originalDefaultValue)); + await _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting, consoleUtilities.AskUserForValue("Title", "TestService", true, originalDefaultValue)); Assert.Equal(originalDefaultValue, _optionSettingHandler.GetOptionSettingValue(fargateRecommendation, ecsServiceNameOptionSetting)); } @@ -262,7 +287,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(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); Assert.Equal("LoadBalanced", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting)); } @@ -276,7 +301,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(beanstalkRecommendation, applicationIAMRoleOptionSetting, new IAMRoleTypeHintResponse {CreateNew = false, + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting, new IAMRoleTypeHintResponse {CreateNew = false, RoleArn = "arn:aws:iam::123456789012:group/Developers" }); var iamRoleTypeHintResponse = _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, applicationIAMRoleOptionSetting); @@ -344,7 +369,7 @@ public void ShouldIncludeTests(RuleEffect effect, bool testPass, bool expectedRe { AWSProfileName = "default" }; - var engine = new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + var engine = new RecommendationEngine(session, _recipeHandler); Assert.Equal(expectedResult, engine.ShouldInclude(effect, testPass)); } @@ -391,7 +416,7 @@ public async Task IsDisplayable_OneDependency() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, loadBalancerTypeOptionSetting)); // Satisfy dependency - _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting, "LoadBalanced"); Assert.Equal("LoadBalanced", _optionSettingHandler.GetOptionSettingValue(beanstalkRecommendation, environmentTypeOptionSetting)); // Verify @@ -414,11 +439,11 @@ public async Task IsDisplayable_ManyDependencies() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(fargateRecommendation, vpcIdOptionSetting)); // Satisfy dependencies - _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, isDefaultOptionSetting, false); + await _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(fargateRecommendation, createNewOptionSetting, false); + await _optionSettingHandler.SetOptionSettingValue(fargateRecommendation, createNewOptionSetting, false); Assert.False(_optionSettingHandler.GetOptionSettingValue(fargateRecommendation, createNewOptionSetting)); // Verify @@ -441,7 +466,7 @@ public async Task IsDisplayable_NotEmptyOperation() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); // Satisfy dependencies - _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, subnetsSetting)); } @@ -463,11 +488,11 @@ public async Task IsDisplayable_NotEmptyOperation_ListType() Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); // Satisfy 1 dependency - _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, vpcIdOptionSetting, "vpc-1234abcd"); Assert.False(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); // Satisfy 2 dependencies - _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, subnetsSetting, new SortedSet { "subnet-1234abcd" }); + await _optionSettingHandler.SetOptionSettingValue(beanstalkRecommendation, subnetsSetting, new SortedSet { "subnet-1234abcd" }); Assert.True(_optionSettingHandler.IsOptionSettingDisplayable(beanstalkRecommendation, securityGroupsSetting)); } diff --git a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs index 6a06a4cae..8ccb9f3b6 100644 --- a/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/ServerModeTests.cs @@ -20,6 +20,11 @@ using System.Collections.Generic; using System.IO; +using AWS.Deploy.Common.Recipes; +using DeploymentTypes = AWS.Deploy.CLI.ServerMode.Models.DeploymentTypes; +using System; +using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Recipes; namespace AWS.Deploy.CLI.UnitTests { @@ -57,10 +62,13 @@ public async Task RecipeController_GetRecipe_EmptyId(string recipeId) var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); - var customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, directoryManager); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + var recipeHandler = new RecipeHandler(deploymentManifestEngine, consoleOrchestratorLogger, directoryManager, fileManager, optionSettingHandler); var projectDefinitionParser = new ProjectDefinitionParser(fileManager, directoryManager); - var recipeController = new RecipeController(customRecipeLocator, projectDefinitionParser); + var recipeController = new RecipeController(recipeHandler, projectDefinitionParser); var response = await recipeController.GetRecipe(recipeId); Assert.IsType(response); @@ -74,11 +82,14 @@ public async Task RecipeController_GetRecipe_HappyPath() var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); var consoleInteractiveServiceImpl = new ConsoleInteractiveServiceImpl(); var consoleOrchestratorLogger = new ConsoleOrchestratorLogger(consoleInteractiveServiceImpl); - var customRecipeLocator = new CustomRecipeLocator(deploymentManifestEngine, consoleOrchestratorLogger, directoryManager); var projectDefinitionParser = new ProjectDefinitionParser(fileManager, directoryManager); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + var recipeHandler = new RecipeHandler(deploymentManifestEngine, consoleOrchestratorLogger, directoryManager, fileManager, optionSettingHandler); - var recipeController = new RecipeController(customRecipeLocator, projectDefinitionParser); - var recipeDefinitions = await RecipeHandler.GetRecipeDefinitions(customRecipeLocator, null); + var recipeController = new RecipeController(recipeHandler, projectDefinitionParser); + var recipeDefinitions = await recipeHandler.GetRecipeDefinitions(null); var recipe = recipeDefinitions.First(); var response = await recipeController.GetRecipe(recipe.Id); @@ -95,28 +106,34 @@ public async Task RecipeController_GetRecipe_WithProjectPath() var fileManager = new FileManager(); var projectDefinitionParser = new ProjectDefinitionParser(fileManager, directoryManager); - var mockCustomRecipeLocator = new Mock(); - - var sourceProjectDirectory = SystemIOUtilities.ResolvePath("WebAppWithDockerFile"); + var deploymentManifestEngine = new Mock(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); var customLocatorCalls = 0; - mockCustomRecipeLocator - .Setup(x => x.LocateCustomRecipePaths(It.IsAny(), It.IsAny())) - .Callback((csProjectPath, solutionPath) => + var sourceProjectDirectory = SystemIOUtilities.ResolvePath("WebAppWithDockerFile"); + deploymentManifestEngine + .Setup(x => x.GetRecipeDefinitionPaths(It.IsAny())) + .Callback((csProjectPath) => { customLocatorCalls++; Assert.Equal(new DirectoryInfo(sourceProjectDirectory).FullName, Directory.GetParent(csProjectPath).FullName); }) - .Returns(Task.FromResult(new HashSet())); + .ReturnsAsync(new List()); + var orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var recipeHandler = new RecipeHandler(deploymentManifestEngine.Object, orchestratorInteractiveService, directoryManager, fileManager, optionSettingHandler); - var projectDefinition = await projectDefinitionParser.Parse(sourceProjectDirectory); - var recipeDefinitions = await RecipeHandler.GetRecipeDefinitions(mockCustomRecipeLocator.Object, projectDefinition); + var projectDefinition = await projectDefinitionParser.Parse(sourceProjectDirectory); + var recipePaths = new HashSet { RecipeLocator.FindRecipeDefinitionsPath() }; + var customRecipePaths = await recipeHandler.LocateCustomRecipePaths(projectDefinition); + var recipeDefinitions = await recipeHandler.GetRecipeDefinitions(recipeDefinitionPaths: recipePaths.Union(customRecipePaths).ToList()); var recipe = recipeDefinitions.First(); Assert.NotEqual(0, customLocatorCalls); customLocatorCalls = 0; - var recipeController = new RecipeController(mockCustomRecipeLocator.Object, projectDefinitionParser); + var recipeController = new RecipeController(recipeHandler, projectDefinitionParser); var response = await recipeController.GetRecipe(recipe.Id, sourceProjectDirectory); Assert.NotEqual(0, customLocatorCalls); diff --git a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs index 2bc7ab0f2..74e73e731 100644 --- a/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/SetOptionSettingTests.cs @@ -8,6 +8,8 @@ using Amazon.Runtime; using AWS.Deploy.CLI.UnitTests.Utilities; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -24,14 +26,27 @@ public class SetOptionSettingTests { private readonly List _recommendations; private readonly IOptionSettingHandler _optionSettingHandler; + private readonly Mock _awsResourceQueryer; private readonly IServiceProvider _serviceProvider; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public SetOptionSettingTests() { var projectPath = SystemIOUtilities.ResolvePath("WebAppNoDockerFile"); - var directoryManager = new DirectoryManager(); - - var parser = new ProjectDefinitionParser(new FileManager(), directoryManager); + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService, _directoryManager, _fileManager, optionSettingHandler); + + var parser = new ProjectDefinitionParser(new FileManager(), _directoryManager); var awsCredentials = new Mock(); var session = new OrchestratorSession( parser.Parse(projectPath).Result, @@ -42,27 +57,41 @@ public SetOptionSettingTests() AWSProfileName = "default" }; - var engine = new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + var engine = new RecommendationEngine(session, _recipeHandler); _recommendations = engine.ComputeRecommendations().GetAwaiter().GetResult(); - + _awsResourceQueryer = new Mock(); var mockServiceProvider = new Mock(); - mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(directoryManager); + mockServiceProvider.Setup(x => x.GetService(typeof(IDirectoryManager))).Returns(_directoryManager); + mockServiceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); _serviceProvider = mockServiceProvider.Object; _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); } + [Fact] + public async Task SetOptionSettingTests_DisallowedValues() + { + var beanstalkApplication = new List { new Amazon.ElasticBeanstalk.Model.ApplicationDescription { ApplicationName = "WebApp1"} }; + _awsResourceQueryer.Setup(x => x.ListOfElasticBeanstalkApplications(It.IsAny())).ReturnsAsync(beanstalkApplication); + var recommendation = _recommendations.First(r => r.Recipe.Id == Constants.ASPNET_CORE_BEANSTALK_RECIPE_ID); + + var optionSetting = _optionSettingHandler.GetOptionSetting(recommendation, "BeanstalkApplication.ApplicationName"); + await Assert.ThrowsAsync(() => _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, "WebApp1")); + } + /// /// This test is to make sure no exception is throw when we set a valid value. /// The values in AllowedValues are the only values allowed to be set. /// [Fact] - public void SetOptionSettingTests_AllowedValues() + public async Task 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(recommendation, optionSetting, optionSetting.AllowedValues.First()); + await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.AllowedValues.First()); Assert.Equal(optionSetting.AllowedValues.First(), _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting)); } @@ -74,46 +103,46 @@ public void SetOptionSettingTests_AllowedValues() /// in AllowedValues can be set. Any other value will throw an exception. /// [Fact] - public void SetOptionSettingTests_MappedValues() + public async Task 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(recommendation, optionSetting, optionSetting.ValueMapping.Values.First())); + await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, optionSetting.ValueMapping.Values.First())); } [Fact] - public void SetOptionSettingTests_KeyValueType() + public async Task SetOptionSettingTests_KeyValueType() { 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")); var values = new Dictionary() { { "key", "value" } }; - _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, values); + await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, values); Assert.Equal(values, _optionSettingHandler.GetOptionSettingValue>(recommendation, optionSetting)); } [Fact] - public void SetOptionSettingTests_KeyValueType_String() + public async Task SetOptionSettingTests_KeyValueType_String() { 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")); var dictionary = new Dictionary() { { "key", "value" } }; var dictionaryString = JsonConvert.SerializeObject(dictionary); - _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, dictionaryString); + await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, dictionaryString); Assert.Equal(dictionary, _optionSettingHandler.GetOptionSettingValue>(recommendation, optionSetting)); } [Fact] - public void SetOptionSettingTests_KeyValueType_Error() + public async Task 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(recommendation, optionSetting, "string")); + await Assert.ThrowsAsync(async () => await _optionSettingHandler.SetOptionSettingValue(recommendation, optionSetting, "string")); } /// @@ -121,14 +150,14 @@ public void SetOptionSettingTests_KeyValueType_Error() /// also sets the corresponding value in recommendation.DeploymentBundle /// [Fact] - public void DeploymentBundleWriteThrough_Docker() + public async Task 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); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DockerExecutionDirectory"), dockerExecutionDirectory); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DockerBuildArgs"), dockerBuildArgs); Assert.Equal(dockerExecutionDirectory, recommendation.DeploymentBundle.DockerExecutionDirectory); Assert.Equal(dockerBuildArgs, recommendation.DeploymentBundle.DockerBuildArgs); @@ -139,16 +168,16 @@ public void DeploymentBundleWriteThrough_Docker() /// also sets the corresponding value in recommendation.DeploymentBundle /// [Fact] - public void DeploymentBundleWriteThrough_Dotnet() + public async Task 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); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DotnetBuildConfiguration"), dotnetBuildConfiguration); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "DotnetPublishArgs"), dotnetPublishArgs); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, "SelfContainedBuild"), selfContainedBuild); Assert.Equal(dotnetBuildConfiguration, recommendation.DeploymentBundle.DotnetPublishBuildConfiguration); Assert.Equal(dotnetPublishArgs, recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments); diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs index c08086b76..25a8e6624 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSecurityGroubsCommandTest.cs @@ -9,6 +9,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -24,14 +25,19 @@ public class ExistingSecurityGroubsCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public ExistingSecurityGroubsCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs index 0a12fb394..76136dff7 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingSubnetsCommandTest.cs @@ -9,6 +9,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -24,14 +25,19 @@ public class ExistingSubnetsCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public ExistingSubnetsCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs index 0366633b1..aa67632f8 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/ExistingVpcCommandTest.cs @@ -9,6 +9,7 @@ using AWS.Deploy.CLI.Commands.TypeHints; using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -24,14 +25,17 @@ public class ExistingVpcCommandTest private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _serviceProvider; public ExistingVpcCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_mockAWSResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs index 0499f9c7f..bafd46464 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintCommands/VPCConnectorCommandTest.cs @@ -11,6 +11,7 @@ using AWS.Deploy.CLI.Common.UnitTests.IO; using AWS.Deploy.CLI.TypeHintResponses; using AWS.Deploy.CLI.UnitTests.Utilities; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -27,15 +28,18 @@ public class VPCConnectorCommandTest private readonly IDirectoryManager _directoryManager; private readonly IToolInteractiveService _toolInteractiveService; private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _serviceProvider; public VPCConnectorCommandTest() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); _toolInteractiveService = new TestToolInteractiveServiceImpl(); - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_mockAWSResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs index 2b35cb96c..142517bcb 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TypeHintTests.cs @@ -22,18 +22,24 @@ using AWS.Deploy.Orchestration.Data; using Moq; using Xunit; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.CLI.UnitTests { public class TypeHintTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public TypeHintTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/HelperFunctions.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/HelperFunctions.cs index e52c0d16d..5cd1aaa63 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/HelperFunctions.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/HelperFunctions.cs @@ -1,10 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 +using System; using System.Threading.Tasks; using Amazon.Runtime; using AWS.Deploy.Common; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.RecommendationEngine; using AWS.Deploy.Recipes; @@ -24,6 +27,13 @@ public static async Task BuildRecommendationEngine( { var fullPath = SystemIOUtilities.ResolvePath(testProjectName); + var deploymentManifestEngine = new DeploymentManifestEngine(directoryManager, fileManager); + var orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + var recipeHandler = new RecipeHandler(deploymentManifestEngine, orchestratorInteractiveService, directoryManager, fileManager, optionSettingHandler); + var parser = new ProjectDefinitionParser(fileManager, directoryManager); var awsCredentials = new Mock(); var session = new OrchestratorSession( @@ -35,7 +45,7 @@ await parser.Parse(fullPath), AWSProfileName = awsProfile }; - return new RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + return new RecommendationEngine(session, recipeHandler); } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index 67058d066..42414dca6 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Amazon.AppRunner.Model; +using Amazon.CloudControlApi.Model; using Amazon.CloudFormation.Model; using Amazon.CloudFront.Model; using Amazon.CloudWatchEvents.Model; @@ -16,6 +17,7 @@ using Amazon.Runtime; using Amazon.S3; using Amazon.SecurityToken.Model; +using AWS.Deploy.Common.Data; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.Data; @@ -62,9 +64,9 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> GetElasticBeanstalkPlatformArns() => throw new NotImplementedException(); public Task> GetListOfVpcs() => throw new NotImplementedException(); public Task> ListOfEC2KeyPairs() => throw new NotImplementedException(); - public Task> ListOfECSClusters() => throw new NotImplementedException(); - public Task> ListOfElasticBeanstalkApplications() => throw new NotImplementedException(); - public Task> ListOfElasticBeanstalkEnvironments(string applicationName) => throw new NotImplementedException(); + public Task> ListOfECSClusters(string ecsClusterName) => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkApplications(string applicationName) => throw new NotImplementedException(); + public Task> ListOfElasticBeanstalkEnvironments(string applicationName, string environmentName) => throw new NotImplementedException(); public Task> ListOfIAMRoles(string servicePrincipal) => throw new NotImplementedException(); public Task> DescribeCloudFormationResources(string stackName) => throw new NotImplementedException(); public Task DescribeElasticBeanstalkEnvironment(string environmentId) => throw new NotImplementedException(); @@ -90,5 +92,6 @@ public Task GetLatestElasticBeanstalkPlatformArn() public Task> DescribeSubnets(string vpcID = null) => throw new NotImplementedException(); public Task> DescribeSecurityGroups(string vpcID = null) => throw new NotImplementedException(); public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); + public Task GetCloudControlApiResource(string type, string identifier) => throw new NotImplementedException(); } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj b/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj index 5509e8dc8..3df9f05f1 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj +++ b/test/AWS.Deploy.Orchestration.UnitTests/AWS.Deploy.Orchestration.UnitTests.csproj @@ -26,6 +26,16 @@ - + + + + + + + Always + + + + diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs index af2726a03..502431d5f 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs @@ -13,18 +13,24 @@ using System.Collections.Generic; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Common.Data; namespace AWS.Deploy.Orchestration.UnitTests.CDK { public class CDKProjectHandlerTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; public CDKProjectHandlerTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs index 391621464..962f59b6c 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs @@ -11,6 +11,7 @@ using Amazon.ElasticBeanstalk; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Data; @@ -55,7 +56,7 @@ public async Task GetExistingDeployedApplications_ListDeploymentsCall() .Returns(Task.FromResult(new List() { stack })); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new List())); var deployedApplicationQueryer = new DeployedApplicationQueryer( @@ -100,7 +101,7 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() .Returns(Task.FromResult(stacks)); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new List())); var deployedApplicationQueryer = new DeployedApplicationQueryer( @@ -163,7 +164,7 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() .Returns(Task.FromResult(stacks)); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new List())); var deployedApplicationQueryer = new DeployedApplicationQueryer( @@ -220,7 +221,7 @@ public async Task GetExistingDeployedApplications_InvalidConfigurations(string r .Returns(Task.FromResult(new List() { stack })); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new List())); var deployedApplicationQueryer = new DeployedApplicationQueryer( @@ -271,7 +272,7 @@ public async Task GetExistingDeployedApplications_ContainsValidBeanstalkEnvironm .Returns(Task.FromResult(new List())); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(environments)); _mockAWSResourceQueryer @@ -324,7 +325,7 @@ public async Task GetExistingDeployedApplication_SkipsEnvironmentsWithIncompatib .Returns(Task.FromResult(new List())); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(environments)); _mockAWSResourceQueryer @@ -385,7 +386,7 @@ public async Task GetExistingDeployedApplication_SkipsEnvironmentsCreatedFromThe .Returns(Task.FromResult(new List())); _mockAWSResourceQueryer - .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny())) + .Setup(x => x.ListOfElasticBeanstalkEnvironments(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(environments)); _mockAWSResourceQueryer diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs index 2f3cb4ced..9edab28a4 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DisplayedResourcesHandlerTests.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; @@ -8,7 +9,11 @@ using Amazon.ElasticBeanstalk.Model; using Amazon.Runtime; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.DisplayedResources; using AWS.Deploy.Orchestration.UnitTests.Utilities; @@ -29,9 +34,22 @@ public class DisplayedResourcesHandlerTests private readonly EnvironmentDescription _environmentDescription; private readonly LoadBalancer _loadBalancer; private OrchestratorSession _session; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly Mock _orchestratorInteractiveService; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public DisplayedResourcesHandlerTests() { + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new Mock(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService.Object, _directoryManager, _fileManager, optionSettingHandler); _mockAWSResourceQueryer = new Mock(); _cloudApplication = new CloudApplication("StackName", "UniqueId", CloudApplicationResourceType.CloudFormationStack, "RecipeId"); _displayedResourcesFactory = new DisplayedResourceCommandFactory(_mockAWSResourceQueryer.Object); @@ -56,7 +74,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine.RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, _session); + return new RecommendationEngine.RecommendationEngine(_session, _recipeHandler); } [Fact] diff --git a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs index aa6793395..45739ca28 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs @@ -9,6 +9,8 @@ using Amazon.ElasticBeanstalk.Model; using Amazon.Runtime; using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.DeploymentManifest; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -23,12 +25,31 @@ namespace AWS.Deploy.Orchestration.UnitTests public class ElasticBeanstalkHandlerTests { private readonly IOptionSettingHandler _optionSettingHandler; - private readonly IServiceProvider _serviceProvider; + private readonly Mock _awsResourceQueryer; + private readonly Mock _serviceProvider; + private readonly IDeploymentManifestEngine _deploymentManifestEngine; + private readonly Mock _orchestratorInteractiveService; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IRecipeHandler _recipeHandler; public ElasticBeanstalkHandlerTests() { - _serviceProvider = new Mock().Object; - _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider)); + _awsResourceQueryer = new Mock(); + _serviceProvider = new Mock(); + _serviceProvider + .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) + .Returns(_awsResourceQueryer.Object); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _deploymentManifestEngine = new DeploymentManifestEngine(_directoryManager, _fileManager); + _orchestratorInteractiveService = new Mock(); + var serviceProvider = new Mock(); + var validatorFactory = new ValidatorFactory(serviceProvider.Object); + var optionSettingHandler = new OptionSettingHandler(validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine, _orchestratorInteractiveService.Object, _directoryManager, _fileManager, optionSettingHandler); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); } [Fact] @@ -90,10 +111,10 @@ public async Task GetAdditionSettingsTest_CustomValues() new Mock().Object, _optionSettingHandler); - _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"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId), "basic"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.HealthCheckURLOptionId), "/url"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.ProxyOptionId), "none"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.XRayTracingOptionId), "true"); // ACT var optionSettings = elasticBeanstalkHandler.GetEnvironmentConfigurationSettings(recommendation); @@ -143,7 +164,7 @@ await parser.Parse(fullPath), AWSProfileName = "default" }; - return new RecommendationEngine.RecommendationEngine(new[] { RecipeLocator.FindRecipeDefinitionsPath() }, session); + return new RecommendationEngine.RecommendationEngine(session, _recipeHandler); } private bool IsEqual(ConfigurationOptionSetting expected, ConfigurationOptionSetting actual) diff --git a/test/AWS.Deploy.Orchestration.UnitTests/RecipeHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/RecipeHandlerTests.cs new file mode 100644 index 000000000..0dc6ae13b --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/RecipeHandlerTests.cs @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.Common.DeploymentManifest; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Orchestration.UnitTests.Utilities; +using AWS.Deploy.Recipes; +using Moq; +using Xunit; + +namespace AWS.Deploy.Orchestration.UnitTests +{ + public class RecipeHandlerTests + { + private readonly Mock _deploymentManifestEngine; + private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly TestDirectoryManager _directoryManager; + private readonly TestFileManager _fileManager; + private readonly Mock _serviceProvider; + private readonly IValidatorFactory _validatorFactory; + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IRecipeHandler _recipeHandler; + + public RecipeHandlerTests() + { + _deploymentManifestEngine = new Mock(); + _orchestratorInteractiveService = new TestToolOrchestratorInteractiveService(); + _directoryManager = new TestDirectoryManager(); + _fileManager = new TestFileManager(); + _serviceProvider = new Mock(); + _validatorFactory = new ValidatorFactory(_serviceProvider.Object); + _optionSettingHandler = new OptionSettingHandler(_validatorFactory); + _recipeHandler = new RecipeHandler(_deploymentManifestEngine.Object, _orchestratorInteractiveService, _directoryManager, _fileManager, _optionSettingHandler); + } + + [Fact] + public async Task DependencyTree_HappyPath() + { + _directoryManager.AddedFiles.Add(RecipeLocator.FindRecipeDefinitionsPath(), new HashSet { "path1" }); + _fileManager.InMemoryStore.Add("path1", File.ReadAllText("./Recipes/OptionSettingCyclicDependency.recipe")); + var recipeDefinitions = await _recipeHandler.GetRecipeDefinitions(null); + + var recipe = Assert.Single(recipeDefinitions); + Assert.Equal("AspNetAppEcsFargate", recipe.Id); + var ecsCluster = recipe.OptionSettings.First(x => x.Id.Equals("ECSCluster")); + Assert.NotNull(ecsCluster); + Assert.Empty(ecsCluster.Dependents); + var ecsClusterCreateNew = ecsCluster.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.NotNull(ecsClusterCreateNew); + Assert.Equal(2, ecsClusterCreateNew.Dependents.Count); + Assert.NotNull(ecsClusterCreateNew.Dependents.First(x => x.Equals("ECSCluster.ClusterArn"))); + Assert.NotNull(ecsClusterCreateNew.Dependents.First(x => x.Equals("ECSCluster.NewClusterName"))); + } + + [Fact] + public async Task DependencyTree_CyclicDependency() + { + _directoryManager.AddedFiles.Add(RecipeLocator.FindRecipeDefinitionsPath(), new HashSet { "path1" }); + _fileManager.InMemoryStore.Add("path1", File.ReadAllText("./Recipes/OptionSettingCyclicDependency.recipe")); + var recipeDefinitions = await _recipeHandler.GetRecipeDefinitions(null); + + var recipe = Assert.Single(recipeDefinitions); + Assert.Equal("AspNetAppEcsFargate", recipe.Id); + var iamRole = recipe.OptionSettings.First(x => x.Id.Equals("ApplicationIAMRole")); + Assert.NotNull(iamRole); + Assert.Empty(iamRole.Dependents); + var iamRoleCreateNew = iamRole.ChildOptionSettings.First(x => x.Id.Equals("CreateNew")); + Assert.NotNull(iamRoleCreateNew); + Assert.Single(iamRoleCreateNew.Dependents); + Assert.NotNull(iamRoleCreateNew.Dependents.First(x => x.Equals("ApplicationIAMRole.RoleArn"))); + var iamRoleRoleArn = iamRole.ChildOptionSettings.First(x => x.Id.Equals("RoleArn")); + Assert.NotNull(iamRoleRoleArn); + Assert.Single(iamRoleRoleArn.Dependents); + Assert.NotNull(iamRoleRoleArn.Dependents.First(x => x.Equals("ApplicationIAMRole.CreateNew"))); + } + } +} diff --git a/test/AWS.Deploy.Orchestration.UnitTests/Recipes/OptionSettingCyclicDependency.recipe b/test/AWS.Deploy.Orchestration.UnitTests/Recipes/OptionSettingCyclicDependency.recipe new file mode 100644 index 000000000..256792138 --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/Recipes/OptionSettingCyclicDependency.recipe @@ -0,0 +1,791 @@ +{ + "$schema": "./aws-deploy-recipe-schema.json", + "Id": "AspNetAppEcsFargate", + "Version": "0.1.0", + "Name": "ASP.NET Core App to Amazon ECS using Fargate", + "OptionSettings": [ + { + "Id": "ECSCluster", + "Name": "ECS Cluster", + "Category": "General", + "Description": "The ECS cluster used for the deployment.", + "Type": "Object", + "TypeHint": "ECSCluster", + "AdvancedSetting": false, + "Updatable": false, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New ECS Cluster", + "Description": "Do you want to create a new ECS cluster?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "ClusterArn", + "Name": "Existing Cluster ARN", + "Description": "The ARN of the existing cluster to use.", + "Type": "String", + "TypeHint": "ExistingECSCluster", + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "arn:[^:]+:ecs:[^:]*:[0-9]{12}:cluster/.+", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid cluster Arn. The ARN should contain the arn:[PARTITION]:ecs namespace, followed by the Region of the cluster, the AWS account ID of the cluster owner, the cluster namespace, and then the cluster name. For example, arn:aws:ecs:region:012345678910:cluster/test. For more information visit https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Cluster.html" + } + } + ], + "DependsOn": [ + { + "Id": "ECSCluster.CreateNew", + "Value": false + } + ] + }, + { + "Id": "NewClusterName", + "Name": "New Cluster Name", + "Description": "The name of the new cluster to create.", + "Type": "String", + "DefaultValue": "{StackName}", + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^([A-Za-z0-9_-]{1,255})$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid cluster name. The cluster name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." + } + }, + { + "ValidatorType": "ExistingResource", + "Configuration": { + "ResourceType": "AWS::ECS::Cluster" + } + } + ], + "DependsOn": [ + { + "Id": "ECSCluster.CreateNew", + "Value": true + } + ] + } + ] + }, + { + "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", + "DefaultValue": "{StackName}-service", + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^([A-Za-z0-9_-]{1,255})$", + "ValidationFailedMessage": "Invalid service name. The service name can only contain letters (case-sensitive), numbers, hyphens, underscores and can't be longer than 255 character in length." + } + } + ] + }, + { + "Id": "DesiredCount", + "Name": "Desired Task Count", + "Category": "Compute", + "Description": "The desired number of ECS tasks to run for the service.", + "Type": "Int", + "DefaultValue": 3, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 5000 + } + } + ] + }, + { + "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", + "TypeHintData": { + "ServicePrincipal": "ecs-tasks.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": true, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New Role", + "Description": "Do you want to create a new role?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": true, + "DependsOn": [ + { + "Id": "ApplicationIAMRole.RoleArn", + "Value": false + } + ] + }, + { + "Id": "RoleArn", + "Name": "Existing Role ARN", + "Description": "The ARN of the existing role to use.", + "Type": "String", + "TypeHint": "ExistingIAMRole", + "TypeHintData": { + "ServicePrincipal": "ecs-tasks.amazonaws.com" + }, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "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" + } + } + ], + "DependsOn": [ + { + "Id": "ApplicationIAMRole.CreateNew", + "Value": false + } + ] + } + ] + }, + { + "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", + "AdvancedSetting": false, + "Updatable": false, + "ChildOptionSettings": [ + { + "Id": "IsDefault", + "Name": "Use default VPC", + "Description": "Do you want to use the default VPC for the deployment?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "CreateNew", + "Name": "Create New VPC", + "Description": "Do you want to create a new VPC?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": false, + "DependsOn": [ + { + "Id": "Vpc.IsDefault", + "Value": false + } + ] + }, + { + "Id": "VpcId", + "Name": "Existing VPC ID", + "Description": "The ID of the existing VPC to use.", + "Type": "String", + "TypeHint": "ExistingVpc", + "DefaultValue": null, + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "Configuration": { + "Regex": "^vpc-([0-9a-f]{8}|[0-9a-f]{17})$", + "AllowEmptyString": true, + "ValidationFailedMessage": "Invalid VPC ID. The VPC ID must start with the \"vpc-\" prefix, followed by either 8 or 17 characters consisting of digits and letters(lower-case) from a to f. For example vpc-abc88de9 is a valid VPC ID." + } + } + ], + "DependsOn": [ + { + "Id": "Vpc.IsDefault", + "Value": false + }, + { + "Id": "Vpc.CreateNew", + "Value": false + } + ] + } + ] + }, + { + "Id": "LoadBalancer", + "Name": "Elastic Load Balancer", + "Category": "LoadBalancer", + "Description": "Load Balancer the ECS Service will register tasks to.", + "Type": "Object", + "AdvancedSetting": true, + "Updatable": true, + "ChildOptionSettings": [ + { + "Id": "CreateNew", + "Name": "Create New Load Balancer", + "Description": "Do you want to create a new Load Balancer?", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false + }, + { + "Id": "ExistingLoadBalancerArn", + "Name": "Existing Load Balancer ARN", + "Description": "The ARN of an existing load balancer to use.", + "Type": "String", + "TypeHint": "ExistingApplicationLoadBalancer", + "DefaultValue": null, + "AdvancedSetting": false, + "Updatable": false, + "Validators": [ + { + "ValidatorType": "Regex", + "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" + } + } + ], + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + } + ] + }, + { + "Id": "DeregistrationDelayInSeconds", + "Name": "Deregistration delay (seconds)", + "Description": "The amount of time to allow requests to finish before deregistering ECS tasks.", + "Type": "Int", + "DefaultValue": 60, + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ] + }, + { + "Id": "HealthCheckPath", + "Name": "Health Check Path", + "Description": "The ping path destination where Elastic Load Balancing sends health check requests.", + "Type": "String", + "DefaultValue": "/", + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "HealthCheckInternval", + "Name": "Health Check Interval", + "Description": "The approximate interval, in seconds, between health checks of an individual instance.", + "Type": "Int", + "DefaultValue": 30, + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 5, + "Max": 300 + } + } + ] + }, + { + "Id": "HealthyThresholdCount", + "Name": "Healthy Threshold Count", + "Description": "The number of consecutive health check successes required before considering an unhealthy target healthy.", + "Type": "Int", + "DefaultValue": 5, + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 2, + "Max": 10 + } + } + ] + }, + { + "Id": "UnhealthyThresholdCount", + "Name": "Unhealthy Threshold Count", + "Description": "The number of consecutive health check successes required before considering an unhealthy target unhealthy.", + "Type": "Int", + "DefaultValue": 2, + "AdvancedSetting": true, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 2, + "Max": 10 + } + } + ] + }, + { + "Id": "ListenerConditionType", + "Name": "Type of Listener Condition", + "Description": "The type of listener rule to create to direct traffic to ECS service.", + "Type": "String", + "DefaultValue": "None", + "AdvancedSetting": false, + "Updatable": true, + "AllowedValues": [ + "None", + "Path" + ], + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + } + ] + }, + { + "Id": "ListenerConditionPathPattern", + "Name": "Listener Condition Path Pattern", + "Description": "The resource path pattern to use for the listener rule. (i.e. \"/api/*\") ", + "Type": "String", + "DefaultValue": null, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Regex", + "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: '_-.$/~\"'@:+'" + } + } + ], + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + }, + { + "Id": "LoadBalancer.ListenerConditionType", + "Value": "Path" + } + ] + }, + { + "Id": "ListenerConditionPriority", + "Name": "Listener Condition Priority", + "Description": "Priority of the condition rule. The value must be unique for the Load Balancer listener.", + "Type": "Int", + "DefaultValue": 100, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 50000 + } + } + ], + "DependsOn": [ + { + "Id": "LoadBalancer.CreateNew", + "Value": false + }, + { + "Id": "LoadBalancer.ListenerConditionType", + "Value": "Path" + } + ] + } + ] + }, + { + "Id": "AutoScaling", + "Name": "AutoScaling", + "Category": "AutoScaling", + "Description": "The AutoScaling configuration for the ECS service.", + "Type": "Object", + "AdvancedSetting": true, + "Updatable": true, + "ChildOptionSettings": [ + { + "Id": "Enabled", + "Name": "Enable", + "Description": "Do you want to enable AutoScaling?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "MinCapacity", + "Name": "Minimum Capacity", + "Description": "The minimum number of ECS tasks handling the demand for the ECS service.", + "Type": "Int", + "DefaultValue": 3, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 5000 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "MaxCapacity", + "Name": "Maximum Capacity", + "Description": "The maximum number of ECS tasks handling the demand for the ECS service.", + "Type": "Int", + "DefaultValue": 6, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 5000 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "ScalingType", + "Name": "AutoScaling Metric", + "Description": "The metric to monitor for scaling changes.", + "Type": "String", + "DefaultValue": "Cpu", + "AdvancedSetting": false, + "Updatable": true, + "AllowedValues": [ + "Cpu", + "Memory", + "Request" + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + } + ] + }, + { + "Id": "CpuTypeTargetUtilizationPercent", + "Name": "CPU Target Utilization", + "Description": "The target cpu utilization percentage that triggers a scaling change.", + "Type": "Double", + "DefaultValue": 70, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 100 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "CpuTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "CpuTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Cpu" + } + ] + }, + { + "Id": "MemoryTypeTargetUtilizationPercent", + "Name": "Memory Target Utilization", + "Description": "The target memory utilization percentage that triggers a scaling change.", + "Type": "Double", + "DefaultValue": 70, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1, + "Max": 100 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "MemoryTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "MemoryTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Memory" + } + ] + }, + { + "Id": "RequestTypeRequestsPerTarget", + "Name": "Request per task", + "Description": "The number of request per ECS task that triggers a scaling change.", + "Type": "Int", + "DefaultValue": 1000, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 1 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + }, + { + "Id": "RequestTypeScaleInCooldownSeconds", + "Name": "Scale in cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + }, + { + "Id": "RequestTypeScaleOutCooldownSeconds", + "Name": "Scale out cooldown (seconds)", + "Description": "The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.", + "Type": "Int", + "DefaultValue": 300, + "AdvancedSetting": false, + "Updatable": true, + "Validators": [ + { + "ValidatorType": "Range", + "Configuration": { + "Min": 0, + "Max": 3600 + } + } + ], + "DependsOn": [ + { + "Id": "AutoScaling.Enabled", + "Value": true + }, + { + "Id": "AutoScaling.ScalingType", + "Value": "Request" + } + ] + } + ] + } + ] +} diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs index 598398bd2..68b621dee 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestDirectoryManager.cs @@ -5,12 +5,14 @@ using System.Collections.Generic; using System.IO; using AWS.Deploy.Common.IO; +using System.Linq; namespace AWS.Deploy.Orchestration.UnitTests { public class TestDirectoryManager : IDirectoryManager { public readonly HashSet CreatedDirectories = new(); + public readonly Dictionary> AddedFiles = new(); public DirectoryInfo CreateDirectory(string path) { @@ -45,7 +47,7 @@ public string[] GetDirectories(string path, string searchPattern = null, SearchO throw new NotImplementedException("If your test needs this method, you'll need to implement this."); public string[] GetFiles(string path, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly) => - throw new NotImplementedException("If your test needs this method, you'll need to implement this."); + AddedFiles.ContainsKey(path) ? AddedFiles[path].ToArray() : new string[0]; public bool IsEmpty(string path) => throw new NotImplementedException("If your test needs this method, you'll need to implement this."); diff --git a/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs b/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs index 9bd034fb5..7afa30b0d 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/TestFileManager.cs @@ -19,6 +19,7 @@ public bool Exists(string path) { return InMemoryStore.ContainsKey(path); } + public bool Exists(string path, string directory) => throw new NotImplementedException(); public Task ReadAllTextAsync(string path) { diff --git a/test/AWS.Deploy.Orchestration.UnitTests/Utilities/TestToolOrchestratorInteractiveService.cs b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/TestToolOrchestratorInteractiveService.cs new file mode 100644 index 000000000..d540858ae --- /dev/null +++ b/test/AWS.Deploy.Orchestration.UnitTests/Utilities/TestToolOrchestratorInteractiveService.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; + +namespace AWS.Deploy.Orchestration.UnitTests.Utilities +{ + public class TestToolOrchestratorInteractiveService : IOrchestratorInteractiveService + { + public IList SectionStartMessages { get; } = new List(); + public IList DebugMessages { get; } = new List(); + public IList OutputMessages { get; } = new List(); + public IList ErrorMessages { get; } = new List(); + + public void LogSectionStart(string message, string description) => SectionStartMessages.Add(message); + public void LogDebugMessage(string message) => DebugMessages.Add(message); + public void LogErrorMessage(string message) => ErrorMessages.Add(message); + public void LogInfoMessage(string message) => OutputMessages.Add(message); + } +} diff --git a/testapps/ConsoleAppTask/Docker/Dockerfile b/testapps/ConsoleAppTask/Docker/Dockerfile new file mode 100644 index 000000000..f90c8b27e --- /dev/null +++ b/testapps/ConsoleAppTask/Docker/Dockerfile @@ -0,0 +1,25 @@ +# See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. +# Dockerfile paths are adjusted to allow docker build from the root of the repository +# This is required for integration tests +# +# Note: this is the same as the Dockerfile up one directory, but duplicated +# here for testing dockerfiles located in alternative locations. + +FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY ["ConsoleAppTask.csproj", "ConsoleAppTask/"] +RUN dotnet restore "ConsoleAppTask/ConsoleAppTask.csproj" +COPY. "ConsoleAppTask/" +WORKDIR "/src/ConsoleAppTask" +RUN dotnet build "ConsoleAppTask.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "ConsoleAppTask.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from= publish / app / publish. +ENTRYPOINT["dotnet", "ConsoleAppTask.dll"] diff --git a/version.json b/version.json index 4cf1ffd91..5dc55fda3 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.45", + "version": "0.46", "publicReleaseRefSpec": [ ".*" ],