// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Amazon.CloudFormation; using Amazon.ElasticBeanstalk; using Amazon.ElasticBeanstalk.Model; using AWS.Deploy.Common; using AWS.Deploy.Common.Data; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Orchestration.Data; using AWS.Deploy.Orchestration.LocalUserSettings; using AWS.Deploy.Orchestration.ServiceHandlers; namespace AWS.Deploy.Orchestration.Utilities { public interface IDeployedApplicationQueryer { /// <summary> /// Get the list of existing deployed <see cref="CloudApplication"/> based on the deploymentTypes filter. /// </summary> Task<List<CloudApplication>> GetExistingDeployedApplications(List<DeploymentTypes> deploymentTypes); /// <summary> /// Get the list of compatible applications by matching elements of the CloudApplication RecipeId and the recommendation RecipeId. /// </summary> Task<List<CloudApplication>> GetCompatibleApplications(List<Recommendation> recommendations, List<CloudApplication>? allDeployedApplications = null, OrchestratorSession? session = null); /// <summary> /// Checks if the given recommendation can be used for a redeployment to an existing cloudformation stack. /// </summary> bool IsCompatible(CloudApplication application, Recommendation recommendation); /// <summary> /// Gets the current option settings associated with the cloud application. This method is only used for non-CloudFormation based cloud applications. /// </summary> Task<IDictionary<string, object>> GetPreviousSettings(CloudApplication application, Recommendation recommendation); } 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, IFileManager fileManager) { _awsResourceQueryer = awsResourceQueryer; _localUserSettingsEngine = localUserSettingsEngine; _orchestratorInteractiveService = orchestratorInteractiveService; _fileManager = fileManager; } public async Task<List<CloudApplication>> GetExistingDeployedApplications(List<DeploymentTypes> deploymentTypes) { var existingApplications = new List<CloudApplication>(); if (deploymentTypes.Contains(DeploymentTypes.CdkProject)) existingApplications.AddRange(await GetExistingCloudFormationStacks()); if (deploymentTypes.Contains(DeploymentTypes.BeanstalkEnvironment)) existingApplications.AddRange(await GetExistingBeanstalkEnvironments()); return existingApplications; } /// <summary> /// Filters the applications that can be re-deployed using the current set of available recommendations. /// </summary> /// <returns>A list of <see cref="CloudApplication"/> that are compatible for a re-deployment</returns> public async Task<List<CloudApplication>> GetCompatibleApplications(List<Recommendation> recommendations, List<CloudApplication>? allDeployedApplications = null, OrchestratorSession? session = null) { var compatibleApplications = new List<CloudApplication>(); if (allDeployedApplications == null) allDeployedApplications = await GetExistingDeployedApplications(recommendations.Select(x => x.Recipe.DeploymentType).ToList()); foreach (var application in allDeployedApplications) { if (recommendations.Any(rec => IsCompatible(application, rec))) { compatibleApplications.Add(application); } } if (session != null) { try { await _localUserSettingsEngine.CleanOrphanStacks(allDeployedApplications.Select(x => x.Name).ToList(), session.ProjectDefinition.ProjectName, session.AWSAccountId, session.AWSRegion); var deploymentManifest = await _localUserSettingsEngine.GetLocalUserSettings(); var lastDeployedStack = deploymentManifest?.LastDeployedStacks? .FirstOrDefault(x => x.Exists(session.AWSAccountId, session.AWSRegion, session.ProjectDefinition.ProjectName)); return compatibleApplications .Select(x => { x.UpdatedByCurrentUser = lastDeployedStack?.Stacks?.Contains(x.Name) ?? false; return x; }) .OrderByDescending(x => x.UpdatedByCurrentUser) .ThenByDescending(x => x.LastUpdatedTime) .ToList(); } catch (FailedToUpdateLocalUserSettingsFileException ex) { _orchestratorInteractiveService.LogErrorMessage(ex.Message); _orchestratorInteractiveService.LogDebugMessage(ex.PrettyPrint()); } catch (InvalidLocalUserSettingsFileException ex) { _orchestratorInteractiveService.LogErrorMessage(ex.Message); _orchestratorInteractiveService.LogDebugMessage(ex.PrettyPrint()); } } return compatibleApplications .OrderByDescending(x => x.LastUpdatedTime) .ToList(); } /// <summary> /// Checks if the given recommendation can be used for a redeployment to an existing cloudformation stack. /// </summary> public bool IsCompatible(CloudApplication application, Recommendation recommendation) { // For persisted projects check both the recipe id and the base recipe id for compatibility. The base recipe id check is for stacks that // were first created by a system recipe and then later moved to a persisted deployment project. if (recommendation.Recipe.PersistedDeploymentProject) { return string.Equals(recommendation.Recipe.Id, application.RecipeId, StringComparison.Ordinal) || string.Equals(recommendation.Recipe.BaseRecipeId, application.RecipeId, StringComparison.Ordinal); } return string.Equals(recommendation.Recipe.Id, application.RecipeId, StringComparison.Ordinal); } /// <summary> /// Gets the current option settings associated with the cloud application.This method is only used for non-CloudFormation based cloud applications. /// </summary> public async Task<IDictionary<string, object>> GetPreviousSettings(CloudApplication application, Recommendation recommendation) { IDictionary<string, object> previousSettings; switch (application.ResourceType) { case CloudApplicationResourceType.BeanstalkEnvironment: 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}"); } return previousSettings; } /// <summary> /// Fetches existing CloudFormation stacks created by the AWS .NET deployment tool /// </summary> /// <returns>A list of <see cref="CloudApplication"/></returns> private async Task<List<CloudApplication>> GetExistingCloudFormationStacks() { var stacks = await _awsResourceQueryer.GetCloudFormationStacks(); var apps = new List<CloudApplication>(); foreach (var stack in stacks) { // Check to see if stack has AWS .NET deployment tool tag and the stack is not deleted or in the process of being deleted. var deployTag = stack.Tags.FirstOrDefault(tags => string.Equals(tags.Key, Constants.CloudFormationIdentifier.STACK_TAG)); // Skip stacks that don't have AWS .NET deployment tool tag if (deployTag == null || // Skip stacks does not have AWS .NET deployment tool description prefix. (This is filter out stacks that have the tag propagated to it like the Beanstalk stack) (stack.Description == null || !stack.Description.StartsWith(Constants.CloudFormationIdentifier.STACK_DESCRIPTION_PREFIX)) || // Skip tags that are deleted or in the process of being deleted stack.StackStatus.ToString().StartsWith("DELETE")) { continue; } // ROLLBACK_COMPLETE occurs when a stack creation fails and successfully rollbacks with cleaning partially created resources. // In this state, only a delete operation can be performed. (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html) // We don't want to include ROLLBACK_COMPLETE because it never succeeded to deploy. // However, a customer can give name of new application same as ROLLBACK_COMPLETE stack, which will trigger the re-deployment flow on the ROLLBACK_COMPLETE stack. if (stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) { continue; } // If a list of compatible recommendations was given then skip existing applications that were used with a // recipe that is not compatible. var recipeId = deployTag.Value; apps.Add(new CloudApplication(stack.StackName, stack.StackId, CloudApplicationResourceType.CloudFormationStack, recipeId, stack.LastUpdatedTime)); } return apps; } /// <summary> /// Fetches existing Elastic Beanstalk environments that can serve as a deployment target. /// These environments must have a valid dotnet specific platform arn. /// Any environment that was created via the AWS .NET deployment tool as part of a CloudFormation stack is not included. /// </summary> /// <returns>A list of <see cref="CloudApplication"/></returns> private async Task<List<CloudApplication>> GetExistingBeanstalkEnvironments() { var validEnvironments = new List<CloudApplication>(); var environments = await _awsResourceQueryer.ListOfElasticBeanstalkEnvironments(); if (!environments.Any()) return validEnvironments; 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(); foreach (var env in environments) { var tags = await _awsResourceQueryer.ListElasticBeanstalkResourceTags(env.EnvironmentArn); // skips all environments that were created via the deploy tool. if (tags.Any(x => string.Equals(x.Key, Constants.CloudFormationIdentifier.STACK_TAG))) continue; 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<IDictionary<string, object>> GetBeanstalkEnvironmentConfigurationSettings(string environmentName, string recipeId, string projectPath) { IDictionary<string, object> optionSettings = new Dictionary<string, object>(); var configurationSettings = await _awsResourceQueryer.GetBeanstalkEnvironmentConfigurationSettings(environmentName); 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); if (string.IsNullOrEmpty(configurationSetting?.Value)) continue; optionSettings[tuple.OptionSettingId] = configurationSetting.Value; } if (recipeId.Equals(Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID)) { var windowsManifest = await GetBeanstalkWindowsManifest(projectPath); if (windowsManifest != null && windowsManifest.Deployments.AspNetCoreWeb.Count != 0) { optionSettings[Constants.ElasticBeanstalk.IISWebSiteOptionId] = windowsManifest.Deployments.AspNetCoreWeb[0].Parameters.IISWebSite; optionSettings[Constants.ElasticBeanstalk.IISAppPathOptionId] = windowsManifest.Deployments.AspNetCoreWeb[0].Parameters.IISPath; } } return optionSettings; } private async Task<ElasticBeanstalkWindowsManifest?> GetBeanstalkWindowsManifest(string projectPath) { try { var manifestPath = Path.Combine(Path.GetDirectoryName(projectPath) ?? string.Empty, Constants.ElasticBeanstalk.WindowsManifestName); if (_fileManager.Exists(manifestPath)) { var manifest = JsonSerializer.Deserialize<ElasticBeanstalkWindowsManifest>(await _fileManager.ReadAllTextAsync(manifestPath), new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip }); return manifest; } return null; } catch (Exception ex) { throw new InvalidWindowsManifestFileException( DeployToolErrorCode.InvalidWindowsManifestFile, $"We detected a malformed Elastic Beanstalk Windows manifest file '{Constants.ElasticBeanstalk.WindowsManifestName}' in your project and were not able to load the previous settings from that file.", ex); } } private ConfigurationOptionSetting? GetBeanstalkEnvironmentConfigurationSetting(List<ConfigurationOptionSetting> configurationSettings, string optionNameSpace, string optionName) { var configurationSetting = configurationSettings .FirstOrDefault(x => string.Equals(optionNameSpace, x.Namespace) && string.Equals(optionName, x.OptionName)); return configurationSetting; } } }