Skip to content

Commit

Permalink
Merge pull request #14 from ktheory/stack-outputs
Browse files Browse the repository at this point in the history
Support stack outputs as parameters in cfn-flow.yml
  • Loading branch information
ktheory committed Oct 30, 2015
2 parents 3cfa057 + f4f6dd3 commit 7a695cb
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 61 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ stack:
# Your parameters, e.g.:
vpcid: vpc-1234
ami: ami-abcd

##
# Use outputs from other stacks

# This set the `load_balancer` parameter to the value of the
# `elbname` output of `my-elb-stack`
load_balancer:
stack: my-elb-stack
output: elbname

# If you don't specify the output name, it's assumed to be same
# as the parameter key:
ssh_security_group:
stack: my-bastion-stack

disable_rollback: true,
timeout_in_minutes: 1,
notification_arns: ["NotificationARN"],
Expand Down Expand Up @@ -229,6 +244,23 @@ stack:
git_sha: <%= `git rev-parse --verify HEAD`.chomp %>
```

#### Use stack outputs as parameters
`cfn-flow` lets you easily reference stack outputs as parameters for new stacks.

```yaml
# cfn-flow.yml
stack:
parameters:
# Set my-param to the `my-param` output of `another-stack`
my-param:
stack: another-stack

# Set my-param to the `my-output` output of `another-stack`
my-param:
stack: another-stack
output: my-output
```
## Usage
Getting help:
Expand Down
43 changes: 7 additions & 36 deletions lib/cfn_flow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,43 +39,11 @@ def stack_params(environment)
unless config['stack'].is_a? Hash
raise Thor::Error.new("No stack defined in #{config_path}. Add 'stack: ...'.")
end
params = StackParams.expanded(config['stack'])

# Dup & symbolize keys
params = config['stack'].map{|k,v| [k.to_sym, v]}.to_h

# Expand params
if params[:parameters].is_a? Hash
expanded_params = params[:parameters].map do |key,value|
{ parameter_key: key, parameter_value: value }
end
params[:parameters] = expanded_params
end

# Expand tags
if params[:tags].is_a? Hash
tags = params[:tags].map do |key, value|
{key: key, value: value}
end

params[:tags] = tags
end

# Append CfnFlow tags
params[:tags] ||= []
params[:tags] << { key: 'CfnFlowService', value: service }
params[:tags] << { key: 'CfnFlowEnvironment', value: environment }

# Expand template body
if params[:template_body].is_a? String
begin
body = CfnFlow::Template.new(params[:template_body]).to_json
params[:template_body] = body
rescue CfnFlow::Template::Error
# Do nothing
end
end

params
params.
add_tag('CfnFlowService' => service).
add_tag('CfnFlowEnvironment' => environment)
end

def template_s3_bucket
Expand Down Expand Up @@ -113,6 +81,7 @@ def cfn_resource
# Clear aws sdk clients & config (for tests)
def clear!
@config = @cfn_client = @cfn_resource = nil
CachedStack.stack_cache.clear
end

# Exit with status code = 1 when raising a Thor::Error
Expand All @@ -131,6 +100,8 @@ def exit_on_failure=(value)
end
end

require 'cfn_flow/cached_stack'
require 'cfn_flow/stack_params'
require 'cfn_flow/template'
require 'cfn_flow/git'
require 'cfn_flow/event_presenter'
Expand Down
32 changes: 32 additions & 0 deletions lib/cfn_flow/cached_stack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module CfnFlow
class CachedStack

class MissingOutput < StandardError; end

def self.stack_cache
@stack_cache ||= {}
end

def self.get_output(stack:, output:)
new(stack).output(output)
end

attr_reader :stack_name

def initialize(stack_name)
@stack_name = stack_name
end

def output(name)
output = stack_cache.outputs.detect{|out| out.output_key == name }
unless output
raise MissingOutput.new("Can't find outpout #{name} for stack #{stack_name}")
end
output.output_value
end

def stack_cache
self.class.stack_cache[stack_name] ||= CfnFlow.cfn_resource.stack(stack_name).load
end
end
end
71 changes: 71 additions & 0 deletions lib/cfn_flow/stack_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module CfnFlow
# Extend hash with some special behavior to generate the
# style of hash aws-sdk expects
class StackParams < Hash

def self.expanded(hash)
self[hash].
with_symbolized_keys.
with_expanded_parameters.
with_expanded_tags.
with_expanded_template_body
end

def with_symbolized_keys
self.inject(StackParams.new) do |accum, pair|
key, value = pair
accum.merge(key.to_sym => value)
end
end

def with_expanded_parameters
return self unless self[:parameters].is_a? Hash

expanded_params = self[:parameters].map do |key,value|
{ parameter_key: key, parameter_value: fetch_value(key, value) }
end

self.merge(parameters: expanded_params)
end

def with_expanded_tags
return self unless self[:tags].is_a? Hash

tags = self[:tags].map do |key, value|
{key: key, value: value}
end

self.merge(tags: tags)
end

def add_tag(hash)
new_tags = hash.map do |k,v|
{key: k, value: v }
end
tags = (self[:tags] || []) + new_tags
self.merge(tags: tags)
end

def with_expanded_template_body
return self unless self[:template_body].is_a? String
body = CfnFlow::Template.new(self[:template_body]).to_json
self.merge(template_body: body)
rescue CfnFlow::Template::Error
# Do nothing
self
end

def fetch_value(key, value)
# Dereference stack output params
if value.is_a?(Hash) && value.key?('stack')
stack_name = value['stack']
stack_output_name = value['output'] || key

value = CachedStack.get_output(stack: stack_name, output: stack_output_name)
else
value
end
end
private :fetch_value
end
end
2 changes: 1 addition & 1 deletion lib/cfn_flow/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module CfnFlow
VERSION = '0.8.0'
VERSION = '0.9.0'
end
71 changes: 71 additions & 0 deletions spec/cfn_flow/cached_stack_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require_relative '../helper'

describe 'CfnFlow::CachedStack' do
subject { CfnFlow::CachedStack }

describe '.stack_cache' do
it 'defaults to a hash' do
subject.stack_cache.must_equal({})
end
end

describe '.get_output' do
let(:output_value) { 'myvalue' }

before do
Aws.config[:cloudformation]= {
stub_responses: {
describe_stacks: { stacks: [ stub_stack_data.merge(outputs: [{ output_key: "myoutput", output_value: output_value } ]) ] }
}
}
end

it 'returns the output' do
subject.get_output(stack: 'mystack', output: 'myoutput').must_equal output_value
end

it 'has required kwargs' do
-> { subject.get_output }.must_raise(ArgumentError)
end
end

describe 'an instance' do
subject { CfnFlow::CachedStack.new('mystack') }
let(:output_value) { 'myvalue' }

before do
Aws.config[:cloudformation]= {
stub_responses: {
describe_stacks: { stacks: [ stub_stack_data.merge(outputs: [{ output_key: "myoutput", output_value: output_value } ]) ] }
}
}
end

it "should return the output value" do
subject.output('myoutput').must_equal output_value
end

describe "with a missing output" do
it "should raise an error" do
-> { subject.output("no-such-output") }.must_raise(CfnFlow::CachedStack::MissingOutput)
end
end

describe "with a missing stack" do

subject { CfnFlow::CachedStack.new('no-such-stack') }
before do
Aws.config[:cloudformation]= {
stub_responses: {
describe_stacks: 'ValidationError'
}
}
end

it "should raise an error" do
-> { subject.output('blah') }.must_raise(Aws::CloudFormation::Errors::ValidationError)
end
end
end

end
Loading

0 comments on commit 7a695cb

Please sign in to comment.