From 7bcb10e3d55b5b546f12a5293fac269925cf5f1f Mon Sep 17 00:00:00 2001 From: aws-sdk-dotnet-automation Date: Thu, 8 Sep 2022 20:54:38 +0000 Subject: [PATCH 1/9] build: version bump to 1.4 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2f931b9b7..0429ca732 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": "1.3", + "version": "1.4", "publicReleaseRefSpec": [ ".*" ], From 8e956acfbe12cc8d0043115e5a46f471fc67ce7f Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Thu, 15 Sep 2022 09:24:47 -0400 Subject: [PATCH 2/9] ci: increase CodeBuild job timeout to account for increased integ tests runtime --- buildtools/ci.template.yml | 1 + 1 file changed, 1 insertion(+) 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 From f2123e54cc36eacf4733ad5203f330c0ca0a1727 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria Date: Wed, 14 Sep 2022 14:05:04 -0700 Subject: [PATCH 3/9] fix: Persist deployment bundle settings across deployments --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 7 +- .../CustomServiceCollectionExtension.cs | 1 + .../Controllers/DeploymentController.cs | 9 +- .../CloudApplicationMetadata.cs | 5 ++ .../Recipes/IOptionSettingHandler.cs | 8 ++ .../Recipes/OptionSettingsType.cs | 30 +++++++ .../CloudFormationIdentifier.cs | 7 +- .../CdkAppSettingsSerializer.cs | 31 ++++--- .../CdkProjectHandler.cs | 8 +- .../DeploymentSettingsHandler.cs | 28 ++---- .../OptionSettingHandler.cs | 66 ++++++++++++++ .../Utilities/Helpers.cs | 4 +- .../Utilities/TemplateMetadataReader.cs | 12 +++ .../CDKRecipeSetup.cs | 10 ++- .../RecipeProps.cs | 34 +++++--- .../GetOptionSettingsMapTests.cs | 87 +++++++++++++++++++ .../TemplateMetadataReaderTests.cs | 8 ++ .../TestFiles/ReadJsonTemplateMetadata.json | 1 + .../TestFiles/ReadYamlTemplateMetadata.yml | 1 + .../CDK/CDKProjectHandlerTests.cs | 40 ++++----- 20 files changed, 321 insertions(+), 76 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/OptionSettingsType.cs create mode 100644 test/AWS.Deploy.CLI.UnitTests/GetOptionSettingsMapTests.cs diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index 2a195f7c8..eba65c7c4 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); + } 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..9247a3efe 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); + } 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.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.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/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/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/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/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingsMapTests.cs b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingsMapTests.cs new file mode 100644 index 000000000..4a20dac53 --- /dev/null +++ b/test/AWS.Deploy.CLI.UnitTests/GetOptionSettingsMapTests.cs @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.CLI.UnitTests.Utilities; +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.UnitTests +{ + public class GetOptionSettingsMapTests + { + private readonly IOptionSettingHandler _optionSettingHandler; + private readonly Mock _serviceProvider; + private readonly IDirectoryManager _directoryManager; + private readonly IFileManager _fileManager; + private readonly IProjectDefinitionParser _projectDefinitionParser; + + public GetOptionSettingsMapTests() + { + _serviceProvider = new Mock(); + _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); + _directoryManager = new DirectoryManager(); + _fileManager = new FileManager(); + _projectDefinitionParser = new ProjectDefinitionParser(_fileManager, _directoryManager); + } + + [Fact] + public async Task GetOptionSettingsMap() + { + // ARRANGE - select recommendation + var engine = await HelperFunctions.BuildRecommendationEngine( + "WebAppWithDockerFile", + _fileManager, + _directoryManager, + "us-west-2", + "123456789012", + "default" + ); + var recommendations = await engine.ComputeRecommendations(); + var selectedRecommendation = recommendations.FirstOrDefault(x => string.Equals(x.Recipe.Id, "AspNetAppAppRunner")); + + // ARRANGE - get project definition + var projectPath = SystemIOUtilities.ResolvePath("WebAppWithDockerFile"); + var projectDefinition = await _projectDefinitionParser.Parse(projectPath); + + // ARRANGE - Modify option setting items + await _optionSettingHandler.SetOptionSettingValue(selectedRecommendation, "ServiceName", "MyAppRunnerService", true); + await _optionSettingHandler.SetOptionSettingValue(selectedRecommendation, "Port", "100", true); + await _optionSettingHandler.SetOptionSettingValue(selectedRecommendation, "ECRRepositoryName", "my-ecr-repository", true); + await _optionSettingHandler.SetOptionSettingValue(selectedRecommendation, "DockerfilePath", Path.Combine(projectPath, "Dockerfile"), true); + await _optionSettingHandler.SetOptionSettingValue(selectedRecommendation, "DockerExecutionDirectory", projectPath, true); + + // ACT and ASSERT - OptionSettingType.All + var container = _optionSettingHandler.GetOptionSettingsMap(selectedRecommendation, projectDefinition, _directoryManager); + Assert.Equal("MyAppRunnerService", container["ServiceName"]); + Assert.Equal(100, container["Port"]); + Assert.Equal("my-ecr-repository", container["ECRRepositoryName"]); + Assert.Equal("Dockerfile", container["DockerfilePath"]); // path relative to projectPath + Assert.Equal(".", container["DockerExecutionDirectory"]); // path relative to projectPath + + // ACT and ASSERT - OptionSettingType.Recipe + container = _optionSettingHandler.GetOptionSettingsMap(selectedRecommendation, projectDefinition, _directoryManager, OptionSettingsType.Recipe); + Assert.Equal("MyAppRunnerService", container["ServiceName"]); + Assert.Equal(100, container["Port"]); + Assert.False(container.ContainsKey("Dockerfile")); + Assert.False(container.ContainsKey("DockerExecutionDirectory")); + Assert.False(container.ContainsKey("ECRRepositoryName")); + + // ACT and ASSERT - OptionSettingType.DeploymentBundle + container = _optionSettingHandler.GetOptionSettingsMap(selectedRecommendation, projectDefinition, _directoryManager, OptionSettingsType.DeploymentBundle); + Assert.Equal("my-ecr-repository", container["ECRRepositoryName"]); + Assert.Equal("Dockerfile", container["DockerfilePath"]); // path relative to projectPath + Assert.Equal(".", container["DockerExecutionDirectory"]); // path relative to projectPath + Assert.False(container.ContainsKey("ServiceName")); + Assert.False(container.ContainsKey("Port")); + } + } +} diff --git a/test/AWS.Deploy.CLI.UnitTests/TemplateMetadataReaderTests.cs b/test/AWS.Deploy.CLI.UnitTests/TemplateMetadataReaderTests.cs index 7e9affbfb..1fda2dc3c 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TemplateMetadataReaderTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/TemplateMetadataReaderTests.cs @@ -53,6 +53,10 @@ public async Task ReadJSONMetadata() var applicationIAMRole = JsonConvert.DeserializeObject(metadata.Settings["ApplicationIAMRole"].ToString()); Assert.True(applicationIAMRole.CreateNew); + + Assert.Equal("Dockerfile", metadata.DeploymentBundleSettings["DockerfilePath"]); + Assert.Equal(".", metadata.DeploymentBundleSettings["DockerExecutionDirectory"]); + Assert.Equal("webappwithdockerfile", metadata.DeploymentBundleSettings["ECRRepositoryName"]); } [Fact] @@ -76,6 +80,10 @@ public async Task ReadYamlMetadata() // ASSERT Assert.Equal("aws-elasticbeanstalk-role", metadata.Settings["ApplicationIAMRole"].ToString()); + + Assert.Equal("Dockerfile", metadata.DeploymentBundleSettings["DockerfilePath"]); + Assert.Equal(".", metadata.DeploymentBundleSettings["DockerExecutionDirectory"]); + Assert.Equal("webappwithdockerfile", metadata.DeploymentBundleSettings["ECRRepositoryName"]); } } } diff --git a/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadJsonTemplateMetadata.json b/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadJsonTemplateMetadata.json index d06bfd264..511832b53 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadJsonTemplateMetadata.json +++ b/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadJsonTemplateMetadata.json @@ -2,6 +2,7 @@ "Description": "AWSDotnetDeployCDKStack", "Metadata": { "aws-dotnet-deploy-settings": "{\"ApplicationIAMRole\":{\"CreateNew\":true,\"RoleArn\":null},\"EnvironmentType\":\"SingleInstance\",\"InstanceType\":\"\",\"BeanstalkEnvironment\":{\"CreateNew\":true,\"EnvironmentName\":\"WebApp1-dev\"},\"BeanstalkApplication\":{\"CreateNew\":true,\"ApplicationName\":\"WebApp1\"},\"ElasticBeanstalkPlatformArn\":\"arn:aws:elasticbeanstalk:us-west-2::platform/.NET Core running on 64bit Amazon Linux 2/2.2.10\",\"LoadBalancerType\":\"application\",\"EC2KeyPair\":\"\",\"ElasticBeanstalkManagedPlatformUpdates\":{\"ManagedActionsEnabled\":true,\"PreferredStartTime\":\"Sun:00:00\",\"UpdateLevel\":\"minor\"},\"XRayTracingSupportEnabled\":false,\"ReverseProxy\":\"nginx\",\"EnhancedHealthReporting\":\"enhanced\",\"HealthCheckURL\":\"/\",\"ElasticBeanstalkRollingUpdates\":{\"RollingUpdatesEnabled\":false,\"RollingUpdateType\":\"Time\",\"MaxBatchSize\":null,\"MinInstancesInService\":null,\"PauseTime\":null,\"Timeout\":\"PT30M\"},\"CNamePrefix\":\"\",\"ElasticBeanstalkEnvironmentVariables\":{}}", + "aws-dotnet-deploy-deployment-bundle-settings": "{\"DockerBuildArgs\":\"\",\"DockerfilePath\":\"Dockerfile\",\"DockerExecutionDirectory\":\".\",\"ECRRepositoryName\":\"webappwithdockerfile\"}", "aws-dotnet-deploy-recipe-id": "AspNetAppElasticBeanstalkLinux", "aws-dotnet-deploy-recipe-version": "0.1.0" }, diff --git a/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadYamlTemplateMetadata.yml b/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadYamlTemplateMetadata.yml index 797c8f0e7..a5b792a40 100644 --- a/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadYamlTemplateMetadata.yml +++ b/test/AWS.Deploy.CLI.UnitTests/TestFiles/ReadYamlTemplateMetadata.yml @@ -1,6 +1,7 @@ Description: AWSDotnetDeployCDKStack Metadata: aws-dotnet-deploy-settings: '{"ApplicationIAMRole":"aws-elasticbeanstalk-role","EnvironmentType":"SingleInstance","InstanceType":"t2.micro","EnvironmentName":"BeanstalkTest2-dev","ApplicationName":"BeanstalkTest2","SolutionStackName":"64bit Amazon Linux 2 v2.1.2 running .NET Core","LoadBalancerType":"application","UseExistingApplication":false,"EC2KeyPair":""}' + aws-dotnet-deploy-deployment-bundle-settings: '{"DockerBuildArgs": "","DockerfilePath": "Dockerfile","DockerExecutionDirectory": ".","ECRRepositoryName": "webappwithdockerfile"}' aws-dotnet-deploy-recipe-id: AspNetAppElasticBeanstalkLinux aws-dotnet-deploy-recipe-version: 0.1.0 Parameters: diff --git a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs index 36d0711ed..b5ac279bf 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/CDK/CDKProjectHandlerTests.cs @@ -23,14 +23,18 @@ public class CDKProjectHandlerTests { private readonly IOptionSettingHandler _optionSettingHandler; private readonly Mock _awsResourceQueryer; + private readonly Mock _cdkAppSettingsSerializer; private readonly Mock _serviceProvider; private readonly Mock _workspaceMetadata; private readonly Mock _cloudFormationTemplateReader; + private readonly Mock _fileManager; + private readonly Mock _directoryManager; private readonly string _cdkBootstrapTemplate; public CDKProjectHandlerTests() { _awsResourceQueryer = new Mock(); + _cdkAppSettingsSerializer = new Mock(); _serviceProvider = new Mock(); _serviceProvider .Setup(x => x.GetService(typeof(IAWSResourceQueryer))) @@ -38,6 +42,8 @@ public CDKProjectHandlerTests() _optionSettingHandler = new OptionSettingHandler(new ValidatorFactory(_serviceProvider.Object)); _workspaceMetadata = new Mock(); _cloudFormationTemplateReader = new Mock(); + _fileManager = new Mock(); + _directoryManager = new Mock(); var templateIdentifier = "AWS.Deploy.Orchestration.CDK.CDKBootstrapTemplate.yaml"; _cdkBootstrapTemplate = typeof(CdkProjectHandler).Assembly.ReadEmbeddedFile(templateIdentifier); @@ -48,13 +54,11 @@ public async Task CheckCDKBootstrap_DoesNotExist() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var awsResourceQuery = new Mock(); awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(null)); - - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -64,13 +68,11 @@ public async Task CheckCDKBootstrap_NoCFParameter() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var awsResourceQuery = new Mock(); awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult(new Stack { Parameters = new List() })); - - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -80,14 +82,12 @@ public async Task CheckCDKBootstrap_NoSSMParameter() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var awsResourceQuery = new Mock(); awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); - - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, _cloudFormationTemplateReader.Object); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -97,7 +97,6 @@ public async Task CheckCDKBootstrap_SSMParameterOld() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var deployToolWorkspaceMetadata = new Mock(); var awsClientFactory = new Mock(); @@ -105,12 +104,11 @@ public async Task CheckCDKBootstrap_SSMParameterOld() awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); - fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); - var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, fileManager.Object); + _fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); + var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, _fileManager.Object); awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("1")); - - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); Assert.True(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -120,7 +118,6 @@ public async Task CheckCDKBootstrap_SSMParameterNewer() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var deployToolWorkspaceMetadata = new Mock(); var awsClientFactory = new Mock(); @@ -128,12 +125,12 @@ public async Task CheckCDKBootstrap_SSMParameterNewer() awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); - fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); - var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, fileManager.Object); + _fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); + var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, _fileManager.Object); awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult("100")); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } @@ -143,7 +140,6 @@ public async Task CheckCDKBootstrap_SSMParameterSame() { var interactiveService = new Mock(); var commandLineWrapper = new Mock(); - var fileManager = new Mock(); var deployToolWorkspaceMetadata = new Mock(); var awsClientFactory = new Mock(); @@ -151,12 +147,12 @@ public async Task CheckCDKBootstrap_SSMParameterSame() awsResourceQuery.Setup(x => x.GetCloudFormationStack(It.IsAny())).Returns(Task.FromResult( new Stack { Parameters = new List() { new Parameter { ParameterKey = "Qualifier", ParameterValue = "q1" } } })); - fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); - var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, fileManager.Object); + _fileManager.Setup(x => x.ReadAllTextAsync(It.IsAny())).ReturnsAsync(_cdkBootstrapTemplate); + var cloudFormationTemplateReader = new CloudFormationTemplateReader(awsClientFactory.Object, deployToolWorkspaceMetadata.Object, _fileManager.Object); var templateVersion = await cloudFormationTemplateReader.ReadCDKTemplateVersion(); awsResourceQuery.Setup(x => x.GetParameterStoreTextValue(It.IsAny())).Returns(Task.FromResult(templateVersion.ToString())); - var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, fileManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); + var cdkProjectHandler = new CdkProjectHandler(interactiveService.Object, commandLineWrapper.Object, awsResourceQuery.Object, _cdkAppSettingsSerializer.Object, _fileManager.Object, _directoryManager.Object, _optionSettingHandler, _workspaceMetadata.Object, cloudFormationTemplateReader); Assert.False(await cdkProjectHandler.DetermineIfCDKBootstrapShouldRun()); } From e292ad9af3e5ca903d5806244ca766c23ac3762a Mon Sep 17 00:00:00 2001 From: Philippe El Asmar <53088140+philasmar@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:10:27 +0000 Subject: [PATCH 4/9] ci: increase codebuild ci role assume duration --- .github/workflows/codebuild-ci.yml | 1 + 1 file changed, 1 insertion(+) 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 From e0b6faa07e60f5834ee8456d1434cb6fd91ddbaa Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Thu, 22 Sep 2022 19:09:00 -0400 Subject: [PATCH 5/9] feat: Enable toggling whether the Application Load Balancer should be internet-facing or internal for the ECS Fargate recipe. --- .../LoadBalancerConfiguration.cs | 4 ++ .../AspNetAppEcsFargate/Generated/Recipe.cs | 2 +- .../ASP.NETAppECSFargate.recipe | 11 +++- .../ServerMode/GetApplyOptionSettings.cs | 66 +++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs index 1e5d8f54e..eb8ad12e2 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Configurations/LoadBalancerConfiguration.cs @@ -68,6 +68,10 @@ public enum ListenerConditionTypeEnum { None, Path} /// public double ListenerConditionPriority { get; set; } = 100; + /// + /// Whether the load balancer has an internet-routable address. + /// + public bool InternetFacing { get; set; } = true; /// A parameterless constructor is needed for /// or the classes will fail to initialize. diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs index c075b12c6..26c5fff44 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs @@ -206,7 +206,7 @@ private void ConfigureLoadBalancer(Configuration settings) ServiceLoadBalancer = new ApplicationLoadBalancer(this, nameof(ServiceLoadBalancer), InvokeCustomizeCDKPropsEvent(nameof(ServiceLoadBalancer), this, new ApplicationLoadBalancerProps { Vpc = AppVpc, - InternetFacing = true + InternetFacing = settings.LoadBalancer.InternetFacing })); LoadBalancerListener = ServiceLoadBalancer.AddListener(nameof(LoadBalancerListener), InvokeCustomizeCDKPropsEvent(nameof(LoadBalancerListener), this, new ApplicationListenerProps diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index a429c95c3..8383735f5 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppEcsFargate", - "Version": "1.0.1", + "Version": "1.1.0", "Name": "ASP.NET Core App to Amazon ECS using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -702,6 +702,15 @@ "Value": "Path" } ] + }, + { + "Id": "InternetFacing", + "Name": "Internet-Facing", + "Description": "Should the load balancer have an internet-routable address? Internet-facing load balancers can route requests from clients over the internet. Internal load balancers can route requests only from clients with access to the VPC.", + "Type": "Bool", + "DefaultValue": true, + "AdvancedSetting": false, + "Updatable": false } ] }, diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs index 11063eabc..2d1cf1d19 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/GetApplyOptionSettings.cs @@ -24,6 +24,7 @@ using AWS.Deploy.CLI.IntegrationTests.Utilities; using AWS.Deploy.Orchestration; using AWS.Deploy.Common.IO; +using Newtonsoft.Json.Linq; namespace AWS.Deploy.CLI.IntegrationTests.ServerMode { @@ -321,6 +322,71 @@ public async Task GetConfigSettingResources_VpcConnectorOptions() } } + /// + /// Tests that the LoadBalancer.InternetFacing option setting correctly toggles the + /// load balancer scheme. + /// + /// desired LoadBalancer.InternetFacing option setting value + /// Expected load balancer scheme in the generated CloudFormation template + [Theory] + [InlineData("true", "internet-facing")] + [InlineData("false", "internal")] + public async Task GetAndApplyECSFargateSettings_LoadBalancerSchemeConfig(string internetFacingValue, string expectedLoadBalancerScheme) + { + _stackName = $"ServerModeWebECSFargate{Guid.NewGuid().ToString().Split('-').Last()}"; + + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var portNumber = 4024; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); + + // Running `cdk diff` to assert against the generated CloudFormation template + // for this recipe takes longer than the default timeout + httpClient.Timeout = new TimeSpan(0, 0, 120); + + 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 recommendation = await restClient.GetRecommendationsAndSetDeploymentTarget(sessionId, "AspNetAppEcsFargate", _stackName); + + var response = await restClient.ApplyConfigSettingsAsync(sessionId, new ApplyConfigSettingsInput() + { + UpdatedSettings = new Dictionary() + { + {"LoadBalancer.InternetFacing", internetFacingValue} + } + }); + + var generateCloudFormationTemplateResponse = await restClient.GenerateCloudFormationTemplateAsync(sessionId); + var cloudFormationTemplate = JObject.Parse(generateCloudFormationTemplateResponse.CloudFormationTemplate); + + // This should find the AWS::ElasticLoadBalancingV2::LoadBalancer resource in the CloudFormation JSON + // based on its "Scheme" property, which is what "LoadBalancer.InternetFacing" ultimately drives. + // If multiple resources end up with a Scheme property or the LoadBalancer is missing, + // this test should fail because .Single() will throw an exception. + var loadBalancerSchemeValue = cloudFormationTemplate.SelectTokens("Resources.*.Properties.Scheme").Single(); + + Assert.Equal(expectedLoadBalancerScheme, loadBalancerSchemeValue.ToString()); + } + finally + { + cancelSource.Cancel(); + _stackName = null; + } + } + public void Dispose() { Dispose(true); From 83795bf59351d16ae53e81925366f45f032595a3 Mon Sep 17 00:00:00 2001 From: Alex Shovlin Date: Mon, 26 Sep 2022 17:23:40 -0400 Subject: [PATCH 6/9] docs: Add troubleshooting guide entry for App Runner Failed with Resource handler returned message: "null" --- site/content/troubleshooting-guide/index.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 482f60a23083c3b29db3e855d50bdeec7aa87045 Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Tue, 27 Sep 2022 12:58:55 -0400 Subject: [PATCH 7/9] fix: self contained build does not work for windows based deployments --- src/AWS.Deploy.Common/Recipes/RecipeDefinition.cs | 5 +++++ src/AWS.Deploy.Common/Recipes/TargetPlatform.cs | 11 +++++++++++ .../DeploymentBundleHandler.cs | 2 +- .../RecipeDefinitions/ASP.NETAppAppRunner.recipe | 3 ++- .../RecipeDefinitions/ASP.NETAppECSFargate.recipe | 3 ++- .../ASP.NETAppElasticBeanstalkLinux.recipe | 3 ++- .../ASP.NETAppElasticBeanstalkWindows.recipe | 3 ++- .../ASP.NETAppExistingBeanstalkEnvironment.recipe | 3 ++- .../RecipeDefinitions/BlazorWasm.recipe | 3 ++- .../ConsoleAppECSFargateScheduleTask.recipe | 3 ++- .../ConsoleAppECSFargateService.recipe | 3 ++- .../RecipeDefinitions/PushContainerImageECR.recipe | 3 ++- .../RecipeDefinitions/aws-deploy-recipe-schema.json | 8 ++++++++ .../WebAppNoDockerFileTests.cs | 8 +++++--- ...sticBeanStalkConfigFile-Windows-SelfContained.json | 7 +++++++ testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj | 6 ++++++ 16 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 src/AWS.Deploy.Common/Recipes/TargetPlatform.cs create mode 100644 testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows-SelfContained.json 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.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.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe index 3aeb840a9..8f8e33b9e 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppAppRunner.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppAppRunner", - "Version": "1.0.1", + "Version": "1.0.2", "Name": "ASP.NET Core App to AWS App Runner", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -10,6 +10,7 @@ "ShortDescription": "Deploys as Linux container image to a fully managed environment. Dockerfile will be automatically generated if needed.", "Description": "This ASP.NET Core application will be built as a container image on Linux and deployed to AWS App Runner, a fully managed service for web applications and APIs. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your web application as a Linux container image on a fully managed environment.", "TargetService": "AWS App Runner", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 8383735f5..3516c7a77 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppEcsFargate", - "Version": "1.1.0", + "Version": "1.1.1", "Name": "ASP.NET Core App to Amazon ECS using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -10,6 +10,7 @@ "ShortDescription": "Deploys as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your application as a container image on Linux.", "TargetService": "Amazon Elastic Container Service", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe index ce13a3bcd..e71f9ce62 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkLinux.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppElasticBeanstalkLinux", - "Version": "1.0.1", + "Version": "1.0.2", "Name": "ASP.NET Core App to AWS Elastic Beanstalk on Linux", "DeploymentType": "CdkProject", "DeploymentBundle": "DotnetPublishZipFile", @@ -10,6 +10,7 @@ "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "TargetService": "AWS Elastic Beanstalk", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe index 98e4e517c..548e71afd 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppElasticBeanstalkWindows.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppElasticBeanstalkWindows", - "Version": "1.0.1", + "Version": "1.0.2", "Name": "ASP.NET Core App to AWS Elastic Beanstalk on Windows", "DeploymentType": "CdkProject", "DeploymentBundle": "DotnetPublishZipFile", @@ -10,6 +10,7 @@ "Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Windows. Recommended if you do not want to deploy your application as a container image.", "ShortDescription": "ASP.NET Core application deployed to AWS Elastic Beanstalk on Windows.", "TargetService": "AWS Elastic Beanstalk", + "TargetPlatform": "Windows", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe index c271600f4..cc2d64acd 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkEnvironment.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppExistingBeanstalkEnvironment", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "ASP.NET Core App to Existing AWS Elastic Beanstalk Environment", "DisableNewDeployments": true, "DeploymentType": "BeanstalkEnvironment", @@ -9,6 +9,7 @@ "ShortDescription": "Deploys your application directly to a Linux EC2 instance using AWS Elastic Beanstalk.", "Description": "This ASP.NET Core application will be built and deployed to an existing AWS Elastic Beanstalk environment. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", "TargetService": "AWS Elastic Beanstalk", + "TargetPlatform": "Linux", "RecipePriority": 0, "RecommendationRules": [ diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe index 70b790b41..51b29808c 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "BlazorWasm", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "Blazor WebAssembly App", "DeploymentType": "CdkProject", "DeploymentBundle": "DotnetPublishZipFile", @@ -10,6 +10,7 @@ "ShortDescription": "Hosts Blazor WebAssembly application in an Amazon S3 bucket with Amazon CloudFront as a content delivery network.", "Description": "This Blazor WebAssembly application will be built and hosted in a new Amazon Simple Storage Service (Amazon S3) bucket. The Blazor application will be exposed publicly through a CloudFront distribution using the Amazon S3 bucket as the origin.", "TargetService": "Amazon S3", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe index af5d8ffec..b153c9cd5 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateScheduleTask.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateScheduleTask", - "Version": "1.0.1", + "Version": "1.0.2", "Name": "Scheduled Task on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -10,6 +10,7 @@ "Description": "This .NET Console application will be built using a Dockerfile and deployed as a scheduled task to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a scheduled task as a container image on Linux.", "ShortDescription": "Deploys a scheduled task as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe index 3976518ba..a53c91ca2 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ConsoleAppECSFargateService.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "ConsoleAppEcsFargateService", - "Version": "1.0.1", + "Version": "1.0.2", "Name": "Service on Amazon Elastic Container Service (ECS) using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -10,6 +10,7 @@ "Description": "This .NET Console application will be built using a Dockerfile and deployed as a service to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy a service as a container image on Linux.", "ShortDescription": "Deploys a service as a Linux container image to a fully managed container orchestration service. Dockerfile will be automatically generated if needed.", "TargetService": "Amazon Elastic Container Service", + "TargetPlatform": "Linux", "DisplayedResources": [ { diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe index bd24193ea..ef627e586 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/PushContainerImageECR.recipe @@ -1,13 +1,14 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "PushContainerImageEcr", - "Version": "1.0.0", + "Version": "1.0.1", "Name": "Container Image to Amazon Elastic Container Registry (ECR)", "DeploymentType": "ElasticContainerRegistryImage", "DeploymentBundle": "Container", "Description": "This .NET application will be built using an existing Dockerfile. The Docker container image will then be pushed to Amazon ECR, a fully managed container registry.", "ShortDescription": "Pushes container image to a fully managed container registry.", "TargetService": "Amazon Elastic Container Service", + "TargetPlatform": "Linux", "RecipePriority": 0, "RecommendationRules": [ 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 af240cc3d..d2cbd592b 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/aws-deploy-recipe-schema.json @@ -16,6 +16,7 @@ "Description", "ShortDescription", "TargetService", + "TargetPlatform", "RecipePriority", "RecommendationRules", "OptionSettings" @@ -95,6 +96,13 @@ "description": "The AWS service that the project will be deployed to", "minLength": 1 }, + "TargetPlatform": { + "type": "string", + "title": "Target Platform", + "description": "The environment platform the recipe deploys to. This is used to publish a self-contained .NET application for that platform.", + "minLength": 1, + "enum": [ "Linux", "Windows" ] + }, "DeploymentConfirmation": { "$ref": "#/definitions/DeploymentConfirmation" }, diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs index aad7c8525..77ad2fe97 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs @@ -128,14 +128,16 @@ public async Task DefaultConfigurations() Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists."); } - [Fact] - public async Task WindowsEBDefaultConfigurations() + [Theory] + [InlineData("ElasticBeanStalkConfigFile-Windows.json")] + [InlineData("ElasticBeanStalkConfigFile-Windows-SelfContained.json")] + public async Task WindowsEBDefaultConfigurations(string configFile) { _stackName = $"WinTest-{Guid.NewGuid().ToString().Split('-').Last()}"; // Deploy var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); - var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", "ElasticBeanStalkConfigFile-Windows.json" }; + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics", "--silent", "--apply", configFile }; Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); // Verify application is deployed and running diff --git a/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows-SelfContained.json b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows-SelfContained.json new file mode 100644 index 000000000..ce3f6cf9a --- /dev/null +++ b/testapps/WebAppNoDockerFile/ElasticBeanStalkConfigFile-Windows-SelfContained.json @@ -0,0 +1,7 @@ +{ + "RecipeId": "AspNetAppElasticBeanstalkWindows", + "Settings": { + "IISAppPath": "/extra-path", + "SelfContainedBuild": true + } +} diff --git a/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj b/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj index 72e6c6788..96312487f 100644 --- a/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj +++ b/testapps/WebAppNoDockerFile/WebAppNoDockerFile.csproj @@ -8,6 +8,12 @@ Always + + Always + + + Always + From ce3761ad29f65121115b6337734294359c03f2cf Mon Sep 17 00:00:00 2001 From: Philippe El Asmar Date: Wed, 28 Sep 2022 11:21:03 -0400 Subject: [PATCH 8/9] feat: add support for deploying to existing windows Beanstalk environments --- src/AWS.Deploy.CLI/Commands/DeployCommand.cs | 2 +- .../Controllers/DeploymentController.cs | 2 +- src/AWS.Deploy.Constants/ElasticBeanstalk.cs | 22 +++ src/AWS.Deploy.Constants/RecipeIdentifier.cs | 1 + .../Data/AWSResourceQueryer.cs | 6 +- .../BeanstalkEnvironmentDeploymentCommand.cs | 6 + .../AWSElasticBeanstalkHandler.cs | 182 ++++++++++++++++- .../Utilities/DeployedApplicationQueryer.cs | 53 ++++- ...ExistingBeanstalkWindowsEnvironment.recipe | 110 +++++++++++ .../ExistingWindowsEnvironment/CLITests.cs | 67 +++++++ .../ServerModeTests.cs | 96 +++++++++ .../WindowsTestContextFixture.cs | 186 ++++++++++++++++++ .../WindowsTestContextFixtureCollection.cs | 16 ++ .../ServerModeTests.cs | 44 +---- .../TestContextFixture.cs | 11 +- .../TestContextFixtureCollection.cs | 6 +- .../Helpers/ElasticBeanstalkHelper.cs | 4 +- .../Helpers/IAMHelper.cs | 27 ++- .../RecommendationTests.cs | 13 +- .../DependencyValidationOptionSettings.cs | 10 +- .../ServerModeTests.cs | 81 ++------ .../Utilities/ServerModeUtilities.cs | 22 +++ .../DeployedApplicationQueryerTests.cs | 98 ++++++++- .../ElasticBeanstalkHandlerTests.cs | 98 +++++++++ 24 files changed, 1018 insertions(+), 145 deletions(-) create mode 100644 src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkWindowsEnvironment.recipe create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs create mode 100644 test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixtureCollection.cs diff --git a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs index eba65c7c4..a709c5a76 100644 --- a/src/AWS.Deploy.CLI/Commands/DeployCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeployCommand.cs @@ -367,7 +367,7 @@ private async Task GetSelectedRecommendationFromPreviousDeployme } 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/ServerMode/Controllers/DeploymentController.cs b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs index 9247a3efe..b128192a9 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Controllers/DeploymentController.cs @@ -442,7 +442,7 @@ public async Task SetDeploymentTarget(string sessionId, [FromBody } else { - previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment); + previousSettings = await deployedApplicationQueryer.GetPreviousSettings(existingDeployment, state.SelectedRecommendation); } state.SelectedRecommendation = await orchestrator.ApplyRecommendationPreviousSettings(state.SelectedRecommendation, previousSettings); 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/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/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/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.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkWindowsEnvironment.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkWindowsEnvironment.recipe new file mode 100644 index 000000000..12b92969f --- /dev/null +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppExistingBeanstalkWindowsEnvironment.recipe @@ -0,0 +1,110 @@ +{ + "$schema": "./aws-deploy-recipe-schema.json", + "Id": "AspNetAppExistingBeanstalkWindowsEnvironment", + "Version": "1.0.0", + "Name": "ASP.NET Core App to Existing AWS Elastic Beanstalk Windows Environment", + "DisableNewDeployments": true, + "DeploymentType": "BeanstalkEnvironment", + "DeploymentBundle": "DotnetPublishZipFile", + "ShortDescription": "Deploys your application directly to a Windows EC2 instance using AWS Elastic Beanstalk.", + "Description": "This ASP.NET Core application will be built and deployed to an existing AWS Elastic Beanstalk Windows environment. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.", + "TargetService": "AWS Elastic Beanstalk", + "TargetPlatform": "Windows", + + "RecipePriority": 0, + "RecommendationRules": [ + { + "Tests": [ + { + "Type": "MSProjectSdkAttribute", + "Condition": { + "Value": "Microsoft.NET.Sdk.Web" + } + }, + { + "Type": "MSProperty", + "Condition": { + "PropertyName": "TargetFramework", + "AllowedValues": [ "netcoreapp3.1", "net5.0", "net6.0" ] + } + } + ], + "Effect": { + "Pass": { "Include": true }, + "Fail": { "Include": false } + } + } + ], + "Categories": [ + { + "Id": "Hosting", + "DisplayName": "Hosting", + "Order": 20 + }, + { + "Id": "Health", + "DisplayName": "Health & Monitoring", + "Order": 30 + } + ], + "OptionSettings": [ + { + "Id": "IISWebSite", + "Name": "IIS Web Site", + "Category": "Hosting", + "Description": "The IIS Web Site the application will be installed in.", + "Type": "String", + "DefaultValue": "Default Web Site", + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "IISAppPath", + "Name": "IIS Application Path", + "Category": "Hosting", + "Description": "The IIS application path that will be the root of the application.", + "Type": "String", + "DefaultValue": "/", + "AdvancedSetting": true, + "Updatable": true + }, + { + "Id": "EnhancedHealthReporting", + "Name": "Enhanced Health Reporting", + "Category": "Health", + "Description": "Enhanced health reporting provides free real-time application and operating system monitoring of the instances and other resources in your environment.", + "Type": "String", + "DefaultValue": "enhanced", + "AllowedValues": [ + "enhanced", + "basic" + ], + "ValueMapping": { + "enhanced": "Enhanced", + "basic": "Basic" + }, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "XRayTracingSupportEnabled", + "Name": "Enable AWS X-Ray Tracing Support", + "Category": "Health", + "Description": "AWS X-Ray is a service that collects data about requests that your application serves, and provides tools you can use to view, filter, and gain insights into that data to identify issues and opportunities for optimization. Do you want to enable AWS X-Ray tracing support?", + "Type": "Bool", + "DefaultValue": false, + "AdvancedSetting": false, + "Updatable": true + }, + { + "Id": "HealthCheckURL", + "Name": "Health Check URL", + "Category": "Health", + "Description": "Customize the load balancer health check to ensure that your application, and not just the web server, is in a good state.", + "Type": "String", + "DefaultValue": "/", + "AdvancedSetting": false, + "Updatable": true + } + ] +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs new file mode 100644 index 000000000..62edfef86 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/CLITests.cs @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AWS.Deploy.CLI.IntegrationTests.Extensions; +using AWS.Deploy.Common; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Common.Recipes; +using AWS.Deploy.Orchestration.ServiceHandlers; +using Moq; +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment +{ + [Collection(nameof(WindowsTestContextFixture))] + public class CLITests + { + private readonly WindowsTestContextFixture _fixture; + private readonly AWSElasticBeanstalkHandler _awsElasticBeanstalkHandler; + private readonly Mock _optionSettingHandler; + private readonly ProjectDefinitionParser _projectDefinitionParser; + private readonly RecipeDefinition _recipeDefinition; + + public CLITests(WindowsTestContextFixture fixture) + { + _projectDefinitionParser = new ProjectDefinitionParser(new FileManager(), new DirectoryManager()); + _fixture = fixture; + _optionSettingHandler = new Mock(); + _awsElasticBeanstalkHandler = new AWSElasticBeanstalkHandler(null, null, null, _optionSettingHandler.Object); + _recipeDefinition = new Mock( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()).Object; + } + + [Fact] + public async Task DeployToExistingBeanstalkEnvironment() + { + var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _fixture.EnvironmentName, "--diagnostics", "--silent", "--region", "us-west-2" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _fixture.App.Run(deployArgs)); + + var environmentDescription = await _fixture.AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(_fixture.EnvironmentName); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await _fixture.HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; + var deployStdOutput = _fixture.InteractiveService.StdOutReader.ReadAllLines(); + var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); + Assert.False(string.IsNullOrEmpty(successMessage)); + + var expectedVersionLabel = successMessage.Split(" ").Last(); + Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs new file mode 100644 index 000000000..06262d7a0 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/ServerModeTests.cs @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +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.IntegrationTests.Utilities; +using AWS.Deploy.ServerMode.Client; +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment +{ + [Collection(nameof(WindowsTestContextFixture))] + public class ServerModeTests + { + private readonly WindowsTestContextFixture _fixture; + private const string BEANSTALK_ENVIRONMENT_RECIPE_ID = "AspNetAppExistingBeanstalkWindowsEnvironment"; + + public ServerModeTests(WindowsTestContextFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task DeployToExistingWindowsBeanstalkEnvironment() + { + var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var portNumber = 4031; + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); + + var serverCommand = new ServerModeCommand(_fixture.ToolInteractiveService, 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 startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput + { + AwsRegion = "us-west-2", + ProjectPath = projectPath + }); + + var sessionId = startSessionOutput.SessionId; + Assert.NotNull(sessionId); + + var existingDeployments = await restClient.GetExistingDeploymentsAsync(sessionId); + var existingDeployment = existingDeployments.ExistingDeployments.First(x => string.Equals(_fixture.EnvironmentName, x.Name)); + + Assert.Equal(_fixture.EnvironmentName, existingDeployment.Name); + Assert.Equal(BEANSTALK_ENVIRONMENT_RECIPE_ID, existingDeployment.RecipeId); + Assert.Null(existingDeployment.BaseRecipeId); + Assert.False(existingDeployment.IsPersistedDeploymentProject); + Assert.Equal(_fixture.EnvironmentId, existingDeployment.ExistingDeploymentId); + Assert.Equal(DeploymentTypes.BeanstalkEnvironment, existingDeployment.DeploymentType); + + var signalRClient = new DeploymentCommunicationClient(baseUrl); + await signalRClient.JoinSession(sessionId); + + var logOutput = new StringBuilder(); + AWS.Deploy.CLI.IntegrationTests.ServerModeTests.RegisterSignalRMessageCallbacks(signalRClient, logOutput); + + await restClient.SetDeploymentTargetAsync(sessionId, new SetDeploymentTargetInput + { + ExistingDeploymentId = _fixture.EnvironmentId + }); + + await restClient.StartDeploymentAsync(sessionId); + + await restClient.WaitForDeployment(sessionId); + + Assert.True(logOutput.Length > 0); + var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; + var deployStdOutput = logOutput.ToString().Split(Environment.NewLine); + var successMessage = deployStdOutput.First(line => line.Trim().StartsWith(successMessagePrefix)); + Assert.False(string.IsNullOrEmpty(successMessage)); + + var expectedVersionLabel = successMessage.Split(" ").Last(); + Assert.True(await _fixture.EBHelper.VerifyEnvironmentVersionLabel(_fixture.EnvironmentName, expectedVersionLabel)); + } + finally + { + cancelSource.Cancel(); + } + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs new file mode 100644 index 000000000..a0d419cf1 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixture.cs @@ -0,0 +1,186 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.ElasticBeanstalk; +using Amazon.IdentityManagement; +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.IntegrationTests.Services; +using AWS.Deploy.Common; +using AWS.Deploy.Common.Data; +using AWS.Deploy.Common.IO; +using AWS.Deploy.Orchestration.ServiceHandlers; +using AWS.Deploy.Orchestration.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment +{ + /// + /// The goal of this class is to be used as shared context between a collection of tests. + /// More info could be found here https://xunit.net/docs/shared-context + /// + public class WindowsTestContextFixture : IAsyncLifetime + { + public readonly App App; + public readonly HttpHelper HttpHelper; + public readonly IAWSResourceQueryer AWSResourceQueryer; + public readonly TestAppManager TestAppManager; + public readonly IDirectoryManager DirectoryManager; + public readonly ICommandLineWrapper CommandLineWrapper; + public readonly IZipFileManager ZipFileManager; + public readonly IToolInteractiveService ToolInteractiveService; + public readonly IElasticBeanstalkHandler ElasticBeanstalkHandler; + public readonly InMemoryInteractiveService InteractiveService; + public readonly ElasticBeanstalkHelper EBHelper; + public readonly IAMHelper IAMHelper; + + public readonly string ApplicationName; + public readonly string EnvironmentName; + public readonly string VersionLabel; + public readonly string RoleName; + public string EnvironmentId; + + public WindowsTestContextFixture() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddCustomServices(); + serviceCollection.AddTestServices(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var awsClientFactory = serviceProvider.GetService(); + awsClientFactory.ConfigureAWSOptions((options) => + { + options.Region = Amazon.RegionEndpoint.USWest2; + }); + + App = serviceProvider.GetService(); + Assert.NotNull(App); + + InteractiveService = serviceProvider.GetService(); + Assert.NotNull(InteractiveService); + + ToolInteractiveService = serviceProvider.GetService(); + + AWSResourceQueryer = serviceProvider.GetService(); + Assert.NotNull(AWSResourceQueryer); + + CommandLineWrapper = serviceProvider.GetService(); + Assert.NotNull(CommandLineWrapper); + + ZipFileManager = serviceProvider.GetService(); + Assert.NotNull(ZipFileManager); + + DirectoryManager = serviceProvider.GetService(); + Assert.NotNull(DirectoryManager); + + ElasticBeanstalkHandler = serviceProvider.GetService(); + Assert.NotNull(ElasticBeanstalkHandler); + + HttpHelper = new HttpHelper(InteractiveService); + TestAppManager = new TestAppManager(); + + var suffix = Guid.NewGuid().ToString().Split('-').Last(); + ApplicationName = $"application{suffix}"; + EnvironmentName = $"environment{suffix}"; + VersionLabel = $"v-{suffix}"; + RoleName = $"aws-elasticbeanstalk-ec2-role{suffix}"; + + EBHelper = new ElasticBeanstalkHelper(new AmazonElasticBeanstalkClient(Amazon.RegionEndpoint.USWest2), AWSResourceQueryer, ToolInteractiveService); + IAMHelper = new IAMHelper(new AmazonIdentityManagementServiceClient(), AWSResourceQueryer, ToolInteractiveService); + } + + public async Task InitializeAsync() + { + await IAMHelper.CreateRoleForBeanstalkEnvionmentDeployment(RoleName); + + var projectPath = TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); + var publishDirectoryInfo = DirectoryManager.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + var zipFilePath = $"{publishDirectoryInfo.FullName}.zip"; + + var publishCommand = + $"dotnet publish \"{projectPath}\"" + + $" -o \"{publishDirectoryInfo}\"" + + $" -c release"; + + var result = await CommandLineWrapper.TryRunWithResult(publishCommand, streamOutputToInteractiveService: true); + Assert.Equal(0, result.ExitCode); + + await ZipFileManager.CreateFromDirectory(publishDirectoryInfo.FullName, zipFilePath); + + SetupWindowsDeploymentManifest(zipFilePath); + + await EBHelper.CreateApplicationAsync(ApplicationName); + await EBHelper.CreateApplicationVersionAsync(ApplicationName, VersionLabel, zipFilePath); + var success = await EBHelper.CreateEnvironmentAsync(ApplicationName, EnvironmentName, VersionLabel, BeanstalkPlatformType.Windows); + Assert.True(success); + + var environmentDescription = await AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(EnvironmentName); + EnvironmentId = environmentDescription.EnvironmentId; + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await HttpHelper.WaitUntilSuccessStatusCode(environmentDescription.CNAME, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + } + + public void SetupWindowsDeploymentManifest(string dotnetZipFilePath) + { + const string MANIFEST_FILENAME = "aws-windows-deployment-manifest.json"; + + var jsonStream = new MemoryStream(); + using (var jsonWriter = new Utf8JsonWriter(jsonStream)) + { + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("manifestVersion"); + jsonWriter.WriteNumberValue(1); + + jsonWriter.WriteStartObject("deployments"); + jsonWriter.WriteStartArray("aspNetCoreWeb"); + + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("name"); + jsonWriter.WriteStringValue("MainApp"); + + jsonWriter.WriteStartObject("parameters"); + jsonWriter.WritePropertyName("appBundle"); + jsonWriter.WriteStringValue("."); + jsonWriter.WritePropertyName("iisWebSite"); + jsonWriter.WriteStringValue("Default Web Site"); + jsonWriter.WritePropertyName("iisPath"); + jsonWriter.WriteStringValue("/"); + jsonWriter.WriteEndObject(); + + jsonWriter.WriteEndObject(); + + jsonWriter.WriteEndArray(); + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + } + + using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update)) + { + var zipEntry = zipArchive.CreateEntry(MANIFEST_FILENAME); + using var zipEntryStream = zipEntry.Open(); + jsonStream.Position = 0; + jsonStream.CopyTo(zipEntryStream); + + } + } + + public async Task DisposeAsync() + { + var success = await EBHelper.DeleteApplication(ApplicationName, EnvironmentName); + await IAMHelper.DeleteRoleAndInstanceProfileAfterBeanstalkEnvionmentDeployment(RoleName); + Assert.True(success); + } + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixtureCollection.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixtureCollection.cs new file mode 100644 index 000000000..b61acf635 --- /dev/null +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ExistingWindowsEnvironment/WindowsTestContextFixtureCollection.cs @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests.ExistingWindowsEnvironment +{ + /// + /// The goal of this class is to create a Collection definition and use as shared context. + /// More info could be found here https://xunit.net/docs/shared-context + /// + [CollectionDefinition(nameof(WindowsTestContextFixture))] + public class WindowsTestContextFixtureCollection : ICollectionFixture + { + } +} diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs index 272196ce6..67eeabf7e 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/ServerModeTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Amazon.Runtime; using AWS.Deploy.CLI.Commands; +using AWS.Deploy.CLI.IntegrationTests.Utilities; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; using Xunit; @@ -31,7 +32,7 @@ public async Task DeployToExistingBeanstalkEnvironment() { var projectPath = _fixture.TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4031; - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); var serverCommand = new ServerModeCommand(_fixture.ToolInteractiveService, portNumber, null, true); var cancelSource = new CancellationTokenSource(); @@ -42,7 +43,7 @@ public async Task DeployToExistingBeanstalkEnvironment() var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -76,7 +77,7 @@ public async Task DeployToExistingBeanstalkEnvironment() await restClient.StartDeploymentAsync(sessionId); - await WaitForDeployment(restClient, sessionId); + await restClient.WaitForDeployment(sessionId); Assert.True(logOutput.Length > 0); var successMessagePrefix = $"The Elastic Beanstalk Environment {_fixture.EnvironmentName} has been successfully updated"; @@ -92,42 +93,5 @@ public async Task DeployToExistingBeanstalkEnvironment() cancelSource.Cancel(); } } - - private async Task WaitForDeployment(RestAPIClient restApiClient, string sessionId) - { - // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. - await Task.Delay(TimeSpan.FromSeconds(3)); - - await Orchestration.Utilities.Helpers.WaitUntil(async () => - { - DeploymentStatus status = (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; ; - return status != DeploymentStatus.Executing; - }, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(15)); - - return (await restApiClient.GetDeploymentStatusAsync(sessionId)).Status; - } - - private async Task WaitTillServerModeReady(RestAPIClient restApiClient) - { - await Orchestration.Utilities.Helpers.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 Task ResolveCredentials() - { - var testCredentials = FallbackCredentialsFactory.GetCredentials(); - return Task.FromResult(testCredentials); - } } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs index cae778e87..3ac1d58d9 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixture.cs @@ -22,6 +22,10 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests { + /// + /// The goal of this class is to be used as shared context between a collection of tests. + /// More info could be found here https://xunit.net/docs/shared-context + /// public class TestContextFixture : IAsyncLifetime { public readonly App App; @@ -39,6 +43,7 @@ public class TestContextFixture : IAsyncLifetime public readonly string ApplicationName; public readonly string EnvironmentName; public readonly string VersionLabel; + public readonly string RoleName; public string EnvironmentId; public TestContextFixture() @@ -83,6 +88,7 @@ public TestContextFixture() ApplicationName = $"application{suffix}"; EnvironmentName = $"environment{suffix}"; VersionLabel = $"v-{suffix}"; + RoleName = $"aws-elasticbeanstalk-ec2-role{suffix}"; EBHelper = new ElasticBeanstalkHelper(new AmazonElasticBeanstalkClient(Amazon.RegionEndpoint.USWest2), AWSResourceQueryer, ToolInteractiveService); IAMHelper = new IAMHelper(new AmazonIdentityManagementServiceClient(), AWSResourceQueryer, ToolInteractiveService); @@ -90,7 +96,7 @@ public TestContextFixture() public async Task InitializeAsync() { - await IAMHelper.CreateRoleForBeanstalkEnvionmentDeployment("aws-elasticbeanstalk-ec2-role"); + await IAMHelper.CreateRoleForBeanstalkEnvionmentDeployment(RoleName); var projectPath = TestAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var publishDirectoryInfo = DirectoryManager.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); @@ -108,7 +114,7 @@ public async Task InitializeAsync() await EBHelper.CreateApplicationAsync(ApplicationName); await EBHelper.CreateApplicationVersionAsync(ApplicationName, VersionLabel, zipFilePath); - var success = await EBHelper.CreateEnvironmentAsync(ApplicationName, EnvironmentName, VersionLabel); + var success = await EBHelper.CreateEnvironmentAsync(ApplicationName, EnvironmentName, VersionLabel, BeanstalkPlatformType.Linux); Assert.True(success); var environmentDescription = await AWSResourceQueryer.DescribeElasticBeanstalkEnvironment(EnvironmentName); @@ -121,6 +127,7 @@ public async Task InitializeAsync() public async Task DisposeAsync() { var success = await EBHelper.DeleteApplication(ApplicationName, EnvironmentName); + await IAMHelper.DeleteRoleAndInstanceProfileAfterBeanstalkEnvionmentDeployment(RoleName); Assert.True(success); } } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixtureCollection.cs b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixtureCollection.cs index b408e1968..7c1981c0c 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixtureCollection.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/BeanstalkBackwardsCompatibilityTests/TestContextFixtureCollection.cs @@ -5,7 +5,11 @@ namespace AWS.Deploy.CLI.IntegrationTests.BeanstalkBackwardsCompatibilityTests { - [CollectionDefinition(nameof(TestContextFixture), DisableParallelization = true)] + /// + /// The goal of this class is to create a Collection definition and use as shared context. + /// More info could be found here https://xunit.net/docs/shared-context + /// + [CollectionDefinition(nameof(TestContextFixture))] public class TestContextFixtureCollection : ICollectionFixture { } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs index 4d54405d8..a33a2d61b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/ElasticBeanstalkHelper.cs @@ -61,7 +61,7 @@ await _client.CreateApplicationVersionAsync(new CreateApplicationVersionRequest }); } - public async Task CreateEnvironmentAsync(string applicationName, string environmentName, string versionLabel) + public async Task CreateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, BeanstalkPlatformType platformType) { _interactiveService.WriteLine($"Creating new Elastic Beanstalk environment {environmentName} with versionLabel {versionLabel}"); @@ -72,7 +72,7 @@ await _client.CreateEnvironmentAsync(new CreateEnvironmentRequest ApplicationName = applicationName, EnvironmentName = environmentName, VersionLabel = versionLabel, - PlatformArn = (await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(BeanstalkPlatformType.Linux)).PlatformArn, + PlatformArn = (await _awsResourceQueryer.GetLatestElasticBeanstalkPlatformArn(platformType)).PlatformArn, OptionSettings = new List { new ConfigurationOptionSetting("aws:autoscaling:launchconfiguration", "IamInstanceProfile", "aws-elasticbeanstalk-ec2-role"), diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs index fad782368..15758cb9e 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Helpers/IAMHelper.cs @@ -23,6 +23,30 @@ public IAMHelper(IAmazonIdentityManagementService client, IAWSResourceQueryer aw _interactiveService = toolInteractiveService; } + /// + /// Delete an existing IAM Role by first removing the role from an existing instance profile and deleting that profile. + /// + public async Task DeleteRoleAndInstanceProfileAfterBeanstalkEnvionmentDeployment(string roleName) + { + var existingRoles = await _awsResourceQueryer.ListOfIAMRoles("ec2.amazonaws.com"); + var role = existingRoles.FirstOrDefault(x => string.Equals(roleName, x.RoleName)); + if (role != null) + { + await _client.RemoveRoleFromInstanceProfileAsync(new RemoveRoleFromInstanceProfileRequest + { + RoleName = roleName, + InstanceProfileName = roleName + }); + + await _client.DeleteInstanceProfileAsync(new DeleteInstanceProfileRequest() + { + InstanceProfileName = roleName + }); + + await _client.DeleteRoleAsync(new DeleteRoleRequest { RoleName = role.RoleName }); + } + } + public async Task CreateRoleForBeanstalkEnvionmentDeployment(string roleName) { _interactiveService.WriteLine($"Creating role {roleName} for deployment to Elastic Beanstalk environemnt"); @@ -51,7 +75,8 @@ public async Task CreateRoleForBeanstalkEnvionmentDeployment(string roleName) await _client.CreateRoleAsync(new CreateRoleRequest { RoleName = roleName, - AssumeRolePolicyDocument = assumeRolepolicyDocument.Replace("'", "\"") + AssumeRolePolicyDocument = assumeRolepolicyDocument.Replace("'", "\""), + MaxSessionDuration = 7200 }); } diff --git a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs index 22ab47d41..40c826f38 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/SaveCdkDeploymentProject/RecommendationTests.cs @@ -74,13 +74,14 @@ public async Task GenerateRecommendationsWithoutCustomRecipes() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - recommendations.Count.ShouldEqual(6); + recommendations.Count.ShouldEqual(7); recommendations[0].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe recommendations[1].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); // default recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe recommendations[3].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Windows"); // default recipe recommendations[4].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[5].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe + recommendations[5].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Windows Environment"); // default recipe + recommendations[6].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe } [Fact] @@ -112,7 +113,7 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(8); + recommendations.Count.ShouldEqual(9); recommendations[0].Name.ShouldEqual(customEcsRecipeName); // custom recipe recommendations[1].Name.ShouldEqual(customEbsRecipeName); // custom recipe recommendations[2].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); // default recipe @@ -120,7 +121,8 @@ public async Task GenerateRecommendationsFromCustomRecipesWithManifestFile() recommendations[4].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); // default recipe recommendations[5].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Windows"); // default recipe recommendations[6].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); // default recipe - recommendations[7].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe + recommendations[7].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Windows Environment"); // default recipe + recommendations[8].Name.ShouldEqual("Container Image to Amazon Elastic Container Registry (ECR)"); // default recipe // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEcsProject, "ECS-CDK.recipe")); @@ -163,7 +165,7 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() var recommendations = await orchestrator.GenerateDeploymentRecommendations(); // ASSERT - Recipes are ordered by priority - recommendations.Count.ShouldEqual(7); + recommendations.Count.ShouldEqual(8); recommendations[0].Name.ShouldEqual(customEbsRecipeName); recommendations[1].Name.ShouldEqual(customEcsRecipeName); recommendations[2].Name.ShouldEqual("ASP.NET Core App to AWS Elastic Beanstalk on Linux"); @@ -171,6 +173,7 @@ public async Task GenerateRecommendationsFromCustomRecipesWithoutManifestFile() recommendations[4].Name.ShouldEqual("ASP.NET Core App to Amazon ECS using AWS Fargate"); recommendations[5].Name.ShouldEqual("ASP.NET Core App to AWS App Runner"); recommendations[6].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Environment"); + recommendations[7].Name.ShouldEqual("ASP.NET Core App to Existing AWS Elastic Beanstalk Windows Environment"); // ASSERT - Recipe paths recommendations[0].Recipe.RecipePath.ShouldEqual(Path.Combine(saveDirectoryPathEbsProject, "EBS-CDK.recipe")); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs index 389bef013..2ceee49a4 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerMode/DependencyValidationOptionSettings.cs @@ -43,12 +43,6 @@ public DependencyValidationOptionSettings() _testAppManager = new TestAppManager(); } - public Task ResolveCredentials() - { - var testCredentials = FallbackCredentialsFactory.GetCredentials(); - return Task.FromResult(testCredentials); - } - [Fact] public async Task DependentOptionSettingsGetInvalidated() { @@ -56,7 +50,7 @@ public async Task DependentOptionSettingsGetInvalidated() var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4022; - 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(); @@ -147,7 +141,7 @@ public async Task SettingInvalidValue() var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4022; - 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(); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs index 7d6e350a0..778879e3b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/ServerModeTests.cs @@ -19,6 +19,7 @@ using AWS.Deploy.CLI.IntegrationTests.Extensions; using AWS.Deploy.CLI.IntegrationTests.Helpers; using AWS.Deploy.CLI.IntegrationTests.Services; +using AWS.Deploy.CLI.IntegrationTests.Utilities; using AWS.Deploy.CLI.ServerMode; using AWS.Deploy.Orchestration.Utilities; using AWS.Deploy.ServerMode.Client; @@ -58,12 +59,6 @@ public ServerModeTests() _testAppManager = new TestAppManager(); } - public Task ResolveCredentials() - { - var testCredentials = FallbackCredentialsFactory.GetCredentials(); - return Task.FromResult(testCredentials); - } - /// /// ServerMode must only be connectable from 127.0.0.1 or localhost. This test confirms that connect attempts using /// the host name fail. @@ -79,8 +74,8 @@ public async Task ConfirmLocalhostOnly() _ = serverCommand.ExecuteAsync(cancelSource.Token); try { - var restClient = new RestAPIClient($"http://localhost:{portNumber}/", ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials)); - await WaitTillServerModeReady(restClient); + var restClient = new RestAPIClient($"http://localhost:{portNumber}/", ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials)); + await restClient.WaitTillServerModeReady(); using var client = new HttpClient(); @@ -102,7 +97,7 @@ public async Task GetRecommendations() { var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4000; - 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(); @@ -111,7 +106,7 @@ public async Task GetRecommendations() try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -149,7 +144,7 @@ public async Task GetRecommendationsWithEncryptedCredentials() aes.GenerateKey(); aes.GenerateIV(); - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials, aes); + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials, aes); var keyInfo = new EncryptionKeyInfo { @@ -168,7 +163,7 @@ public async Task GetRecommendationsWithEncryptedCredentials() try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -200,7 +195,7 @@ public async Task WebFargateDeploymentNoConfigChanges() var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4011; - 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(); @@ -211,7 +206,7 @@ public async Task WebFargateDeploymentNoConfigChanges() var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -242,7 +237,7 @@ public async Task WebFargateDeploymentNoConfigChanges() await restClient.StartDeploymentAsync(sessionId); - await WaitForDeployment(restClient, sessionId); + await restClient.WaitForDeployment(sessionId); var stackStatus = await _cloudFormationHelper.GetStackStatus(_stackName); Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); @@ -322,7 +317,7 @@ public async Task RecommendationsForNewDeployments_DoesNotIncludeExistingBeansta { 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(); @@ -334,7 +329,7 @@ public async Task RecommendationsForNewDeployments_DoesNotIncludeExistingBeansta var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -362,7 +357,7 @@ public async Task ShutdownViaRestClient() { var portNumber = 4003; var cancelSource = new CancellationTokenSource(); - using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ResolveCredentials); + using var httpClient = ServerModeHttpClientFactory.ConstructHttpClient(ServerModeExtensions.ResolveCredentials); var serverCommand = new ServerModeCommand(_serviceProvider.GetRequiredService(), portNumber, null, true); var serverTask = serverCommand.ExecuteAsync(cancelSource.Token); @@ -372,7 +367,7 @@ public async Task ShutdownViaRestClient() var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); await restClient.ShutdownAsync(); Thread.Sleep(100); @@ -395,7 +390,7 @@ public async Task InvalidStackName_ThrowsException(string invalidStackName) { var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); var portNumber = 4012; - 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(); @@ -406,7 +401,7 @@ public async Task InvalidStackName_ThrowsException(string invalidStackName) var baseUrl = $"http://localhost:{portNumber}/"; var restClient = new RestAPIClient(baseUrl, httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -440,7 +435,7 @@ public async Task CheckCategories() { var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppNoDockerFile", "WebAppNoDockerFile.csproj")); var portNumber = 4200; - 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(); @@ -449,7 +444,7 @@ public async Task CheckCategories() try { var restClient = new RestAPIClient($"http://localhost:{portNumber}/", httpClient); - await WaitTillServerModeReady(restClient); + await restClient.WaitTillServerModeReady(); var startSessionOutput = await restClient.StartDeploymentSessionAsync(new StartDeploymentSessionInput { @@ -511,46 +506,6 @@ internal static void RegisterSignalRMessageCallbacks(IDeploymentCommunicationCli }; } - private async Task WaitForDeployment(RestAPIClient restApiClient, string sessionId) - { - // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. - await Task.Delay(TimeSpan.FromSeconds(3)); - - GetDeploymentStatusOutput output = null; - - await Orchestration.Utilities.Helpers.WaitUntil(async () => - { - output = (await restApiClient.GetDeploymentStatusAsync(sessionId)); - - return output.Status != DeploymentStatus.Executing; - }, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(15)); - - if (output.Exception != null) - { - throw new Exception("Error waiting on stack status: " + output.Exception.Message); - } - - return output.Status; - } - - - private async Task WaitTillServerModeReady(RestAPIClient restApiClient) - { - await Orchestration.Utilities.Helpers.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 void Dispose() { Dispose(true); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs index a984afb1a..d7b1f7478 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/ServerModeUtilities.cs @@ -98,5 +98,27 @@ public static Task ResolveCredentials() var testCredentials = FallbackCredentialsFactory.GetCredentials(); return Task.FromResult(testCredentials); } + + public static async Task WaitForDeployment(this RestAPIClient restApiClient, string sessionId) + { + // Do an initial delay to avoid a race condition of the status being checked before the deployment has kicked off. + await Task.Delay(TimeSpan.FromSeconds(3)); + + GetDeploymentStatusOutput output = null; + + await Orchestration.Utilities.Helpers.WaitUntil(async () => + { + output = (await restApiClient.GetDeploymentStatusAsync(sessionId)); + + return output.Status != DeploymentStatus.Executing; + }, TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(15)); + + if (output.Exception != null) + { + throw new Exception("Error waiting on stack status: " + output.Exception.Message); + } + + return output.Status; + } } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs index 5e75e95b6..b9f90e7db 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/DeployedApplicationQueryerTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -27,6 +28,7 @@ public class DeployedApplicationQueryerTests { private readonly Mock _mockAWSResourceQueryer; private readonly IDirectoryManager _directoryManager; + private readonly TestFileManager _fileManager; private readonly Mock _mockLocalUserSettingsEngine; private readonly Mock _mockOrchestratorInteractiveService; @@ -34,6 +36,7 @@ public DeployedApplicationQueryerTests() { _mockAWSResourceQueryer = new Mock(); _directoryManager = new TestDirectoryManager(); + _fileManager = new TestFileManager(); _mockLocalUserSettingsEngine = new Mock(); _mockOrchestratorInteractiveService = new Mock(); } @@ -62,7 +65,8 @@ public async Task GetExistingDeployedApplications_ListDeploymentsCall() var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); @@ -107,7 +111,8 @@ public async Task GetExistingDeployedApplications_CompatibleSystemRecipes() var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var recommendations = new List { @@ -170,7 +175,8 @@ public async Task GetExistingDeployedApplications_WithDeploymentProjects() var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var recommendations = new List { @@ -227,7 +233,8 @@ public async Task GetExistingDeployedApplications_InvalidConfigurations(string r var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); @@ -286,7 +293,8 @@ public async Task GetExistingDeployedApplications_ContainsValidBeanstalkEnvironm var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); @@ -339,7 +347,8 @@ public async Task GetExistingDeployedApplication_SkipsEnvironmentsWithIncompatib var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); @@ -400,7 +409,8 @@ public async Task GetExistingDeployedApplication_SkipsEnvironmentsCreatedFromThe var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); var deploymentTypes = new List() { DeploymentTypes.CdkProject, DeploymentTypes.BeanstalkEnvironment }; var result = await deployedApplicationQueryer.GetExistingDeployedApplications(deploymentTypes); @@ -446,14 +456,84 @@ public async Task GetPreviousSettings_BeanstalkEnvironment() var deployedApplicationQueryer = new DeployedApplicationQueryer( _mockAWSResourceQueryer.Object, _mockLocalUserSettingsEngine.Object, - _mockOrchestratorInteractiveService.Object); + _mockOrchestratorInteractiveService.Object, + _fileManager); - var optionSettings = await deployedApplicationQueryer.GetPreviousSettings(application); + var projectDefinition = new ProjectDefinition(null, "testPath", "", "net6.0"); + var recipeDefinitiion = new RecipeDefinition("AspNetAppExistingBeanstalkEnvironment", "", "", DeploymentTypes.BeanstalkEnvironment, DeploymentBundleTypes.DotnetPublishZipFile, "", "", "", "", ""); + var recommendation = new Recommendation(recipeDefinitiion, projectDefinition, 100, new Dictionary()); + + var optionSettings = await deployedApplicationQueryer.GetPreviousSettings(application, recommendation); Assert.Equal("enhanced", optionSettings[Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId]); Assert.Equal("/", optionSettings[Constants.ElasticBeanstalk.HealthCheckURLOptionId]); Assert.Equal("nginx", optionSettings[Constants.ElasticBeanstalk.ProxyOptionId]); Assert.Equal("false", optionSettings[Constants.ElasticBeanstalk.XRayTracingOptionId]); } + + [Fact] + public async Task GetPreviousSettings_BeanstalkWindowsEnvironment() + { + var application = new CloudApplication("name", "Id", CloudApplicationResourceType.BeanstalkEnvironment, "recipe"); + var configurationSettings = new List + { + new ConfigurationOptionSetting + { + Namespace = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionNameSpace, + OptionName = Constants.ElasticBeanstalk.EnhancedHealthReportingOptionName, + Value = "enhanced" + }, + new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.HealthCheckURLOptionName, + Namespace = Constants.ElasticBeanstalk.HealthCheckURLOptionNameSpace, + Value = "/" + }, + new ConfigurationOptionSetting + { + OptionName = Constants.ElasticBeanstalk.XRayTracingOptionName, + Namespace = Constants.ElasticBeanstalk.XRayTracingOptionNameSpace, + Value = "false" + } + }; + + _mockAWSResourceQueryer + .Setup(x => x.GetBeanstalkEnvironmentConfigurationSettings(It.IsAny())) + .Returns(Task.FromResult(configurationSettings)); + + var deployedApplicationQueryer = new DeployedApplicationQueryer( + _mockAWSResourceQueryer.Object, + _mockLocalUserSettingsEngine.Object, + _mockOrchestratorInteractiveService.Object, + _fileManager); + + var manifestJson = @"{ + ""manifestVersion"": 1, + ""deployments"": { + ""aspNetCoreWeb"": [ + { + ""name"": ""app"", + ""parameters"": { + ""iisPath"": ""/path"", + ""iisWebSite"": ""Default Web Site Custom"" + } + } + ] + } + }"; + _fileManager.InMemoryStore.Add(Path.Combine("testPath", "aws-windows-deployment-manifest.json"), manifestJson); + var projectDefinition = new ProjectDefinition(null, Path.Combine("testPath", "project.csproj"), "", "net6.0"); + var recipeDefinitiion = new RecipeDefinition("AspNetAppExistingBeanstalkWindowsEnvironment", "", "", DeploymentTypes.BeanstalkEnvironment, DeploymentBundleTypes.DotnetPublishZipFile, "", "", "", "", ""); + var recommendation = new Recommendation(recipeDefinitiion, projectDefinition, 100, new Dictionary()); + + var optionSettings = await deployedApplicationQueryer.GetPreviousSettings(application, recommendation); + + Assert.Equal("enhanced", optionSettings[Constants.ElasticBeanstalk.EnhancedHealthReportingOptionId]); + Assert.Equal("/", optionSettings[Constants.ElasticBeanstalk.HealthCheckURLOptionId]); + Assert.Equal("false", optionSettings[Constants.ElasticBeanstalk.XRayTracingOptionId]); + Assert.Equal("false", optionSettings[Constants.ElasticBeanstalk.XRayTracingOptionId]); + Assert.Equal("/path", optionSettings[Constants.ElasticBeanstalk.IISAppPathOptionId]); + Assert.Equal("Default Web Site Custom", optionSettings[Constants.ElasticBeanstalk.IISWebSiteOptionId]); + } } } diff --git a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs index d363f1c23..ec38bbcdd 100644 --- a/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs +++ b/test/AWS.Deploy.Orchestration.UnitTests/ElasticBeanstalkHandlerTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.IO.Compression; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -19,6 +21,7 @@ using AWS.Deploy.Recipes; using Moq; using Xunit; +using System.Text.Json; namespace AWS.Deploy.Orchestration.UnitTests { @@ -173,5 +176,100 @@ private bool IsEqual(ConfigurationOptionSetting expected, ConfigurationOptionSet && string.Equals(expected.Namespace, actual.Namespace) && string.Equals(expected.Value, actual.Value); } + + /// + /// This method tests in the case of an existing windows beanstalk recipe, if there is no windows manifest file, then one is created and it contains the correct values. + /// + [Fact] + public async Task SetupWindowsDeploymentManifestTest() + { + // ARRANGE + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + var recommendations = await engine.ComputeRecommendations(); + var recommendation = recommendations.First(r => r.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID)); + + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + var zipPath = Path.Combine(tempDirectory, "testZip.zip"); + ZipFile.CreateFromDirectory(recommendation.GetProjectDirectory(), zipPath); + + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISWebSiteOptionId), "website"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISAppPathOptionId), "apppath"); + + var elasticBeanstalkHandler = new AWSElasticBeanstalkHandler(new Mock().Object, + new Mock().Object, + new Mock().Object, + _optionSettingHandler); + + elasticBeanstalkHandler.SetupWindowsDeploymentManifest(recommendation, zipPath); + + using (FileStream zipToOpen = new FileStream(zipPath, FileMode.Open)) + { + using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update)) + { + ZipArchiveEntry readmeEntry = archive.GetEntry("aws-windows-deployment-manifest.json"); + var manifestFile = JsonSerializer.Deserialize(readmeEntry.Open()); + Assert.NotNull(manifestFile); + var aspNetCoreWebEntry = Assert.Single(manifestFile.Deployments.AspNetCoreWeb); + Assert.Equal("website", aspNetCoreWebEntry.Parameters.IISWebSite); + Assert.Equal("apppath", aspNetCoreWebEntry.Parameters.IISPath); + } + } + } + + /// + /// This method tests in the case of an existing windows beanstalk recipe, if there is a windows manifest file, then one is updated correctly. + /// The manifest file is generated from and updated to contain the IIS Website and IIS App path. + /// + [Fact] + public async Task SetupWindowsDeploymentManifestTest_ExistingFile() + { + // ARRANGE + var engine = await BuildRecommendationEngine("WebAppNoDockerFile"); + var recommendations = await engine.ComputeRecommendations(); + var recommendation = recommendations.First(r => r.Recipe.Id.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID)); + + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + var manifest = new ElasticBeanstalkWindowsManifest(); + var deployment = new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments(); + manifest.Deployments.AspNetCoreWeb.Add(deployment); + var zipPath = Path.Combine(tempDirectory, "testZip.zip"); + ZipFile.CreateFromDirectory(recommendation.GetProjectDirectory(), zipPath); + using (var zipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Update)) + { + using (var jsonStream = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(manifest))) + { + var zipEntry = zipArchive.CreateEntry(Constants.ElasticBeanstalk.WindowsManifestName); + using var zipEntryStream = zipEntry.Open(); + jsonStream.Position = 0; + jsonStream.CopyTo(zipEntryStream); + } + } + + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISWebSiteOptionId), "website"); + await _optionSettingHandler.SetOptionSettingValue(recommendation, _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISAppPathOptionId), "apppath"); + + var elasticBeanstalkHandler = new AWSElasticBeanstalkHandler(new Mock().Object, + new Mock().Object, + new Mock().Object, + _optionSettingHandler); + + elasticBeanstalkHandler.SetupWindowsDeploymentManifest(recommendation, zipPath); + + using (FileStream zipToOpen = new FileStream(zipPath, FileMode.Open)) + { + using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Read)) + { + ZipArchiveEntry readmeEntry = archive.GetEntry("aws-windows-deployment-manifest.json"); + var manifestFileJson = readmeEntry.Open(); + var manifestFile = JsonSerializer.Deserialize(manifestFileJson); + Assert.NotNull(manifestFile); + var aspNetCoreWebEntry = Assert.Single(manifestFile.Deployments.AspNetCoreWeb); + Assert.Equal("website", aspNetCoreWebEntry.Parameters.IISWebSite); + Assert.Equal("apppath", aspNetCoreWebEntry.Parameters.IISPath); + } + } + } } } From 3cd1d58524a7bff19c85cb5a56e95dd9200ce61b Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Fri, 23 Sep 2022 21:35:11 -0700 Subject: [PATCH 9/9] feat: Add HTTP3 support for BlazorWasm deployments --- THIRD_PARTY_LICENSES | 2 +- .../AWS.Deploy.Recipes.CDK.Common.csproj | 2 +- .../AspNetAppAppRunner/AspNetAppAppRunner.csproj | 2 +- .../AspNetAppEcsFargate/AspNetAppEcsFargate.csproj | 2 +- .../AspNetAppElasticBeanstalkLinux.csproj | 2 +- .../AspNetAppElasticBeanstalkWindows.csproj | 2 +- .../CdkTemplates/BlazorWasm/BlazorWasm.csproj | 2 +- .../ConsoleAppECSFargateScheduleTask.csproj | 2 +- .../ConsoleAppEcsFargateService.csproj | 2 +- src/AWS.Deploy.Recipes/RecipeDefinitions/BlazorWasm.recipe | 6 ++++-- 10 files changed, 13 insertions(+), 11 deletions(-) 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/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/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 @@ - + - + - +