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 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