cfn-flow 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|