// 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.Text; using System.Threading.Tasks; using Amazon.CloudFormation.Model; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using Newtonsoft.Json; using YamlDotNet.RepresentationModel; namespace AWS.Deploy.Orchestration.Utilities { public interface ICloudFormationTemplateReader { /// /// For a given Cloud Application loads the metadata for it. This includes the settings used to deploy and the recipe information. /// Task LoadCloudApplicationMetadata(string cloudApplication); /// /// Parses the CDK Bootstrap template that is used by the tool and retrieves the template version. /// Task ReadCDKTemplateVersion(); } /// /// A class for reading the metadata section of an CloudFormation template to pull out the AWS .NET deployment tool settings. /// public class CloudFormationTemplateReader : ICloudFormationTemplateReader { private readonly IAWSClientFactory _awsClientFactory; private readonly IDeployToolWorkspaceMetadata _deployToolWorkspaceMetadata; private readonly IFileManager _fileManager; public CloudFormationTemplateReader(IAWSClientFactory awsClientFactory, IDeployToolWorkspaceMetadata deployToolWorkspaceMetadata, IFileManager fileManager) { _awsClientFactory = awsClientFactory; _deployToolWorkspaceMetadata = deployToolWorkspaceMetadata; _fileManager = fileManager; } public async Task LoadCloudApplicationMetadata(string cloudApplication) { using var client = _awsClientFactory.GetAWSClient(); var request = new GetTemplateRequest { StackName = cloudApplication }; var response = await client.GetTemplateAsync(request); if(IsJsonCFTemplate(response.TemplateBody)) return ReadSettingsFromJSONCFTemplate(response.TemplateBody); else return ReadSettingsFromYAMLCFTemplate(response.TemplateBody); } private async Task LoadCDKBootstrapTemplate() { return await _fileManager.ReadAllTextAsync(_deployToolWorkspaceMetadata.CDKBootstrapTemplatePath); } /// /// Parses the CDK Bootstrap template that is used by the tool and retrieves the template version. /// public async Task ReadCDKTemplateVersion() { try { var template = await LoadCDKBootstrapTemplate(); var yamlMetadata = new YamlStream(); using var reader = new StringReader(template); yamlMetadata.Load(reader); var root = (YamlMappingNode)yamlMetadata.Documents[0].RootNode; // The YAML path to the version is: .Resources.CdkBootstrapVersion.Properties.Value var resourcesNode = (YamlMappingNode)root.Children[new YamlScalarNode("Resources")]; var cdkBootstrapVersionNode = (YamlMappingNode)resourcesNode.Children[new YamlScalarNode("CdkBootstrapVersion")]; var propertiesNode = (YamlMappingNode)cdkBootstrapVersionNode.Children[new YamlScalarNode("Properties")]; var valueNode = (YamlScalarNode)propertiesNode.Children[new YamlScalarNode("Value")]; var cdkBootstrapVersion = valueNode.Value ?? String.Empty; return int.Parse(cdkBootstrapVersion); } catch(Exception ex) { throw new FailedToReadCdkBootstrapVersionException(DeployToolErrorCode.FailedToReadCdkBootstrapVersion, "Failed to read the CDK Bootstrap version from the Template file.", ex); } } /// /// Read the AWS .NET deployment tool metadata from the CloudFormation template which is in JSON format. /// /// private static CloudApplicationMetadata ReadSettingsFromJSONCFTemplate(string templateBody) { try { var cfTemplate = JsonConvert.DeserializeObject(templateBody); var cloudApplicationMetadata = new CloudApplicationMetadata( cfTemplate?.Metadata?[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_ID] ?? throw new Exception("Error parsing existing application's metadata to retrieve Recipe ID."), cfTemplate?.Metadata?[Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_VERSION] ?? throw new Exception("Error parsing existing application's metadata to retrieve Recipe Version.") ); 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) { throw new ParsingExistingCloudApplicationMetadataException(DeployToolErrorCode.ErrorParsingApplicationMetadata, "Error parsing existing application's metadata", e); } } /// /// Read the AWS .NET deployment tool metadata from the CloudFormation template which is in YAML format. /// /// private static CloudApplicationMetadata ReadSettingsFromYAMLCFTemplate(string templateBody) { try { var metadataSection = ExtractMetadataSection(templateBody); var yamlMetadata = new YamlStream(); using var reader = new StringReader(metadataSection); yamlMetadata.Load(reader); var root = (YamlMappingNode)yamlMetadata.Documents[0].RootNode; var metadataNode = (YamlMappingNode)root.Children[new YamlScalarNode("Metadata")]; var cloudApplicationMetadata = new CloudApplicationMetadata( ((YamlScalarNode)metadataNode.Children[new YamlScalarNode(Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_ID)]).Value ?? throw new Exception("Error parsing existing application's metadata to retrieve Recipe ID."), ((YamlScalarNode)metadataNode.Children[new YamlScalarNode(Constants.CloudFormationIdentifier.STACK_METADATA_RECIPE_VERSION)]).Value ?? throw new Exception("Error parsing existing application's metadata to retrieve Recipe Version.") ); 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) { throw new ParsingExistingCloudApplicationMetadataException(DeployToolErrorCode.ErrorParsingApplicationMetadata, "Error parsing existing application's metadata", e); } } /// /// YamlDotNet does not like CloudFormation short hand notation. To avoid getting any parse failures due to use of the short hand notation /// using string parsing to extract just the Metadata section from the template. /// /// private static string ExtractMetadataSection(string templateBody) { var builder = new StringBuilder(); bool inMetadata = false; using var reader = new StringReader(templateBody); string? line; while((line = reader.ReadLine()) != null) { if(!inMetadata) { // See if we found the start of the Metadata section if(line.StartsWith("Metadata:")) { builder.AppendLine(line); inMetadata = true; } } else { // See if we have found the next top level node signaling the end of the Metadata section if (line.Length > 0 && char.IsLetterOrDigit(line[0])) { break; } builder.AppendLine(line); } } return builder.ToString(); } private bool IsJsonCFTemplate(string templateBody) { try { JsonConvert.DeserializeObject(templateBody); return true; } catch { return false; } } } public class CFTemplate { public Dictionary? Metadata { get; set; } } }