Skip to content

Commit 7a695cb

Browse files
committed
Merge pull request #14 from ktheory/stack-outputs
Support stack outputs as parameters in cfn-flow.yml
2 parents 3cfa057 + f4f6dd3 commit 7a695cb

File tree

8 files changed

+331
-61
lines changed

8 files changed

+331
-61
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ stack:
184184
# Your parameters, e.g.:
185185
vpcid: vpc-1234
186186
ami: ami-abcd
187+
188+
##
189+
# Use outputs from other stacks
190+
191+
# This set the `load_balancer` parameter to the value of the
192+
# `elbname` output of `my-elb-stack`
193+
load_balancer:
194+
stack: my-elb-stack
195+
output: elbname
196+
197+
# If you don't specify the output name, it's assumed to be same
198+
# as the parameter key:
199+
ssh_security_group:
200+
stack: my-bastion-stack
201+
187202
disable_rollback: true,
188203
timeout_in_minutes: 1,
189204
notification_arns: ["NotificationARN"],
@@ -229,6 +244,23 @@ stack:
229244
git_sha: <%= `git rev-parse --verify HEAD`.chomp %>
230245
```
231246

247+
#### Use stack outputs as parameters
248+
`cfn-flow` lets you easily reference stack outputs as parameters for new stacks.
249+
250+
```yaml
251+
# cfn-flow.yml
252+
stack:
253+
parameters:
254+
# Set my-param to the `my-param` output of `another-stack`
255+
my-param:
256+
stack: another-stack
257+
258+
# Set my-param to the `my-output` output of `another-stack`
259+
my-param:
260+
stack: another-stack
261+
output: my-output
262+
```
263+
232264
## Usage
233265
234266
Getting help:

lib/cfn_flow.rb

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,43 +39,11 @@ def stack_params(environment)
3939
unless config['stack'].is_a? Hash
4040
raise Thor::Error.new("No stack defined in #{config_path}. Add 'stack: ...'.")
4141
end
42+
params = StackParams.expanded(config['stack'])
4243

43-
# Dup & symbolize keys
44-
params = config['stack'].map{|k,v| [k.to_sym, v]}.to_h
45-
46-
# Expand params
47-
if params[:parameters].is_a? Hash
48-
expanded_params = params[:parameters].map do |key,value|
49-
{ parameter_key: key, parameter_value: value }
50-
end
51-
params[:parameters] = expanded_params
52-
end
53-
54-
# Expand tags
55-
if params[:tags].is_a? Hash
56-
tags = params[:tags].map do |key, value|
57-
{key: key, value: value}
58-
end
59-
60-
params[:tags] = tags
61-
end
62-
63-
# Append CfnFlow tags
64-
params[:tags] ||= []
65-
params[:tags] << { key: 'CfnFlowService', value: service }
66-
params[:tags] << { key: 'CfnFlowEnvironment', value: environment }
67-
68-
# Expand template body
69-
if params[:template_body].is_a? String
70-
begin
71-
body = CfnFlow::Template.new(params[:template_body]).to_json
72-
params[:template_body] = body
73-
rescue CfnFlow::Template::Error
74-
# Do nothing
75-
end
76-
end
77-
78-
params
44+
params.
45+
add_tag('CfnFlowService' => service).
46+
add_tag('CfnFlowEnvironment' => environment)
7947
end
8048

8149
def template_s3_bucket
@@ -113,6 +81,7 @@ def cfn_resource
11381
# Clear aws sdk clients & config (for tests)
11482
def clear!
11583
@config = @cfn_client = @cfn_resource = nil
84+
CachedStack.stack_cache.clear
11685
end
11786

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

103+
require 'cfn_flow/cached_stack'
104+
require 'cfn_flow/stack_params'
134105
require 'cfn_flow/template'
135106
require 'cfn_flow/git'
136107
require 'cfn_flow/event_presenter'

lib/cfn_flow/cached_stack.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module CfnFlow
2+
class CachedStack
3+
4+
class MissingOutput < StandardError; end
5+
6+
def self.stack_cache
7+
@stack_cache ||= {}
8+
end
9+
10+
def self.get_output(stack:, output:)
11+
new(stack).output(output)
12+
end
13+
14+
attr_reader :stack_name
15+
16+
def initialize(stack_name)
17+
@stack_name = stack_name
18+
end
19+
20+
def output(name)
21+
output = stack_cache.outputs.detect{|out| out.output_key == name }
22+
unless output
23+
raise MissingOutput.new("Can't find outpout #{name} for stack #{stack_name}")
24+
end
25+
output.output_value
26+
end
27+
28+
def stack_cache
29+
self.class.stack_cache[stack_name] ||= CfnFlow.cfn_resource.stack(stack_name).load
30+
end
31+
end
32+
end

lib/cfn_flow/stack_params.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module CfnFlow
2+
# Extend hash with some special behavior to generate the
3+
# style of hash aws-sdk expects
4+
class StackParams < Hash
5+
6+
def self.expanded(hash)
7+
self[hash].
8+
with_symbolized_keys.
9+
with_expanded_parameters.
10+
with_expanded_tags.
11+
with_expanded_template_body
12+
end
13+
14+
def with_symbolized_keys
15+
self.inject(StackParams.new) do |accum, pair|
16+
key, value = pair
17+
accum.merge(key.to_sym => value)
18+
end
19+
end
20+
21+
def with_expanded_parameters
22+
return self unless self[:parameters].is_a? Hash
23+
24+
expanded_params = self[:parameters].map do |key,value|
25+
{ parameter_key: key, parameter_value: fetch_value(key, value) }
26+
end
27+
28+
self.merge(parameters: expanded_params)
29+
end
30+
31+
def with_expanded_tags
32+
return self unless self[:tags].is_a? Hash
33+
34+
tags = self[:tags].map do |key, value|
35+
{key: key, value: value}
36+
end
37+
38+
self.merge(tags: tags)
39+
end
40+
41+
def add_tag(hash)
42+
new_tags = hash.map do |k,v|
43+
{key: k, value: v }
44+
end
45+
tags = (self[:tags] || []) + new_tags
46+
self.merge(tags: tags)
47+
end
48+
49+
def with_expanded_template_body
50+
return self unless self[:template_body].is_a? String
51+
body = CfnFlow::Template.new(self[:template_body]).to_json
52+
self.merge(template_body: body)
53+
rescue CfnFlow::Template::Error
54+
# Do nothing
55+
self
56+
end
57+
58+
def fetch_value(key, value)
59+
# Dereference stack output params
60+
if value.is_a?(Hash) && value.key?('stack')
61+
stack_name = value['stack']
62+
stack_output_name = value['output'] || key
63+
64+
value = CachedStack.get_output(stack: stack_name, output: stack_output_name)
65+
else
66+
value
67+
end
68+
end
69+
private :fetch_value
70+
end
71+
end

lib/cfn_flow/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module CfnFlow
2-
VERSION = '0.8.0'
2+
VERSION = '0.9.0'
33
end

spec/cfn_flow/cached_stack_spec.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
require_relative '../helper'
2+
3+
describe 'CfnFlow::CachedStack' do
4+
subject { CfnFlow::CachedStack }
5+
6+
describe '.stack_cache' do
7+
it 'defaults to a hash' do
8+
subject.stack_cache.must_equal({})
9+
end
10+
end
11+
12+
describe '.get_output' do
13+
let(:output_value) { 'myvalue' }
14+
15+
before do
16+
Aws.config[:cloudformation]= {
17+
stub_responses: {
18+
describe_stacks: { stacks: [ stub_stack_data.merge(outputs: [{ output_key: "myoutput", output_value: output_value } ]) ] }
19+
}
20+
}
21+
end
22+
23+
it 'returns the output' do
24+
subject.get_output(stack: 'mystack', output: 'myoutput').must_equal output_value
25+
end
26+
27+
it 'has required kwargs' do
28+
-> { subject.get_output }.must_raise(ArgumentError)
29+
end
30+
end
31+
32+
describe 'an instance' do
33+
subject { CfnFlow::CachedStack.new('mystack') }
34+
let(:output_value) { 'myvalue' }
35+
36+
before do
37+
Aws.config[:cloudformation]= {
38+
stub_responses: {
39+
describe_stacks: { stacks: [ stub_stack_data.merge(outputs: [{ output_key: "myoutput", output_value: output_value } ]) ] }
40+
}
41+
}
42+
end
43+
44+
it "should return the output value" do
45+
subject.output('myoutput').must_equal output_value
46+
end
47+
48+
describe "with a missing output" do
49+
it "should raise an error" do
50+
-> { subject.output("no-such-output") }.must_raise(CfnFlow::CachedStack::MissingOutput)
51+
end
52+
end
53+
54+
describe "with a missing stack" do
55+
56+
subject { CfnFlow::CachedStack.new('no-such-stack') }
57+
before do
58+
Aws.config[:cloudformation]= {
59+
stub_responses: {
60+
describe_stacks: 'ValidationError'
61+
}
62+
}
63+
end
64+
65+
it "should raise an error" do
66+
-> { subject.output('blah') }.must_raise(Aws::CloudFormation::Errors::ValidationError)
67+
end
68+
end
69+
end
70+
71+
end

0 commit comments

Comments
 (0)