diff --git a/CHANGELOG.md b/CHANGELOG.md index 9089c7b9..b69ab6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,12 @@ The format is based on [Keep a Changelog], and this project adheres to - Test on Ruby 3.3 in the CI build ([#376]). +- Introduce `user_data_file`, `user_data_file_as_lines`, and `include_file` + convenience methods to the YAML ERB template compiler ([#377]). + [Unreleased]: https://github.com/envato/stack_master/compare/v2.13.4...HEAD [#376]: https://github.com/envato/stack_master/pull/376 +[#377]: https://github.com/envato/stack_master/pull/377 ## [2.13.4] - 2023-08-02 diff --git a/lib/stack_master.rb b/lib/stack_master.rb index f52904e5..bdc16996 100644 --- a/lib/stack_master.rb +++ b/lib/stack_master.rb @@ -45,6 +45,8 @@ module StackMaster autoload :StackDefinition, 'stack_master/stack_definition' autoload :TemplateCompiler, 'stack_master/template_compiler' autoload :Identity, 'stack_master/identity' + autoload :CloudFormationInterpolatingEruby, 'stack_master/cloudformation_interpolating_eruby' + autoload :CloudFormationTemplateEruby, 'stack_master/cloudformation_template_eruby' autoload :StackDiffer, 'stack_master/stack_differ' autoload :Validator, 'stack_master/validator' diff --git a/lib/stack_master/cloudformation_interpolating_eruby.rb b/lib/stack_master/cloudformation_interpolating_eruby.rb new file mode 100644 index 00000000..5da3f83a --- /dev/null +++ b/lib/stack_master/cloudformation_interpolating_eruby.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'erubis' + +module StackMaster + # This class is a modified version of the `Erubis::Eruby` class. It allows + # for the use of the `<%= %>` expressions to interpolate CloudFormation + # values into the source string. + class CloudFormationInterpolatingEruby < Erubis::Eruby + include Erubis::ArrayEnhancer + + def self.evaluate_file(source_path, context = Erubis::Context.new) + template_contents = File.read(source_path) + eruby = new(template_contents) + eruby.filename = source_path + eruby.evaluate(context) + end + + # Evaluate the source string and return an array of objects ready for use + # in a CloudFormation `Fn::Join` intrinsic function. + # + # @example Produces an array + # CloudFormationInterpolatingEruby.new("my_variable=<%= { 'Ref' => 'Param1' } %>;").evaluate + # #=> ['my_variable=', { 'Ref' => 'Param1' }, ';'] + def evaluate(context = Erubis::Context.new) + format_lines_for_cloudformation(super) + end + + def add_expr(src, code, indicator) + if indicator == '=' + src << " #{@bufvar} << (" << code << ');' + else + super + end + end + + private + + # Split up long strings containing multiple lines. One string per line in the + # CloudFormation array makes the compiled template and diffs more readable. + def format_lines_for_cloudformation(source) + source.flat_map do |lines| + lines = lines.to_s if lines.is_a?(Symbol) + next(lines) unless lines.is_a?(String) + + newlines = Array.new(lines.count("\n"), "\n") + newlines = lines.split("\n").map { |line| "#{line}#{newlines.pop}" } + newlines.insert(0, "\n") if lines.start_with?("\n") + newlines + end + end + end +end diff --git a/lib/stack_master/cloudformation_template_eruby.rb b/lib/stack_master/cloudformation_template_eruby.rb new file mode 100644 index 00000000..24807494 --- /dev/null +++ b/lib/stack_master/cloudformation_template_eruby.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'erubis' +require 'json' + +module StackMaster + # This class is a modified version of the `Erubis::Eruby` class. It + # provides extra helper methods to ease the creation of CloudFormation + # templates. These helper methods are available within ERB `<%= %> + # expressions`. + class CloudFormationTemplateEruby < Erubis::Eruby + # Adds the contents of an EC2 userdata script to the CloudFormation + # template. + # + # Allows using the ERB `<%= %>` expressions within the user data script + # to interpolate CloudFormation values. + def user_data_file(filepath) + JSON.pretty_generate({ 'Fn::Base64' => { 'Fn::Join' => ['', user_data_file_as_lines(filepath)] } }) + end + + # Evaluate the ERB file and return the result as an array of lines. + # + # Allows using ERB `<%= %>` expressions to interpolate CloudFormation + # objects into the result. + def user_data_file_as_lines(filepath) + StackMaster::CloudFormationInterpolatingEruby.evaluate_file(filepath, self) + end + + # Add the contents of another file into the CloudFormation template as a + # string. ERB `<%= %>` expressions are not evaluated. + def include_file(filepath) + JSON.pretty_generate(File.read(filepath)) + end + end +end diff --git a/lib/stack_master/sparkle_formation/template_file.rb b/lib/stack_master/sparkle_formation/template_file.rb index 1ca35cb7..858ec1b0 100644 --- a/lib/stack_master/sparkle_formation/template_file.rb +++ b/lib/stack_master/sparkle_formation/template_file.rb @@ -5,19 +5,6 @@ module StackMaster module SparkleFormation TemplateFileNotFound = ::Class.new(StandardError) - class SfEruby < Erubis::Eruby - include Erubis::ArrayEnhancer - - def add_expr(src, code, indicator) - case indicator - when '=' - src << " #{@bufvar} << (" << code << ');' - else - super - end - end - end - class TemplateContext < AttributeStruct include ::SparkleFormation::SparkleAttribute include ::SparkleFormation::SparkleAttribute::Aws @@ -49,47 +36,12 @@ def render(file_name, vars = {}) end end - # Splits up long strings with multiple lines in them to multiple strings - # in the CF array. Makes the compiled template and diffs more readable. - class CloudFormationLineFormatter - def self.format(template) - new(template).format - end - - def initialize(template) - @template = template - end - - def format - @template.flat_map do |lines| - lines = lines.to_s if Symbol === lines - if String === lines - newlines = [] - lines.count("\n").times do - newlines << "\n" - end - newlines = lines.split("\n").map do |line| - "#{line}#{newlines.pop}" - end - if lines.start_with?("\n") - newlines.insert(0, "\n") - end - newlines - else - lines - end - end - end - end - module Template def self.render(prefix, file_name, vars) file_path = File.join(::SparkleFormation.sparkle_path, prefix, file_name) - template = File.read(file_path) template_context = TemplateContext.build(vars, prefix) - compiled_template = SfEruby.new(template).evaluate(template_context) - CloudFormationLineFormatter.format(compiled_template) - rescue Errno::ENOENT => e + CloudFormationInterpolatingEruby.evaluate_file(file_path, template_context) + rescue Errno::ENOENT Kernel.raise TemplateFileNotFound, "Could not find template file at path: #{file_path}" end end @@ -112,4 +64,3 @@ def _user_data_file(file_name, vars = {}) SparkleFormation::SparkleAttribute::Aws.send(:include, StackMaster::SparkleFormation::UserDataFile) SparkleFormation::SparkleAttribute::Aws.send(:include, StackMaster::SparkleFormation::JoinedFile) - diff --git a/lib/stack_master/template_compilers/yaml_erb.rb b/lib/stack_master/template_compilers/yaml_erb.rb index 79c0df14..57bed748 100644 --- a/lib/stack_master/template_compilers/yaml_erb.rb +++ b/lib/stack_master/template_compilers/yaml_erb.rb @@ -3,13 +3,12 @@ module StackMaster::TemplateCompilers class YamlErb def self.require_dependencies - require 'erubis' require 'yaml' end def self.compile(template_dir, template, compile_time_parameters, _compiler_options = {}) template_file_path = File.join(template_dir, template) - template = Erubis::Eruby.new(File.read(template_file_path)) + template = StackMaster::CloudFormationTemplateEruby.new(File.read(template_file_path)) template.filename = template_file_path template.result(params: compile_time_parameters)