diff --git a/.github/workflows/codebuild-ci.yml b/.github/workflows/codebuild-ci.yml index ea62a1a8a..a9be1f380 100644 --- a/.github/workflows/codebuild-ci.yml +++ b/.github/workflows/codebuild-ci.yml @@ -17,6 +17,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v1 with: role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} + role-duration-seconds: 7200 aws-region: us-west-2 - name: Run CodeBuild id: codebuild diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index a080bc445..b8ac90477 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -17,7 +17,7 @@ ** 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 ** 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.CDK.Lib; version 2.43.1 -- 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/ diff --git a/buildtools/ci.template.yml b/buildtools/ci.template.yml index 33a371849..1d3f7ca17 100644 --- a/buildtools/ci.template.yml +++ b/buildtools/ci.template.yml @@ -96,6 +96,7 @@ Resources: BuildSpec: buildtools/ci.buildspec.yml Artifacts: Type: NO_ARTIFACTS + TimeoutInMinutes: 120 CodeBuildProjectRole: Type: AWS::IAM::Role diff --git a/site/content/troubleshooting-guide/index.md b/site/content/troubleshooting-guide/index.md index c69df7efb..15abedb52 100644 --- a/site/content/troubleshooting-guide/index.md +++ b/site/content/troubleshooting-guide/index.md @@ -106,3 +106,13 @@ Resource handler returned message: "'MemorySize' value failed to satisfy constra **Why this is happening:** The [BucketDeployment](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_deployment.BucketDeployment.html) CDK Construct used to deploy the Blazor recipe uses an AWS Lambda function to replicate the application files from the CDK bucket to the deployment bucket. In some versions of the deploy tool the default memory limit for this Lambda function exceeded the 3008MB quota placed on new AWS accounts. **Resolution:** See [Lambda: Concurrency and memory quotas](https://docs.aws.amazon.com/lambda/latest/dg/troubleshooting-deployment.html#troubleshooting-deployment-quotas) for how to request a quota increase. + +## App Runner Failed with _Resource handler returned message: "null"_ +When attempting to deploy to App Runner, creation of the `AWS::AppRunner::Service` resource may fail with a message such as: +``` +CREATE_FAILED | AWS::AppRunner::Service | Recipe/AppRunnerService (RecipeAppRunnerService) Resource handler returned message: "null" +``` + +**Why this is happening:** This error could happen for a variety of reasons, such as the application failing its initial health check or limited permissions. + +**Resolution:** The resolution will depend on the failure reason. To aid diagnosis, attempt to deploy your application again. While it is deploying, navigate to the the Deployment and Application logs sections of _App Runner > Services > [name of your cloud application]_ in the AWS Console and review the logs for any unexpected errors. See [Viewing App Runner logs streamed to CloudWatch Logs](https://docs.aws.amazon.com/apprunner/latest/dg/monitor-cwl.html) for more details. \ No newline at end of file diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 2a195f7c8..a709c5a76 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -361,9 +361,14 @@ private async Task GetSelectedRecommendationFromPreviousDeployme IDictionary previousSettings; if (deployedApplication.ResourceType == CloudApplicationResourceType.CloudFormationStack) - previousSettings = (await _cloudFormationTemplateReader.LoadCloudApplicationMetadata(deployedApplication.Name)).Settings; + { + var metadata = await _cloudFormationTemplateReader.LoadCloudApplicationMetadata(deployedApplication.Name); + previousSettings = metadata.Settings.Union(metadata.DeploymentBundleSettings).ToDictionary(x => x.Key, x => x.Value); + } else - previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication); + { + previousSettings = await _deployedApplicationQueryer.GetPreviousSettings(deployedApplication, selectedRecommendation); + } await orchestrator.ApplyAllReplacementTokens(selectedRecommendation, deployedApplication.Name); diff --git a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs index 3226eb5ed..fd3f77977 100644 --- a/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs +++ b/src/AWS.Deploy.CLI/Extensions/CustomServiceCollectionExtension.cs @@ -38,6 +38,7 @@ public static void AddCustomServices(this IServiceCollection serviceCollection, serviceCollection.TryAdd(new ServiceDescriptor(typeof(IAWSUtilities), typeof(AWSUtilities), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKInstaller), typeof(CDKInstaller), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICDKManager), typeof(CDKManager), lifetime)); + serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICdkAppSettingsSerializer), typeof(CdkAppSettingsSerializer), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICdkProjectHandler), typeof(CdkProjectHandler), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICloudApplicationNameGenerator), typeof(CloudApplicationNameGenerator), lifetime)); serviceCollection.TryAdd(new ServiceDescriptor(typeof(ICommandLineWrapper), typeof(CommandLineWrapper), lifetime)); diff --git a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 497ce7988..b128192a9 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -436,9 +436,14 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody IDictionary previousSettings; if (existingDeployment.ResourceType == CloudApplicationResourceType.CloudFormationStack) - previousSettings = (await templateMetadataReader.LoadCloudApplicationMetadata(existingDeployment.Name)).Settings; + { + var metadata = await templateMetadataReader.LoadCloudApplicationMetadata(existingDeployment.Name); + previousSettings = metadata.Settings.Union(metadata.DeploymentBundleSettings).ToDictionary(x => x.Key, x => x.Value); + } else - previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment); + { + previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment, state.SelectedRecommendation); + } state.SelectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(state.SelectedRecommendation, previousSettings); @@ -719,7 +724,9 @@ private CdkProjectHandler CreateCdkProjectHandler(SessionState state, IServicePr serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService() diff --git a/src/AWS.Deploy.Common/CloudApplicationMetadata.cs b/src/AWS.Deploy.Common/CloudApplicationMetadata.cs index d611f6f8e..f6764db27 100644 --- a/src/AWS.Deploy.Common/CloudApplicationMetadata.cs +++ b/src/AWS.Deploy.Common/CloudApplicationMetadata.cs @@ -27,6 +27,11 @@ public class CloudApplicationMetadata /// public IDictionary Settings { get; set; } = new Dictionary(); + /// + /// Comprises of option settings that are part of the deployment bundle definition. + /// + public IDictionary DeploymentBundleSettings { get; set; } = new Dictionary(); + public CloudApplicationMetadata(string recipeId, string recipeVersion) { RecipeId = recipeId; diff --git a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs index 919a1ff05..2323278c2 100644 --- a/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs +++ b/src/AWS.Deploy.Common/Recipes/IOptionSettingHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.Common.Recipes @@ -84,5 +85,12 @@ public interface IOptionSettingHandler /// /// true if the option setting item has been modified or false otherwise bool IsOptionSettingModified(Recommendation recommendation, OptionSettingItem optionSetting); + + /// + /// Returns a Dictionary containing the configurable option settings for the specified recommendation. The returned dictionary can contain specific types of option settings depending on the value of . + /// The key in the dictionary is the fully qualified ID of each option setting + /// The value in the dictionary is the value of each option setting + /// + Dictionary GetOptionSettingsMap(Recommendation recommendation, ProjectDefinition projectDefinition, IDirectoryManager directoryManager, OptionSettingsType optionSettingsType = OptionSettingsType.All); } } diff --git a/src/AWS.Deploy.Common/Recipes/OptionSettingsType.cs b/src/AWS.Deploy.Common/Recipes/OptionSettingsType.cs new file mode 100644 index 000000000..965d5686e --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/OptionSettingsType.cs @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Text; + +namespace AWS.Deploy.Common.Recipes +{ + /// + /// This enum is used to specify the type of option settings that are retrieved when invoking + /// + public enum OptionSettingsType + { + /// + /// Theses option settings are part of the individual recipe files. + /// + Recipe, + + /// + /// These option settings are part of the deployment bundle definitions. + /// + DeploymentBundle, + + /// + /// Comprises of all types of option settings + /// + All + } +} diff --git a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs index 14883c92f..62e050389 100644 --- a/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs +++ b/src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs @@ -55,6 +55,11 @@ public class RecipeDefinition /// public string TargetService { get; set; } + /// + /// The environment platform the recipe deploys to. This is used to publish a self-contained .NET application for that platform. + /// + public TargetPlatform? TargetPlatform { get; set; } + /// /// The list of DisplayedResources that lists logical CloudFormation IDs with a description. /// diff --git a/src/AWS.Deploy.Common/Recipes/TargetPlatform.cs b/src/AWS.Deploy.Common/Recipes/TargetPlatform.cs new file mode 100644 index 000000000..c513cb6f4 --- /dev/null +++ b/src/AWS.Deploy.Common/Recipes/TargetPlatform.cs @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace AWS.Deploy.Common.Recipes +{ + public enum TargetPlatform + { + Linux, + Windows + } +} diff --git a/src/AWS.Deploy.Constants/CloudFormationIdentifier.cs b/src/AWS.Deploy.Constants/CloudFormationIdentifier.cs index c3fc8b0b3..bc2162423 100644 --- a/src/AWS.Deploy.Constants/CloudFormationIdentifier.cs +++ b/src/AWS.Deploy.Constants/CloudFormationIdentifier.cs @@ -23,10 +23,15 @@ internal static class CloudFormationIdentifier public const string STACK_DESCRIPTION_PREFIX = "AWSDotnetDeployCDKStack"; /// - /// The CloudFormation template metadata key used to hold the last used settings to deploy the application. + /// The CloudFormation template metadata key used to hold the last used recipe option settings to deploy the application. /// public const string STACK_METADATA_SETTINGS = "aws-dotnet-deploy-settings"; + /// + /// The CloudFormation template metadata key used to hold the last used deployment bundle settings to deploy the application. + /// + public const string STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS = "aws-dotnet-deploy-deployment-bundle-settings"; + /// /// The CloudFormation template metadata key for storing the id of the AWS .NET deployment tool recipe. /// diff --git a/src/AWS.Deploy.Constants/ElasticBeanstalk.cs b/src/AWS.Deploy.Constants/ElasticBeanstalk.cs index b0db84c39..7b4a0a5ab 100644 --- a/src/AWS.Deploy.Constants/ElasticBeanstalk.cs +++ b/src/AWS.Deploy.Constants/ElasticBeanstalk.cs @@ -22,6 +22,14 @@ internal static class ElasticBeanstalk public const string HealthCheckURLOptionNameSpace = "aws:elasticbeanstalk:application"; public const string HealthCheckURLOptionName = "Application Healthcheck URL"; + public const string LinuxPlatformType = ".NET Core"; + public const string WindowsPlatformType = "Windows Server"; + + public const string IISAppPathOptionId = "IISAppPath"; + public const string IISWebSiteOptionId = "IISWebSite"; + + public const string WindowsManifestName = "aws-windows-deployment-manifest.json"; + /// /// This list stores a named tuple of OptionSettingId, OptionSettingNameSpace and OptionSettingName. /// OptionSettingId refers to the Id property for an option setting item in the recipe file. @@ -35,5 +43,19 @@ internal static class ElasticBeanstalk new (ProxyOptionId, ProxyOptionNameSpace, ProxyOptionName), new (HealthCheckURLOptionId, HealthCheckURLOptionNameSpace, HealthCheckURLOptionName) }; + + /// + /// This is the list of option settings available for Windows Beanstalk deployments. + /// This list stores a named tuple of OptionSettingId, OptionSettingNameSpace and OptionSettingName. + /// OptionSettingId refers to the Id property for an option setting item in the recipe file. + /// OptionSettingNameSpace and OptionSettingName provide a way to configure the environments metadata and update its behaviour. + /// A comprehensive list of all configurable settings can be found here + /// + public static List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> WindowsOptionSettingQueryList = new() + { + new (EnhancedHealthReportingOptionId, EnhancedHealthReportingOptionNameSpace, EnhancedHealthReportingOptionName), + new (XRayTracingOptionId, XRayTracingOptionNameSpace, XRayTracingOptionName), + new (HealthCheckURLOptionId, HealthCheckURLOptionNameSpace, HealthCheckURLOptionName) + }; } } diff --git a/src/AWS.Deploy.Constants/RecipeIdentifier.cs b/src/AWS.Deploy.Constants/RecipeIdentifier.cs index 4eb7760b1..b59fc259f 100644 --- a/src/AWS.Deploy.Constants/RecipeIdentifier.cs +++ b/src/AWS.Deploy.Constants/RecipeIdentifier.cs @@ -8,6 +8,7 @@ internal static class RecipeIdentifier { // Recipe IDs public const string EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkEnvironment"; + public const string EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkWindowsEnvironment"; public const string PUSH_TO_ECR_RECIPE_ID = "PushContainerImageEcr"; // Replacement Tokens diff --git a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs index 781858cad..98454fe3b 100644 --- a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs +++ b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs @@ -4,19 +4,30 @@ using System.Collections.Generic; using System.IO; using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Recipes.CDK.Common; using Newtonsoft.Json; namespace AWS.Deploy.Orchestration { - public class CdkAppSettingsSerializer + public interface ICdkAppSettingsSerializer + { + /// + /// Creates the contents for the appsettings.json file inside the CDK project. This file is deserialized into to be used the by the CDK templates. + /// + string Build(CloudApplication cloudApplication, Recommendation recommendation, OrchestratorSession session); + } + + public class CdkAppSettingsSerializer : ICdkAppSettingsSerializer { private readonly IOptionSettingHandler _optionSettingHandler; + private readonly IDirectoryManager _directoryManager; - public CdkAppSettingsSerializer(IOptionSettingHandler optionSettingHandler) + public CdkAppSettingsSerializer(IOptionSettingHandler optionSettingHandler, IDirectoryManager directoryManager) { _optionSettingHandler = optionSettingHandler; + _directoryManager = directoryManager; } public string Build(CloudApplication cloudApplication, Recommendation recommendation, OrchestratorSession session) @@ -33,23 +44,21 @@ public string Build(CloudApplication cloudApplication, Recommendation recommenda recommendation.Recipe.Version, session.AWSAccountId, session.AWSRegion, - new () + settings: _optionSettingHandler.GetOptionSettingsMap(recommendation, session.ProjectDefinition, _directoryManager, OptionSettingsType.Recipe) ) { + // These deployment bundle settings need to be set separately because they are not configurable by the user. + // These settings will not be part of the CloudFormation template metadata. + // The only exception to this is the ECR Repository name. ECRRepositoryName = recommendation.DeploymentBundle.ECRRepositoryName ?? "", ECRImageTag = recommendation.DeploymentBundle.ECRImageTag ?? "", DotnetPublishZipPath = recommendation.DeploymentBundle.DotnetPublishZipPath ?? "", DotnetPublishOutputDirectory = recommendation.DeploymentBundle.DotnetPublishOutputDirectory ?? "" }; - // Option Settings - foreach (var optionSetting in recommendation.Recipe.OptionSettings) - { - var optionSettingValue = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); - - if (optionSettingValue != null) - appSettingsContainer.Settings[optionSetting.Id] = optionSettingValue; - } + // Persist deployment bundle settings + var deploymentBundleSettingsMap = _optionSettingHandler.GetOptionSettingsMap(recommendation, session.ProjectDefinition, _directoryManager, OptionSettingsType.DeploymentBundle); + appSettingsContainer.DeploymentBundleSettings = JsonConvert.SerializeObject(deploymentBundleSettingsMap); return JsonConvert.SerializeObject(appSettingsContainer, Formatting.Indented); } diff --git a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs index af596c38f..6a9af5a78 100644 --- a/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs +++ b/src/AWS.Deploy.Orchestration/CdkProjectHandler.cs @@ -31,7 +31,7 @@ public class CdkProjectHandler : ICdkProjectHandler { private readonly IOrchestratorInteractiveService _interactiveService; private readonly ICommandLineWrapper _commandLineWrapper; - private readonly CdkAppSettingsSerializer _appSettingsBuilder; + private readonly ICdkAppSettingsSerializer _appSettingsBuilder; private readonly IDirectoryManager _directoryManager; private readonly IAWSResourceQueryer _awsResourceQueryer; private readonly IFileManager _fileManager; @@ -42,7 +42,9 @@ public CdkProjectHandler( IOrchestratorInteractiveService interactiveService, ICommandLineWrapper commandLineWrapper, IAWSResourceQueryer awsResourceQueryer, + ICdkAppSettingsSerializer cdkAppSettingsSerializer, IFileManager fileManager, + IDirectoryManager directoryManager, IOptionSettingHandler optionSettingHandler, IDeployToolWorkspaceMetadata workspaceMetadata, ICloudFormationTemplateReader cloudFormationTemplateReader) @@ -50,8 +52,8 @@ public CdkProjectHandler( _interactiveService = interactiveService; _commandLineWrapper = commandLineWrapper; _awsResourceQueryer = awsResourceQueryer; - _appSettingsBuilder = new CdkAppSettingsSerializer(optionSettingHandler); - _directoryManager = new DirectoryManager(); + _appSettingsBuilder = cdkAppSettingsSerializer; + _directoryManager = directoryManager; _fileManager = fileManager; _workspaceMetadata = workspaceMetadata; _cloudFormationTemplateReader = cloudFormationTemplateReader; diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index a54774e4b..1c39acb9e 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -530,11 +530,11 @@ public async Task> GetElasticBeanstalkPlatformArns(params var allPlatformSummaries = new List(); if (platformTypes.Contains(BeanstalkPlatformType.Linux)) { - allPlatformSummaries.AddRange(await fetchPlatforms(".NET Core")); + allPlatformSummaries.AddRange(await fetchPlatforms(Constants.ElasticBeanstalk.LinuxPlatformType)); } - else if (platformTypes.Contains(BeanstalkPlatformType.Windows)) + if (platformTypes.Contains(BeanstalkPlatformType.Windows)) { - var windowsPlatforms = await fetchPlatforms("Windows Server"); + var windowsPlatforms = await fetchPlatforms(Constants.ElasticBeanstalk.WindowsPlatformType); SortElasticBeanstalkWindowsPlatforms(windowsPlatforms); allPlatformSummaries.AddRange(windowsPlatforms); } diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 2c4683e83..94ae57ffd 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -109,7 +109,7 @@ public async Task CreateDotnetPublishZip(Recommendation recommendation) recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild && !additionalArguments.Contains("--runtime ") && !additionalArguments.Contains("-r ") - ? "--runtime linux-x64" + ? $"--runtime {(recommendation.Recipe.TargetPlatform == TargetPlatform.Windows ? "win-x64" : "linux-x64")}" : ""; var publishCommand = $"dotnet publish \"{recommendation.ProjectPath}\"" + diff --git a/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs b/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs index 0bf601426..222b477b2 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentCommands/BeanstalkEnvironmentDeploymentCommand.cs @@ -39,6 +39,12 @@ public async Task ExecuteAsync(Orchestrator orchestrator, CloudApplication cloud orchestrator._interactiveService.LogSectionStart($"Creating application version", "Uploading deployment bundle to S3 and create an Elastic Beanstalk application version"); + // This step is only required for Elastic Beanstalk Windows deployments since a manifest file needs to be created for that deployment. + if (recommendation.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID)) + { + elasticBeanstalkHandler.SetupWindowsDeploymentManifest(recommendation, deploymentPackage); + } + var versionLabel = $"v-{DateTime.Now.Ticks}"; var s3location = await elasticBeanstalkHandler.CreateApplicationStorageLocationAsync(applicationName, versionLabel, deploymentPackage); await s3Handler.UploadToS3Async(s3location.S3Bucket, s3location.S3Key, deploymentPackage); diff --git a/src/AWS.Deploy.Orchestration/DeploymentSettingsHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentSettingsHandler.cs index c4a6c4eee..a4edd95a8 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentSettingsHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentSettingsHandler.cs @@ -11,6 +11,7 @@ using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; +using AWS.Deploy.Orchestration.Utilities; using Newtonsoft.Json; namespace AWS.Deploy.Orchestration @@ -128,35 +129,18 @@ public async Task SaveSettings(SaveSettingsConfiguration saveSettingsConfig, Rec AWSRegion = orchestratorSession.AWSRegion, ApplicationName = recommendation.Recipe.DeploymentType == DeploymentTypes.ElasticContainerRegistryImage ? null : cloudApplication.Name, RecipeId = cloudApplication.RecipeId, - Settings = new Dictionary() + Settings = _optionSettingHandler.GetOptionSettingsMap(recommendation, orchestratorSession.ProjectDefinition, _directoryManager) }; - var optionSettings = recommendation.GetConfigurableOptionSettingItems(); - foreach (var optionSetting in optionSettings) + if (saveSettingsConfig.SettingsType == SaveSettingsType.Modified) { - if (saveSettingsConfig.SettingsType == SaveSettingsType.Modified && !_optionSettingHandler.IsOptionSettingModified(recommendation, optionSetting)) + foreach (var optionSetting in recommendation.GetConfigurableOptionSettingItems()) { - continue; - } - - var id = optionSetting.FullyQualifiedId; - var value = _optionSettingHandler.GetOptionSettingValue(recommendation, optionSetting); - if (optionSetting.TypeHint.HasValue && (optionSetting.TypeHint == OptionSettingTypeHint.FilePath || optionSetting.TypeHint == OptionSettingTypeHint.DockerExecutionDirectory)) - { - var path = value?.ToString(); - if (string.IsNullOrEmpty(path)) + if (!_optionSettingHandler.IsOptionSettingModified(recommendation, optionSetting)) { - continue; + deploymentSettings.Settings.Remove(optionSetting.FullyQualifiedId); } - - // All file paths or directory paths must be persisted relative the the customers .NET project. - // This is a done to ensure that the resolved paths work correctly across all cloned repos. - // The relative path is also canonicalized to work across Unix and Windows OS. - var absolutePath = _directoryManager.GetAbsolutePath(projectDirectory, path); - value = _directoryManager.GetRelativePath(projectDirectory, absolutePath) - .Replace(Path.DirectorySeparatorChar, '/'); } - deploymentSettings.Settings[id] = value; } try diff --git a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs index de2582acc..b38511d27 100644 --- a/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs +++ b/src/AWS.Deploy.Orchestration/OptionSettingHandler.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Extensions; +using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; @@ -423,5 +425,69 @@ public bool IsOptionSettingModified(Recommendation recommendation, OptionSetting } return false; } + + /// + /// Returns a Dictionary containing the configurable option settings for the specified recommendation. The returned dictionary can contain specific types of option settings depending on the value of . + /// The key in the dictionary is the fully qualified ID of each option setting + /// The value in the dictionary is the value of each option setting + /// + public Dictionary GetOptionSettingsMap(Recommendation recommendation, ProjectDefinition projectDefinition, IDirectoryManager directoryManager, OptionSettingsType optionSettingsType = OptionSettingsType.All) + { + var projectDirectory = Path.GetDirectoryName(projectDefinition.ProjectPath); + if (string.IsNullOrEmpty(projectDirectory)) + { + var message = $"Failed to get deployment settings container because {projectDefinition.ProjectPath} is null or empty"; + throw new InvalidOperationException(message); + } + + var settingsContainer = new Dictionary(); + + IEnumerable optionSettingsId; + var recipeOptionSettingsId = recommendation.GetConfigurableOptionSettingItems().Select(x => x.FullyQualifiedId); + var deploymentBundleOptionSettingsId = recommendation.Recipe.DeploymentBundleSettings.Select(x => x.FullyQualifiedId); + + switch (optionSettingsType) + { + case OptionSettingsType.Recipe: + optionSettingsId = recipeOptionSettingsId.Except(deploymentBundleOptionSettingsId); + break; + case OptionSettingsType.DeploymentBundle: + optionSettingsId = deploymentBundleOptionSettingsId; + break; + case OptionSettingsType.All: + optionSettingsId = recipeOptionSettingsId.Union(deploymentBundleOptionSettingsId); + break; + default: + throw new InvalidOperationException($"{nameof(optionSettingsType)} doest not have a valid type"); + } + + foreach (var optionSettingId in optionSettingsId) + { + var optionSetting = GetOptionSetting(recommendation, optionSettingId); + var value = GetOptionSettingValue(recommendation, optionSetting); + if (optionSetting.TypeHint.HasValue && (optionSetting.TypeHint == OptionSettingTypeHint.FilePath || optionSetting.TypeHint == OptionSettingTypeHint.DockerExecutionDirectory)) + { + var path = value?.ToString(); + if (string.IsNullOrEmpty(path)) + { + continue; + } + + // All file paths or directory paths must be persisted relative the the customers .NET project. + // This is a done to ensure that the resolved paths work correctly across all cloned repos. + // The relative path is also canonicalized to work across Unix and Windows OS. + var absolutePath = directoryManager.GetAbsolutePath(projectDirectory, path); + value = directoryManager.GetRelativePath(projectDirectory, absolutePath) + .Replace(Path.DirectorySeparatorChar, '/'); + } + + if (value != null) + { + settingsContainer[optionSetting.FullyQualifiedId] = value; + } + } + + return settingsContainer; + } } } diff --git a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs index d6f429bdf..3189bb556 100644 --- a/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs +++ b/src/AWS.Deploy.Orchestration/ServiceHandlers/AWSElasticBeanstalkHandler.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.IO.Compression; +using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Amazon.ElasticBeanstalk; @@ -11,17 +14,66 @@ using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; +using System.Text.Json.Serialization; namespace AWS.Deploy.Orchestration.ServiceHandlers { public interface IElasticBeanstalkHandler { + /// + /// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries. + /// This method creates the manifest file if it doesn't exist, or it creates a new one. + /// The two main settings that are updated are IIS Website and IIS App Path. + /// + void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath); Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage); Task CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle); Task UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List optionSettings); List GetEnvironmentConfigurationSettings(Recommendation recommendation); } + /// + /// This class represents the structure of the Windows manifest file to be included with Windows Elastic Beanstalk deployments. + /// + public class ElasticBeanstalkWindowsManifest + { + [JsonPropertyName("manifestVersion")] + public int ManifestVersion { get; set; } = 1; + + [JsonPropertyName("deployments")] + public ManifestDeployments Deployments { get; set; } = new(); + + public class ManifestDeployments + { + + [JsonPropertyName("aspNetCoreWeb")] + public List AspNetCoreWeb { get; set; } = new(); + + public class AspNetCoreWebDeployments + { + + [JsonPropertyName("name")] + public string Name { get; set; } = "app"; + + + [JsonPropertyName("parameters")] + public AspNetCoreWebParameters Parameters { get; set; } = new(); + + public class AspNetCoreWebParameters + { + [JsonPropertyName("appBundle")] + public string AppBundle { get; set; } = "."; + + [JsonPropertyName("iisPath")] + public string IISPath { get; set; } = "/"; + + [JsonPropertyName("iisWebSite")] + public string IISWebSite { get; set; } = "Default Web Site"; + } + } + } + } + public class AWSElasticBeanstalkHandler : IElasticBeanstalkHandler { private readonly IAWSClientFactory _awsClientFactory; @@ -37,6 +89,121 @@ public AWSElasticBeanstalkHandler(IAWSClientFactory awsClientFactory, IOrchestra _optionSettingHandler = optionSettingHandler; } + private T GetOrCreateNode(object? json) where T : new() + { + try + { + return JsonSerializer.Deserialize(json?.ToString() ?? string.Empty); + } + catch + { + return new T(); + } + } + + /// + /// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries. + /// This method creates the manifest file if it doesn't exist, or it creates a new one. + /// The two main settings that are updated are IIS Website and IIS App Path. + /// + public void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath) + { + var iisWebSiteOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISWebSiteOptionId); + var iisAppPathOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISAppPathOptionId); + + var iisWebSiteValue = _optionSettingHandler.GetOptionSettingValue(recommendation, iisWebSiteOptionSetting); + var iisAppPathValue = _optionSettingHandler.GetOptionSettingValue(recommendation, iisAppPathOptionSetting); + + var iisWebSite = !string.IsNullOrEmpty(iisWebSiteValue) ? iisWebSiteValue : "Default Web Site"; + var iisAppPath = !string.IsNullOrEmpty(iisAppPathValue) ? iisAppPathValue : "/"; + + var newManifestFile = new ElasticBeanstalkWindowsManifest(); + newManifestFile.Deployments.AspNetCoreWeb.Add(new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments + { + Parameters = new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments.AspNetCoreWebParameters + { + IISPath = iisAppPath, + IISWebSite = iisWebSite + } + }); + + using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update)) + { + var zipEntry = zipArchive.GetEntry(Constants.ElasticBeanstalk.WindowsManifestName); + var serializedManifest = JsonSerializer.Serialize(new Dictionary()); + if (zipEntry != null) + { + using (var streamReader = new StreamReader(zipEntry.Open())) + { + serializedManifest = streamReader.ReadToEnd(); + } + } + + var jsonDoc = GetOrCreateNode>(serializedManifest); + + if (!jsonDoc.ContainsKey("manifestVersion")) + { + jsonDoc["manifestVersion"] = newManifestFile.ManifestVersion; + } + + if (jsonDoc.ContainsKey("deployments")) + { + var deploymentNode = GetOrCreateNode>(jsonDoc["deployments"]); + + if (deploymentNode.ContainsKey("aspNetCoreWeb")) + { + var aspNetCoreWebNode = GetOrCreateNode>(deploymentNode["aspNetCoreWeb"]); + if (aspNetCoreWebNode.Count == 0) + { + aspNetCoreWebNode.Add(newManifestFile.Deployments.AspNetCoreWeb[0]); + } + else + { + // We only need 1 entry in the 'aspNetCoreWeb' node that defines the parameters we are interested in. Typically, only 1 entry exists. + var aspNetCoreWebEntry = GetOrCreateNode>(JsonSerializer.Serialize(aspNetCoreWebNode[0])); + + var nameValue = aspNetCoreWebEntry.ContainsKey("name") ? aspNetCoreWebEntry["name"].ToString() : string.Empty; + aspNetCoreWebEntry["name"] = !string.IsNullOrEmpty(nameValue) ? nameValue : newManifestFile.Deployments.AspNetCoreWeb[0].Name; + + if (aspNetCoreWebEntry.ContainsKey("parameters")) + { + var parametersNode = GetOrCreateNode>(aspNetCoreWebEntry["parameters"]); + parametersNode["appBundle"] = "."; + parametersNode["iisPath"] = iisAppPath; + parametersNode["iisWebSite"] = iisWebSite; + + aspNetCoreWebEntry["parameters"] = parametersNode; + } + else + { + aspNetCoreWebEntry["parameters"] = newManifestFile.Deployments.AspNetCoreWeb[0].Parameters; + } + aspNetCoreWebNode[0] = aspNetCoreWebEntry; + } + deploymentNode["aspNetCoreWeb"] = aspNetCoreWebNode; + } + else + { + deploymentNode["aspNetCoreWeb"] = newManifestFile.Deployments.AspNetCoreWeb; + } + + jsonDoc["deployments"] = deploymentNode; + } + else + { + jsonDoc["deployments"] = newManifestFile.Deployments; + } + + using (var jsonStream = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(jsonDoc, new JsonSerializerOptions { WriteIndented = true }))) + { + zipEntry ??= zipArchive.CreateEntry(Constants.ElasticBeanstalk.WindowsManifestName); + using var zipEntryStream = zipEntry.Open(); + jsonStream.Position = 0; + jsonStream.CopyTo(zipEntryStream); + } + } + } + public async Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage) { string bucketName; @@ -83,7 +250,20 @@ public List GetEnvironmentConfigurationSettings(Reco { var additionalSettings = new List(); - foreach (var tuple in Constants.ElasticBeanstalk.OptionSettingQueryList) + List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> tupleList; + switch (recommendation.Recipe.Id) + { + case Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID: + tupleList = Constants.ElasticBeanstalk.OptionSettingQueryList; + break; + case Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID: + tupleList = Constants.ElasticBeanstalk.WindowsOptionSettingQueryList; + break; + default: + throw new InvalidOperationException($"The recipe '{recommendation.Recipe.Id}' is not supported."); + }; + + foreach (var tuple in tupleList) { var optionSetting = _optionSettingHandler.GetOptionSetting(recommendation, tuple.OptionSettingId); diff --git a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs index 1364e19ce..9d25e4d3e 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/DeployedApplicationQueryer.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Amazon.CloudFormation; using Amazon.ElasticBeanstalk; @@ -14,6 +16,7 @@ using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.LocalUserSettings; +using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.Orchestration.Utilities { @@ -37,7 +40,7 @@ public interface IDeployedApplicationQueryer /// /// Gets the current option settings associated with the cloud application. This method is only used for non-CloudFormation based cloud applications. /// - Task> GetPreviousSettings(CloudApplication application); + Task> GetPreviousSettings(CloudApplication application, Recommendation recommendation); } public class DeployedApplicationQueryer : IDeployedApplicationQueryer @@ -45,15 +48,18 @@ public class DeployedApplicationQueryer : IDeployedApplicationQueryer private readonly IAWSResourceQueryer _awsResourceQueryer; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly IOrchestratorInteractiveService _orchestratorInteractiveService; + private readonly IFileManager _fileManager; public DeployedApplicationQueryer( IAWSResourceQueryer awsResourceQueryer, ILocalUserSettingsEngine localUserSettingsEngine, - IOrchestratorInteractiveService orchestratorInteractiveService) + IOrchestratorInteractiveService orchestratorInteractiveService, + IFileManager fileManager) { _awsResourceQueryer = awsResourceQueryer; _localUserSettingsEngine = localUserSettingsEngine; _orchestratorInteractiveService = orchestratorInteractiveService; + _fileManager = fileManager; } public async Task> GetExistingDeployedApplications(List deploymentTypes) @@ -139,13 +145,13 @@ public bool IsCompatible(CloudApplication application, Recommendation recommenda /// /// Gets the current option settings associated with the cloud application.This method is only used for non-CloudFormation based cloud applications. /// - public async Task> GetPreviousSettings(CloudApplication application) + public async Task> GetPreviousSettings(CloudApplication application, Recommendation recommendation) { IDictionary previousSettings; switch (application.ResourceType) { case CloudApplicationResourceType.BeanstalkEnvironment: - previousSettings = await GetBeanstalkEnvironmentConfigurationSettings(application.Name); + previousSettings = await GetBeanstalkEnvironmentConfigurationSettings(application.Name, recommendation.Recipe.Id, recommendation.ProjectPath); break; default: throw new InvalidOperationException($"Cannot fetch existing option settings for the following {nameof(CloudApplicationResourceType)}: {application.ResourceType}"); @@ -212,7 +218,8 @@ private async Task> GetExistingBeanstalkEnvironments() if (!environments.Any()) return validEnvironments; - var dotnetPlatformArns = (await _awsResourceQueryer.GetElasticBeanstalkPlatformArns()).Select(x => x.PlatformArn).ToList(); + var dotnetPlatforms = await _awsResourceQueryer.GetElasticBeanstalkPlatformArns(); + var dotnetPlatformArns = dotnetPlatforms.Select(x => x.PlatformArn).ToList(); // only select environments that have a dotnet specific platform ARN. environments = environments.Where(x => x.Status == EnvironmentStatus.Ready && dotnetPlatformArns.Contains(x.PlatformArn)).ToList(); @@ -225,18 +232,34 @@ private async Task> GetExistingBeanstalkEnvironments() if (tags.Any(x => string.Equals(x.Key, Constants.CloudFormationIdentifier.STACK_TAG))) continue; - validEnvironments.Add(new CloudApplication(env.EnvironmentName, env.EnvironmentId, CloudApplicationResourceType.BeanstalkEnvironment, Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID, env.DateUpdated)); + var recipeId = env.PlatformArn.Contains(Constants.ElasticBeanstalk.LinuxPlatformType) ? + Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID : + Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID; + validEnvironments.Add(new CloudApplication(env.EnvironmentName, env.EnvironmentId, CloudApplicationResourceType.BeanstalkEnvironment, recipeId, env.DateUpdated)); } return validEnvironments; } - private async Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName) + private async Task> GetBeanstalkEnvironmentConfigurationSettings(string environmentName, string recipeId, string projectPath) { IDictionary optionSettings = new Dictionary(); var configurationSettings = await _awsResourceQueryer.GetBeanstalkEnvironmentConfigurationSettings(environmentName); - foreach (var tuple in Constants.ElasticBeanstalk.OptionSettingQueryList) + List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> tupleList; + switch (recipeId) + { + case Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID: + tupleList = Constants.ElasticBeanstalk.OptionSettingQueryList; + break; + case Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID: + tupleList = Constants.ElasticBeanstalk.WindowsOptionSettingQueryList; + break; + default: + throw new InvalidOperationException($"The recipe '{recipeId}' is not supported."); + } + + foreach (var tuple in tupleList) { var configurationSetting = GetBeanstalkEnvironmentConfigurationSetting(configurationSettings, tuple.OptionSettingNameSpace, tuple.OptionSettingName); @@ -246,6 +269,20 @@ private async Task> GetBeanstalkEnvironmentConfigura optionSettings[tuple.OptionSettingId] = configurationSetting.Value; } + if (recipeId.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID)) + { + var manifestPath = Path.Combine(Path.GetDirectoryName(projectPath) ?? string.Empty, Constants.ElasticBeanstalk.WindowsManifestName); + if (_fileManager.Exists(manifestPath)) + { + var manifest = JsonSerializer.Deserialize(await _fileManager.ReadAllTextAsync(manifestPath)); + if (manifest.Deployments.AspNetCoreWeb.Count != 0) + { + optionSettings[Constants.ElasticBeanstalk.IISWebSiteOptionId] = manifest.Deployments.AspNetCoreWeb[0].Parameters.IISWebSite; + optionSettings[Constants.ElasticBeanstalk.IISAppPathOptionId] = manifest.Deployments.AspNetCoreWeb[0].Parameters.IISPath; + } + } + } + return optionSettings; } diff --git a/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs b/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs index dcb719e15..87abcbd31 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/Helpers.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; namespace AWS.Deploy.Orchestration.Utilities { @@ -94,7 +96,7 @@ public static string GetDeployToolWorkspaceDirectoryRoot(string userProfilePath, /// Absolute or relative JSON file path where the deployment settings will be saved. Only the settings modified by the user are persisted. /// Absolute or relative JSON file path where the deployment settings will be saved. All deployment settings are persisted. /// Absolute path to the user's .NET project directory - /// + /// /// public static SaveSettingsConfiguration GetSaveSettingsConfiguration(string? saveSettingsPath, string? saveAllSettingsPath, string projectDirectoryPath, IFileManager fileManager) { diff --git a/src/AWS.Deploy.Orchestration/Utilities/TemplateMetadataReader.cs b/src/AWS.Deploy.Orchestration/Utilities/TemplateMetadataReader.cs index e05d7e3a9..1673f06b5 100644 --- a/src/AWS.Deploy.Orchestration/Utilities/TemplateMetadataReader.cs +++ b/src/AWS.Deploy.Orchestration/Utilities/TemplateMetadataReader.cs @@ -114,6 +114,12 @@ private static CloudApplicationMetadata ReadSettingsFromJSONCFTemplate(string te var jsonString = cfTemplate.Metadata[Constants.CloudFormationIdentifier.STACK_METADATA_SETTINGS]; cloudApplicationMetadata.Settings = JsonConvert.DeserializeObject>(jsonString ?? "") ?? new Dictionary(); + if (cfTemplate.Metadata.ContainsKey(Constants.CloudFormationIdentifier.STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS)) + { + jsonString = cfTemplate.Metadata[Constants.CloudFormationIdentifier.STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS]; + cloudApplicationMetadata.DeploymentBundleSettings = JsonConvert.DeserializeObject>(jsonString ?? "") ?? new Dictionary(); + } + return cloudApplicationMetadata; } catch (Exception e) @@ -148,6 +154,12 @@ private static CloudApplicationMetadata ReadSettingsFromYAMLCFTemplate(string te var jsonString = ((YamlScalarNode)metadataNode.Children[new YamlScalarNode(Constants.CloudFormationIdentifier.STACK_METADATA_SETTINGS)]).Value; cloudApplicationMetadata.Settings = JsonConvert.DeserializeObject>(jsonString ?? "") ?? new Dictionary(); + if (metadataNode.Children.ContainsKey(Constants.CloudFormationIdentifier.STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS)) + { + jsonString = ((YamlScalarNode)metadataNode.Children[new YamlScalarNode(Constants.CloudFormationIdentifier.STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS)]).Value; + cloudApplicationMetadata.DeploymentBundleSettings = JsonConvert.DeserializeObject>(jsonString ?? "") ?? new Dictionary(); + } + return cloudApplicationMetadata; } catch(Exception e) diff --git a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj index edc7081f7..3a4461ab4 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj +++ b/src/AWS.Deploy.Recipes.CDK.Common/AWS.Deploy.Recipes.CDK.Common.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs index c8ad60f56..86bd57c3a 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/CDKRecipeSetup.cs @@ -28,7 +28,7 @@ public static void RegisterStack(Stack stack, IRecipeProps recipeConfigura stack.Tags.SetTag(Constants.CloudFormationIdentifier.STACK_TAG, $"{recipeConfiguration.RecipeId}"); // Serializes all AWS .NET deployment tool settings. - var json = JsonSerializer.Serialize( + var recipeSettingsJson = JsonSerializer.Serialize( recipeConfiguration.Settings, new JsonSerializerOptions { @@ -47,10 +47,16 @@ public static void RegisterStack(Stack stack, IRecipeProps recipeConfigura } // Save the settings, recipe id and version as metadata to the CloudFormation template. - metadata[Constants.CloudFormationIdentifier.STACK_METADATA_SETTINGS] = json; + metadata[Constants.CloudFormationIdentifier.STACK_METADATA_SETTINGS] = recipeSettingsJson; metadata[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_ID] = recipeConfiguration.RecipeId; metadata[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_VERSION] = recipeConfiguration.RecipeVersion; + // Save the deployment bundle settings. + if (!string.IsNullOrEmpty(recipeConfiguration.DeploymentBundleSettings)) + { + metadata[Constants.CloudFormationIdentifier.STACK_METADATA_DEPLOYMENT_BUNDLE_SETTINGS] = recipeConfiguration.DeploymentBundleSettings; + } + // For the CDK to pick up the changes to the metadata .NET Dictionary you have to reassign the Metadata property. stack.TemplateOptions.Metadata = metadata; diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs index 1b02f26ae..76f4c0d39 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs @@ -14,57 +14,62 @@ public interface IRecipeProps /// /// The name of the CloudFormation stack /// - public string StackName { get; set; } + string StackName { get; set; } /// /// The path to the .NET project to deploy to AWS. /// - public string ProjectPath { get; set; } + string ProjectPath { get; set; } /// /// The ECR Repository Name where the docker image will be pushed to. /// - public string? ECRRepositoryName { get; set; } + string? ECRRepositoryName { get; set; } /// /// The ECR Image Tag of the docker image. /// - public string? ECRImageTag { get; set; } + string? ECRImageTag { get; set; } /// /// The path of the zip file containing the assemblies produced by the dotnet publish command. /// - public string? DotnetPublishZipPath { get; set; } + string? DotnetPublishZipPath { get; set; } /// /// The directory containing the assemblies produced by the dotnet publish command. /// - public string? DotnetPublishOutputDirectory { get; set; } + string? DotnetPublishOutputDirectory { get; set; } /// /// The ID of the recipe being used to deploy the application. /// - public string RecipeId { get; set; } + string RecipeId { get; set; } /// /// The version of the recipe being used to deploy the application. /// - public string RecipeVersion { get; set; } + string RecipeVersion { get; set; } /// /// The configured settings made by the frontend. These are recipe specific and defined in the recipe's definition. /// - public T Settings { get; set; } + T Settings { get; set; } + + /// + /// These option settings are part of the deployment bundle definition + /// + string? DeploymentBundleSettings { get; set; } /// /// The Region used during deployment. /// - public string? AWSRegion { get; set; } + string? AWSRegion { get; set; } /// /// The account ID used during deployment. /// - public string? AWSAccountId { get; set; } + string? AWSAccountId { get; set; } } /// @@ -118,6 +123,11 @@ public class RecipeProps : IRecipeProps /// public T Settings { get; set; } + /// + /// These option settings are part of the deployment bundle definition + /// + public string? DeploymentBundleSettings { get; set; } + /// /// The Region used during deployment. /// @@ -147,7 +157,7 @@ public RecipeProps(string stackName, string projectPath, string recipeId, string RecipeVersion = recipeVersion; AWSAccountId = awsAccountId; AWSRegion = awsRegion; - Settings = settings; + Settings = settings; } } } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/AspNetAppAppRunner.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/AspNetAppAppRunner.csproj index 3ab794f6c..2f549ab79 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/AspNetAppAppRunner.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppAppRunner/AspNetAppAppRunner.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj index e0005ad3a..7cbccadd4 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/AspNetAppEcsFargate.csproj @@ -25,7 +25,7 @@ - + - + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj index c1dccd5ce..59592c16c 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppElasticBeanstalkWindows/AspNetAppElasticBeanstalkWindows.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj index d66ed2957..8bb4c3e96 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj +++ b/src/AWS.Deploy.Recipes/CdkTemplates/BlazorWasm/BlazorWasm.csproj @@ -25,7 +25,7 @@ - + - + - +