cfn-flow 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/cfn_flow/cli.rb +68 -7
- data/lib/cfn_flow/stack_params.rb +1 -2
- data/lib/cfn_flow/version.rb +1 -1
- data/spec/cfn_flow/cli_spec.rb +82 -7
- data/spec/helper.rb +2 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a758c3ab1d93215d5c19a055eee983c2f5776b2c
|
4
|
+
data.tar.gz: 1b83fda8361c92793ef426042e6aafb13c3e6ebf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50dee4788230e13a9af86471b9c7ac3fcb1df86b136bc29d7462ddcbb76a85a218edb1514d4c590311ea204d43dbe8ee1f2002668236f46e59215346dd4707de
|
7
|
+
data.tar.gz: 7ac38fd70956fbbd4409d0abfbfc452ec5e565605812def986e956edce459c3ba513e9c8928054ba7a4a95f12601864273dbfd69366af759d447e524929db202
|
data/README.md
CHANGED
@@ -37,7 +37,7 @@ It provides a *simple*, *standard*, and *flexible* process for using CloudFormat
|
|
37
37
|
|
38
38
|
## Opinions
|
39
39
|
|
40
|
-
`cfn-flow` introduces a
|
40
|
+
`cfn-flow` introduces a consistent, convenient workflow that encourages good template organization
|
41
41
|
and deploy practices.
|
42
42
|
|
43
43
|
1. *Optimize for happiness.* The workflow should be easy & enjoyable to use.
|
data/lib/cfn_flow/cli.rb
CHANGED
@@ -68,6 +68,8 @@ module CfnFlow
|
|
68
68
|
say "Polling for events..."
|
69
69
|
invoke :events, [stack.name], ['--poll']
|
70
70
|
|
71
|
+
say "Stack Outputs:"
|
72
|
+
invoke :show, [stack.name], ['--format=outputs-table']
|
71
73
|
|
72
74
|
# Optionally cleanup other stacks in this environment
|
73
75
|
if options[:cleanup]
|
@@ -81,6 +83,41 @@ module CfnFlow
|
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
86
|
+
desc 'update ENVIRONMENT STACK', 'Updates a stack (use sparingly for mutable infrastructure)'
|
87
|
+
def update(environment, name)
|
88
|
+
# Export environment as an env var so it can be interpolated in config
|
89
|
+
ENV['CFN_FLOW_ENVIRONMENT'] = environment
|
90
|
+
|
91
|
+
stack = find_stack_in_service(name)
|
92
|
+
|
93
|
+
# Check that environment matches
|
94
|
+
unless stack.tags.any?{|tag| tag.key == 'CfnFlowEnvironment' && tag.value == environment }
|
95
|
+
raise Thor::Error.new "Stack #{name} is not tagged for environment #{environment}"
|
96
|
+
end
|
97
|
+
|
98
|
+
begin
|
99
|
+
params = CfnFlow.stack_params(environment)
|
100
|
+
params.delete(:tags) # No allowed for Stack#update
|
101
|
+
stack.update(params)
|
102
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
103
|
+
raise Thor::Error.new(e.message)
|
104
|
+
end
|
105
|
+
|
106
|
+
say "Updating stack #{stack.name}"
|
107
|
+
|
108
|
+
# NB: there's a potential race condition where polling for events would
|
109
|
+
# see the last complete state before the stack has a chance to begin updating.
|
110
|
+
# Consider putting a sleep, wait_for an UPDATE_IN_PROGRESS state beforehand,
|
111
|
+
# or look for events newer than the last event before updating.
|
112
|
+
|
113
|
+
# Invoke events
|
114
|
+
say "Polling for events..."
|
115
|
+
invoke :events, [stack.name], ['--poll']
|
116
|
+
|
117
|
+
say "Stack Outputs:"
|
118
|
+
invoke :show, [stack.name], ['--format=outputs-table']
|
119
|
+
end
|
120
|
+
|
84
121
|
desc 'list [ENVIRONMENT]', 'List running stacks in all environments, or ENVIRONMENT'
|
85
122
|
method_option 'no-header', type: :boolean, desc: 'Do not print column headers'
|
86
123
|
def list(environment=nil)
|
@@ -105,10 +142,27 @@ module CfnFlow
|
|
105
142
|
end
|
106
143
|
|
107
144
|
desc 'show STACK', 'Show details about STACK'
|
108
|
-
method_option :
|
145
|
+
method_option :format, type: :string, default: 'yaml', enum: %w(yaml json outputs-table), desc: "Format in which to display the stack."
|
109
146
|
def show(name)
|
110
|
-
|
111
|
-
|
147
|
+
formatters = {
|
148
|
+
'json' => ->(stack) { say MultiJson.dump(stack.data.to_hash, pretty: true) },
|
149
|
+
'yaml' => ->(stack) { say stack.data.to_hash.to_yaml },
|
150
|
+
'outputs-table' => ->(stack) do
|
151
|
+
outputs = stack.outputs.to_a
|
152
|
+
if outputs.any?
|
153
|
+
table_header = [['KEY', 'VALUE', 'DESCRIPTION']]
|
154
|
+
table_data = outputs.map do |s|
|
155
|
+
[ s.output_key, s.output_value, s.description ]
|
156
|
+
end
|
157
|
+
|
158
|
+
print_table(table_header + table_data)
|
159
|
+
else
|
160
|
+
say "No stack outputs to show."
|
161
|
+
end
|
162
|
+
end
|
163
|
+
}
|
164
|
+
stack = find_stack_in_service(name)
|
165
|
+
formatters[options[:format]].call(stack)
|
112
166
|
end
|
113
167
|
|
114
168
|
desc 'events STACK', 'List events for STACK'
|
@@ -123,10 +177,14 @@ module CfnFlow
|
|
123
177
|
if options[:poll]
|
124
178
|
# Display events until we're COMPLETE/FAILED
|
125
179
|
delay = (ENV['CFN_FLOW_EVENT_POLL_INTERVAL'] || 2).to_i
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
180
|
+
begin
|
181
|
+
stack.wait_until(max_attempts: -1, delay: delay) do |s|
|
182
|
+
EventPresenter.present(s.events) {|p| say p }
|
183
|
+
# Wait until the stack status ends with _FAILED or _COMPLETE
|
184
|
+
s.stack_status.match(/_(FAILED|COMPLETE)$/)
|
185
|
+
end
|
186
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
187
|
+
# The stack was deleted. Keep on trucking.
|
130
188
|
end
|
131
189
|
end
|
132
190
|
end
|
@@ -138,6 +196,9 @@ module CfnFlow
|
|
138
196
|
if options[:force] || yes?("Are you sure you want to shut down #{name}?", :red)
|
139
197
|
stack.delete
|
140
198
|
say "Deleted stack #{name}"
|
199
|
+
|
200
|
+
say "Polling for events..."
|
201
|
+
invoke :events, [stack.name], ['--poll']
|
141
202
|
end
|
142
203
|
end
|
143
204
|
|
data/lib/cfn_flow/version.rb
CHANGED
data/spec/cfn_flow/cli_spec.rb
CHANGED
@@ -150,9 +150,10 @@ describe 'CfnFlow::CLI' do
|
|
150
150
|
it 'can cleanup' do
|
151
151
|
|
152
152
|
# Stubbing hacks alert!
|
153
|
-
# The first
|
154
|
-
# The
|
153
|
+
# The first two times we call :describe_stacks, return the stack we launch.
|
154
|
+
# The third time, we're loading 'another-stack' to clean it up
|
155
155
|
stack_stubs = [
|
156
|
+
{ stacks: [ stub_stack_data(stack_name: 'cfn-flow-spec-stack') ] },
|
156
157
|
{ stacks: [ stub_stack_data(stack_name: 'cfn-flow-spec-stack') ] },
|
157
158
|
{ stacks: [ stub_stack_data(stack_name: 'another-stack') ] }
|
158
159
|
]
|
@@ -173,6 +174,57 @@ describe 'CfnFlow::CLI' do
|
|
173
174
|
|
174
175
|
end
|
175
176
|
|
177
|
+
describe '#update' do
|
178
|
+
it 'fails with no args' do
|
179
|
+
out, err = capture_io { cli.start [:update] }
|
180
|
+
out.must_equal ''
|
181
|
+
err.must_match(/ERROR.+no arguments/)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'returns an error when stack is not in service' do
|
185
|
+
stack_data = stub_stack_data
|
186
|
+
stack_data[:tags][0][:value] = 'none-such-service'
|
187
|
+
Aws.config[:cloudformation]= {
|
188
|
+
stub_responses: {
|
189
|
+
describe_stacks: { stacks: [ stack_data ] }
|
190
|
+
}
|
191
|
+
}
|
192
|
+
out, err = capture_io { cli.start [:update, 'production', 'none-such-stack'] }
|
193
|
+
out.must_equal ''
|
194
|
+
err.must_match "not tagged for service #{CfnFlow.service}"
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'returns an error when stack environment does not match' do
|
198
|
+
stack_data = stub_stack_data
|
199
|
+
Aws.config[:cloudformation]= {
|
200
|
+
stub_responses: {
|
201
|
+
describe_stacks: { stacks: [ stack_data ] }
|
202
|
+
}
|
203
|
+
}
|
204
|
+
out, err = capture_io { cli.start [:update, 'none-such-env', 'mystack'] }
|
205
|
+
out.must_equal ''
|
206
|
+
err.must_match "not tagged for environment none-such-env"
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'succeeds' do
|
210
|
+
stack_name = CfnFlow.config['stack']['stack_name']
|
211
|
+
|
212
|
+
Aws.config[:cloudformation]= {
|
213
|
+
stub_responses: {
|
214
|
+
describe_stacks: { stacks: [ stub_stack_data(stack_name: stack_name, stack_status: 'UPDATE_COMPLETE') ] },
|
215
|
+
describe_stack_events: { stack_events: [ stub_event_data(resource_status: 'UPDATE_COMPLETE') ] },
|
216
|
+
}
|
217
|
+
}
|
218
|
+
out, err = capture_io { cli.start [:update, 'production', stack_name] }
|
219
|
+
|
220
|
+
out.must_match "Updating stack #{stack_name}"
|
221
|
+
out.must_match "Polling for events..."
|
222
|
+
out.must_match "UPDATE_COMPLETE"
|
223
|
+
out.must_match "Stack Outputs:"
|
224
|
+
err.must_equal ''
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
176
228
|
describe '#list' do
|
177
229
|
it 'has no output with no stacks' do
|
178
230
|
out, err = capture_io { cli.start [:list] }
|
@@ -257,11 +309,30 @@ describe 'CfnFlow::CLI' do
|
|
257
309
|
err.must_equal ''
|
258
310
|
end
|
259
311
|
|
260
|
-
it 'handles
|
261
|
-
out, _ = capture_io { cli.start [:show, 'mystack', '--json'] }
|
312
|
+
it 'handles json format' do
|
313
|
+
out, _ = capture_io { cli.start [:show, 'mystack', '--format=json'] }
|
262
314
|
expected = MultiJson.dump(CfnFlow.cfn_resource.stack('mystack').data.to_hash, pretty: true) + "\n"
|
263
315
|
out.must_equal expected
|
264
316
|
end
|
317
|
+
|
318
|
+
describe 'outputs-table format' do
|
319
|
+
it 'handles shows a table when there are events' do
|
320
|
+
out, _ = capture_io { cli.start [:show, 'mystack', '--format=outputs-table'] }
|
321
|
+
out.must_match(/KEY\s+VALUE\s+DESCRIPTION/)
|
322
|
+
out.must_match(/mykey\s+myvalue\s+My Output/)
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'handles no events' do
|
326
|
+
Aws.config[:cloudformation]= {
|
327
|
+
stub_responses: {
|
328
|
+
describe_stacks: { stacks: [ stub_stack_data(outputs: nil) ] }
|
329
|
+
}
|
330
|
+
}
|
331
|
+
out, _ = capture_io { cli.start [:show, 'mystack', '--format=outputs-table'] }
|
332
|
+
out.must_match "No stack outputs to show."
|
333
|
+
|
334
|
+
end
|
335
|
+
end
|
265
336
|
end
|
266
337
|
|
267
338
|
it 'returns an error with missing stacks' do
|
@@ -351,14 +422,18 @@ describe 'CfnFlow::CLI' do
|
|
351
422
|
describe 'with a stack' do
|
352
423
|
before do
|
353
424
|
Aws.config[:cloudformation] = {
|
354
|
-
stub_responses: {
|
425
|
+
stub_responses: {
|
426
|
+
describe_stacks: { stacks: [ stub_stack_data ] },
|
427
|
+
describe_stack_events: { stack_events: [ stub_event_data ] }
|
428
|
+
}
|
355
429
|
}
|
356
430
|
end
|
357
431
|
|
358
432
|
it 'deletes the stack' do
|
359
433
|
Thor::LineEditor.stub :readline, "yes" do
|
360
434
|
out, err = capture_io { cli.start [:delete, 'mystack'] }
|
361
|
-
out.
|
435
|
+
out.must_match "Deleted stack mystack"
|
436
|
+
out.must_match "Polling for events..."
|
362
437
|
err.must_equal ''
|
363
438
|
end
|
364
439
|
end
|
@@ -373,7 +448,7 @@ describe 'CfnFlow::CLI' do
|
|
373
448
|
|
374
449
|
it 'does not ask when --force is set' do
|
375
450
|
out, err = capture_io { cli.start [:delete, '--force', 'mystack'] }
|
376
|
-
out.
|
451
|
+
out.must_match "Deleted stack mystack"
|
377
452
|
err.must_equal ''
|
378
453
|
end
|
379
454
|
end
|
data/spec/helper.rb
CHANGED
@@ -50,7 +50,8 @@ class Minitest::Spec
|
|
50
50
|
tags: [
|
51
51
|
{key: 'CfnFlowService', value: CfnFlow.service},
|
52
52
|
{key: 'CfnFlowEnvironment', value: 'production'}
|
53
|
-
]
|
53
|
+
],
|
54
|
+
outputs: [ output_key: 'mykey', output_value: 'myvalue', description: 'My Output' ]
|
54
55
|
}.merge(attrs)
|
55
56
|
end
|
56
57
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cfn-flow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|