-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #377 from envato/yaml-template-userdata
Introduce `user_data_file` helper method for YAML ERB templates
- Loading branch information
Showing
12 changed files
with
368 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# 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 | ||
|
||
# Load a template from a file at the specified path and evaluate it. | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'erubis' | ||
require 'json' | ||
|
||
module StackMaster | ||
# This class is a modified version of `Erubis::Eruby`. It provides extra | ||
# helper methods to ease the dynamic creation of CloudFormation templates | ||
# with ERB. These helper methods are available within `<%= %>` 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 template at the specified filepath 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 within the referenced file are not | ||
# evaluated. | ||
def include_file(filepath) | ||
JSON.pretty_generate(File.read(filepath)) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
module StackMaster | ||
VERSION = "2.13.4" | ||
VERSION = "2.14.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#!/bin/bash | ||
|
||
echo 'Hello, World!' | ||
REGION=<%= { 'Ref' => 'AWS::Region' } %> | ||
echo $REGION |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Description: A test case for storing the userdata script in a dedicated file | ||
|
||
Resources: | ||
LaunchConfig: | ||
Type: 'AWS::AutoScaling::LaunchConfiguration' | ||
Properties: | ||
UserData: <%= user_data_file(File.join(__dir__, 'user_data.sh.erb')) %> |
62 changes: 62 additions & 0 deletions
62
spec/stack_master/cloudformation_interpolating_eruby_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.