Skip to content

Commit

Permalink
Extract CloudFormationInterpolatingEruby class
Browse files Browse the repository at this point in the history
  • Loading branch information
orien committed Feb 2, 2024
1 parent feab2eb commit 2a2cdf3
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 50 deletions.
1 change: 1 addition & 0 deletions lib/stack_master.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ 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 :StackDiffer, 'stack_master/stack_differ'
autoload :Validator, 'stack_master/validator'
Expand Down
62 changes: 62 additions & 0 deletions lib/stack_master/cloudformation_interpolating_eruby.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require 'erubis'

module StackMaster
# This class is a modified version of `Erubis::Eruby`. It allows using
# `<%= %>` ERB expressions to interpolate values into a source string. We use
# this capability to enrich user data scripts with data and parameters pulled
# from the AWS CloudFormation service. The evaluation produces an array of
# objects ready for use in a CloudFormation `Fn::Join` intrinsic function.
class CloudFormationInterpolatingEruby < Erubis::Eruby
include Erubis::ArrayEnhancer

# A convenience method that loads a template from a file and evaluates it.
#
# @param source_path [String] The path to the file to evaluate.
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

# @return [Array] The result of evaluating the source: an array of strings
# from the source intermindled with Hash objects from the ERB
# expressions. To be included in a CloudFormation template, this
# value needs to be used in a CloudFormation `Fn::Join` intrinsic
# function.
# @see Erubis::Eruby#evaluate
# @example
# CloudFormationInterpolatingEruby.new("my_variable=<%= { 'Ref' => 'Param1' } %>;").evaluate
# #=> ['my_variable=', { 'Ref' => 'Param1' }, ';']
def evaluate(_context = Erubis::Context.new)
format_lines_for_cloudformation(super)
end

# @see Erubis::Eruby#add_expr
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
52 changes: 2 additions & 50 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 Down
62 changes: 62 additions & 0 deletions spec/stack_master/cloudformation_interpolating_eruby_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
RSpec.describe(StackMaster::CloudFormationInterpolatingEruby) do
describe('#evaluate') do
subject(:evaluate) { described_class.new(user_data).evaluate }

context('given a simple user data script') do
let(:user_data) { <<~SHELL }
#!/bin/bash
REGION=ap-southeast-2
echo $REGION
SHELL

it 'returns an array of lines' do
expect(evaluate).to eq([
"#!/bin/bash\n",
"\n",
"REGION=ap-southeast-2\n",
"echo $REGION\n",
])
end
end

context('given a user data script referring parameters') do
let(:user_data) { <<~SHELL }
#!/bin/bash
<%= { 'Ref' => 'Param1' } %> <%= { 'Ref' => 'Param2' } %>
SHELL

it 'includes CloudFormation objects in the array' do
expect(evaluate).to eq([
"#!/bin/bash\n",
{ 'Ref' => 'Param1' },
' ',
{ 'Ref' => 'Param2' },
"\n",
])
end
end
end

describe('.evaluate_file') do
subject(:evaluate_file) { described_class.evaluate_file('my/userdata.sh') }

context('given a simple user data script file') do
before { allow(File).to receive(:read).with('my/userdata.sh').and_return(<<~SHELL) }
#!/bin/bash
REGION=ap-southeast-2
echo $REGION
SHELL

it 'returns an array of lines' do
expect(evaluate_file).to eq([
"#!/bin/bash\n",
"\n",
"REGION=ap-southeast-2\n",
"echo $REGION\n",
])
end
end
end
end

0 comments on commit 2a2cdf3

Please sign in to comment.