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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d70dd0718f3ff8dd77315c1478efa29bde5b9411
4
- data.tar.gz: a15a7d6ee25f1d91908d7d437cde77443f8e1a24
3
+ metadata.gz: a758c3ab1d93215d5c19a055eee983c2f5776b2c
4
+ data.tar.gz: 1b83fda8361c92793ef426042e6aafb13c3e6ebf
5
5
  SHA512:
6
- metadata.gz: 50a5acc781978e22d1a8513d90b14609331de3951b93d51988c7e92c777842d7e7288e1f9dfe830fd9b84f6fad31d7bdaca36a64d5c51784eba2f39d1d78935d
7
- data.tar.gz: b412de302fb2bab1ee786f9910354e656f4b60796a172e54705012502cf581847770244672afeec8d0ef13cdfebde09ae7808a455f4ea928bf9a52af557a4d24
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 consist, convenient workflow that encourages good template organization
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 :json, type: :boolean, desc: 'Show stack as JSON (default is YAML)'
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
- data = find_stack_in_service(name).data.to_hash
111
- say options[:json] ? MultiJson.dump(data, pretty: true) : data.to_yaml
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
- stack.wait_until(max_attempts: -1, delay: delay) do |s|
127
- EventPresenter.present(s.events) {|p| say p }
128
- # Wait until the stack status ends with _FAILED or _COMPLETE
129
- s.stack_status.match(/_(FAILED|COMPLETE)$/)
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
 
@@ -12,8 +12,7 @@ module CfnFlow
12
12
  end
13
13
 
14
14
  def with_symbolized_keys
15
- self.inject(StackParams.new) do |accum, pair|
16
- key, value = pair
15
+ self.reduce(StackParams.new) do |accum, (key, value)|
17
16
  accum.merge(key.to_sym => value)
18
17
  end
19
18
  end
@@ -1,3 +1,3 @@
1
1
  module CfnFlow
2
- VERSION = '0.9.0'
2
+ VERSION = '0.10.0'
3
3
  end
@@ -150,9 +150,10 @@ describe 'CfnFlow::CLI' do
150
150
  it 'can cleanup' do
151
151
 
152
152
  # Stubbing hacks alert!
153
- # The first time we call :describe_stacks, return the stack we launch.
154
- # The second time, we're loading 'another-stack' to clean it up
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 --json option' do
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: { describe_stacks: { stacks: [ stub_stack_data ] } }
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.must_equal "Deleted stack mystack\n"
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.must_equal "Deleted stack mystack\n"
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.9.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-10-30 00:00:00.000000000 Z
11
+ date: 2015-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk