Skip to content

Commit

Permalink
Provide user_data_file helper method for YAML ERB templates
Browse files Browse the repository at this point in the history
  • Loading branch information
orien committed Feb 2, 2024
1 parent 253e9e6 commit f39486e
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 53 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions lib/stack_master.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
53 changes: 53 additions & 0 deletions lib/stack_master/cloudformation_interpolating_eruby.rb
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions lib/stack_master/cloudformation_template_eruby.rb
Original file line number Diff line number Diff line change
@@ -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
53 changes: 2 additions & 51 deletions lib/stack_master/sparkle_formation/template_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

3 changes: 1 addition & 2 deletions lib/stack_master/template_compilers/yaml_erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f39486e

Please sign in to comment.