// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using AWS.Deploy.Common; using AWS.Deploy.Common.Extensions; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Recipes.Validation; namespace AWS.Deploy.Orchestration { public class OptionSettingHandler : IOptionSettingHandler { private readonly IValidatorFactory _validatorFactory; public OptionSettingHandler(IValidatorFactory validatorFactory) { _validatorFactory = validatorFactory; } /// /// This method runs all the option setting validators for the configurable settings. /// In case of a first time deployment, all settings and validators are run. /// In case of a redeployment, only the updatable settings are considered. /// public List RunOptionSettingValidators(Recommendation recommendation, IEnumerable? optionSettings = null) { if (optionSettings == null) optionSettings = recommendation.GetConfigurableOptionSettingItems().Where(x => !recommendation.IsExistingCloudApplication || x.Updatable); List settingValidatorFailedResults = new List(); foreach (var optionSetting in optionSettings) { if (!IsOptionSettingDisplayable(recommendation, optionSetting)) { optionSetting.Validation.ValidationStatus = ValidationStatus.Valid; optionSetting.Validation.ValidationMessage = string.Empty; optionSetting.Validation.InvalidValue = null; continue; } var optionSettingValue = GetOptionSettingValue(recommendation, optionSetting); var failedValidators = _validatorFactory.BuildValidators(optionSetting) .Select(async validator => await validator.Validate(optionSettingValue, recommendation, optionSetting)) .Select(x => x.Result) .Where(x => !x.IsValid) .ToList(); if (failedValidators.Any()) { optionSetting.Validation.ValidationStatus = ValidationStatus.Invalid; optionSetting.Validation.ValidationMessage = string.Join(Environment.NewLine, failedValidators.Select(x => x.ValidationFailedMessage)).Trim(); optionSetting.Validation.InvalidValue = optionSettingValue; } else { optionSetting.Validation.ValidationStatus = ValidationStatus.Valid; optionSetting.Validation.ValidationMessage = string.Empty; optionSetting.Validation.InvalidValue = null; } settingValidatorFailedResults.AddRange(failedValidators); settingValidatorFailedResults.AddRange(RunOptionSettingValidators(recommendation, optionSetting.ChildOptionSettings)); } return settingValidatorFailedResults; } /// /// Assigns a value to the OptionSettingItem. /// /// /// Thrown if one or more determine /// is not valid. /// public async Task SetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSettingItem, object value, bool skipValidation = false) { IOptionSettingItemValidator[] validators = new IOptionSettingItemValidator[0]; if (!skipValidation && IsOptionSettingDisplayable(recommendation, optionSettingItem)) validators = _validatorFactory.BuildValidators(optionSettingItem); await optionSettingItem.SetValue(this, value, validators, recommendation, skipValidation); if (!skipValidation) RunOptionSettingValidators(recommendation, optionSettingItem.Dependents.Select(x => GetOptionSetting(recommendation, x))); // If the optionSettingItem came from the selected recommendation's deployment bundle, // set the corresponding property on recommendation.DeploymentBundle SetDeploymentBundleProperty(recommendation, optionSettingItem, value); } /// /// Assigns a value to the OptionSettingItem based on the fullyQualifiedId /// /// /// Thrown if one or more determine /// is not valid. /// /// /// Thrown if there doesn't exist an option setting with the given fullyQualifiedId /// public async Task SetOptionSettingValue(Recommendation recommendation, string fullyQualifiedId, object value, bool skipValidation = false) { var optionSetting = GetOptionSetting(recommendation, fullyQualifiedId); await SetOptionSettingValue(recommendation, optionSetting, value, skipValidation); } /// /// Sets the corresponding value in when the /// corresponding was just set /// /// Selected recommendation /// Option setting that was just set /// Value that was just set, assumed to be valid private void SetDeploymentBundleProperty(Recommendation recommendation, OptionSettingItem optionSettingItem, object value) { switch (optionSettingItem.Id) { case Constants.Docker.DockerExecutionDirectoryOptionId: recommendation.DeploymentBundle.DockerExecutionDirectory = value.ToString() ?? string.Empty; break; case Constants.Docker.DockerfileOptionId: recommendation.DeploymentBundle.DockerfilePath = value.ToString() ?? string.Empty; break; case Constants.Docker.DockerBuildArgsOptionId: recommendation.DeploymentBundle.DockerBuildArgs = value.ToString() ?? string.Empty; break; case Constants.Docker.ECRRepositoryNameOptionId: recommendation.DeploymentBundle.ECRRepositoryName = value.ToString() ?? string.Empty; break; case Constants.RecipeIdentifier.DotnetPublishConfigurationOptionId: recommendation.DeploymentBundle.DotnetPublishBuildConfiguration = value.ToString() ?? string.Empty; break; case Constants.RecipeIdentifier.DotnetPublishArgsOptionId: recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments = value.ToString() ?? string.Empty; break; case Constants.RecipeIdentifier.DotnetPublishSelfContainedBuildOptionId: recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild = Convert.ToBoolean(value); break; default: return; } } /// /// Interactively traverses given json path and returns target option setting. /// Throws exception if there is no that matches /> /// In case an option setting of type is encountered, /// that can have the key value pair name as the leaf node with the option setting Id as the node before that. /// /// /// Dot (.) separated key values string pointing to an option setting. /// Read more /// /// Option setting at the json path. Throws if there doesn't exist an option setting. public OptionSettingItem GetOptionSetting(Recommendation recommendation, string? jsonPath) { if (string.IsNullOrEmpty(jsonPath)) throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"The Option Setting Item {jsonPath} does not exist as part of the" + $" {recommendation.Recipe.Name} recipe"); var ids = jsonPath.Split('.'); OptionSettingItem? optionSetting = null; for (int i = 0; i < ids.Length; i++) { var optionSettings = optionSetting?.ChildOptionSettings ?? recommendation.GetConfigurableOptionSettingItems(); optionSetting = optionSettings.FirstOrDefault(os => os.Id.Equals(ids[i])); if (optionSetting == null) { throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"The Option Setting Item {jsonPath} does not exist as part of the" + $" {recommendation.Recipe.Name} recipe"); } if (optionSetting.Type.Equals(OptionSettingValueType.KeyValue)) { return optionSetting; } } return optionSetting!; } /// /// Interactively traverses given json path and returns target option setting. /// Throws exception if there is no that matches /> /// In case an option setting of type is encountered, /// that can have the key value pair name as the leaf node with the option setting Id as the node before that. /// /// /// Dot (.) separated key values string pointing to an option setting. /// Read more /// /// Option setting at the json path. Throws if there doesn't exist an option setting. public OptionSettingItem GetOptionSetting(RecipeDefinition recipe, string? jsonPath) { if (string.IsNullOrEmpty(jsonPath)) throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"An option setting item with the specified fully qualified Id '{jsonPath}' cannot be found in the" + $" '{recipe.Name}' recipe."); var ids = jsonPath.Split('.'); OptionSettingItem? optionSetting = null; for (int i = 0; i < ids.Length; i++) { var optionSettings = optionSetting?.ChildOptionSettings ?? recipe.OptionSettings; optionSetting = optionSettings.FirstOrDefault(os => os.Id.Equals(ids[i])); if (optionSetting == null) { throw new OptionSettingItemDoesNotExistException(DeployToolErrorCode.OptionSettingItemDoesNotExistInRecipe, $"An option setting item with the specified fully qualified Id '{jsonPath}' cannot be found in the" + $" '{recipe.Name}' recipe."); } if (optionSetting.Type.Equals(OptionSettingValueType.KeyValue)) { return optionSetting; } } return optionSetting!; } /// /// Retrieves the value of the Option Setting Item in a given recommendation. /// public T? GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting) { var displayableOptionSettings = new Dictionary(); if (optionSetting.Type == OptionSettingValueType.Object) { foreach (var childOptionSetting in optionSetting.ChildOptionSettings) { displayableOptionSettings.Add(childOptionSetting.Id, IsOptionSettingDisplayable(recommendation, childOptionSetting)); } } return optionSetting.GetValue(recommendation.ReplacementTokens, displayableOptionSettings); } /// /// Retrieves the value of the Option Setting Item in a given recommendation. /// public object GetOptionSettingValue(Recommendation recommendation, OptionSettingItem optionSetting) { var displayableOptionSettings = new Dictionary(); if (optionSetting.Type == OptionSettingValueType.Object) { foreach (var childOptionSetting in optionSetting.ChildOptionSettings) { displayableOptionSettings.Add(childOptionSetting.Id, IsOptionSettingDisplayable(recommendation, childOptionSetting)); } } return optionSetting.GetValue(recommendation.ReplacementTokens, displayableOptionSettings); } /// /// Retrieves the default value of the Option Setting Item in a given recommendation. /// public T? GetOptionSettingDefaultValue(Recommendation recommendation, OptionSettingItem optionSetting) { return optionSetting.GetDefaultValue(recommendation.ReplacementTokens); } /// /// Retrieves the default value of the Option Setting Item in a given recommendation. /// public object? GetOptionSettingDefaultValue(Recommendation recommendation, OptionSettingItem optionSetting) { return optionSetting.GetDefaultValue(recommendation.ReplacementTokens); } /// /// Checks whether all the dependencies are satisfied or not, if there exists an unsatisfied dependency then returns false. /// It allows caller to decide whether we want to display an to configure or not. /// /// Returns true, if all the dependencies are satisfied, else false. public bool IsOptionSettingDisplayable(Recommendation recommendation, OptionSettingItem optionSetting) { if (!optionSetting.DependsOn.Any()) { return true; } foreach (var dependency in optionSetting.DependsOn) { var dependsOnOptionSetting = GetOptionSetting(recommendation, dependency.Id); var dependsOnOptionSettingValue = GetOptionSettingValue(recommendation, dependsOnOptionSetting); if ( dependsOnOptionSetting != null) { if (dependsOnOptionSettingValue == null) { if (dependency.Operation == null || dependency.Operation == PropertyDependencyOperationType.Equals) { if (dependency.Value != null) return false; } else if (dependency.Operation == PropertyDependencyOperationType.NotEmpty) { return false; } } else { if (dependency.Operation == null || dependency.Operation == PropertyDependencyOperationType.Equals) { if (!dependsOnOptionSettingValue.Equals(dependency.Value)) return false; } else if (dependency.Operation == PropertyDependencyOperationType.NotEmpty) { if (dependsOnOptionSetting.Type == OptionSettingValueType.List && dependsOnOptionSettingValue.TryDeserialize>(out var listValue) && listValue != null && !listValue.Any()) { return false; } else if (dependsOnOptionSetting.Type == OptionSettingValueType.KeyValue && dependsOnOptionSettingValue.TryDeserialize>(out var keyValue) && keyValue != null && !keyValue.Any()) { return false; } else if (string.IsNullOrEmpty(dependsOnOptionSettingValue?.ToString())) { return false; } } } } } return true; } /// /// Checks whether the Option Setting Item can be displayed as part of the settings summary of the previous deployment. /// public bool IsSummaryDisplayable(Recommendation recommendation, OptionSettingItem optionSettingItem) { if (!IsOptionSettingDisplayable(recommendation, optionSettingItem)) return false; var value = GetOptionSettingValue(recommendation, optionSettingItem); if (string.IsNullOrEmpty(value?.ToString())) return false; return true; } /// /// Checks whether the option setting item has been modified by the user. If it has been modified, then it will hold a non-default value /// /// true if the option setting item has been modified or false otherwise public bool IsOptionSettingModified(Recommendation recommendation, OptionSettingItem optionSetting) { // If the option setting is not displayable, that means its dependencies are not satisfied and it does not play any role in the deployment. // We do not need to evaluate whether it has been modified or not. if (!IsOptionSettingDisplayable(recommendation, optionSetting)) { return false; } if (optionSetting.Type.Equals(OptionSettingValueType.List)) { var currentSet = GetOptionSettingValue>(recommendation, optionSetting) ?? new SortedSet(); var defaultSet = GetOptionSettingDefaultValue>(recommendation, optionSetting) ?? new SortedSet(); // return true if both have different lengths or all elements in currentSet are not present in defaultSet return defaultSet.Count != currentSet.Count || !currentSet.All(x => defaultSet.Contains(x)); } if (optionSetting.Type.Equals(OptionSettingValueType.KeyValue)) { var currentDict = GetOptionSettingValue>(recommendation, optionSetting) ?? new Dictionary(); var defaultDict = GetOptionSettingDefaultValue>(recommendation, optionSetting) ?? new Dictionary(); // return true if both have different lengths or all keyValue pairs are not equal between currentDict and defaultDict return defaultDict.Count != currentDict.Count || !currentDict.All(keyPair => defaultDict.ContainsKey(keyPair.Key) && string.Equals(defaultDict[keyPair.Key], currentDict[keyPair.Key])); } if (optionSetting.Type.Equals(OptionSettingValueType.Int)) { var currentValue = GetOptionSettingValue(recommendation, optionSetting); var defaultValue = GetOptionSettingDefaultValue(recommendation, optionSetting); return defaultValue != currentValue; } if (optionSetting.Type.Equals(OptionSettingValueType.Double)) { var currentValue = GetOptionSettingValue(recommendation, optionSetting); var defaultValue = GetOptionSettingDefaultValue(recommendation, optionSetting); return defaultValue != currentValue; } if (optionSetting.Type.Equals(OptionSettingValueType.Bool)) { var currentValue = GetOptionSettingValue(recommendation, optionSetting); var defaultValue = GetOptionSettingDefaultValue(recommendation, optionSetting); return defaultValue != currentValue; } if (optionSetting.Type.Equals(OptionSettingValueType.String)) { var currentValue = GetOptionSettingValue(recommendation, optionSetting); var defaultValue = GetOptionSettingDefaultValue(recommendation, optionSetting); if (string.IsNullOrEmpty(currentValue) && string.IsNullOrEmpty(defaultValue)) return false; return !string.Equals(currentValue, defaultValue); } // The option setting is of type Object and it has nested child settings. // return true is any of the child settings are modified. foreach (var childSetting in optionSetting.ChildOptionSettings) { if (IsOptionSettingModified(recommendation, childSetting)) return true; } 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; } } }