stackup 1.4.5 → 1.7.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.
@@ -56,7 +56,11 @@ module Stackup
56
56
  options[:change_set_name] = name
57
57
  options[:change_set_type] = stack.exists? ? "UPDATE" : "CREATE"
58
58
  force = options.delete(:force)
59
+ allow_empty_change_set = options.delete(:allow_empty_change_set)
59
60
  options[:template_body] = MultiJson.dump(options.delete(:template)) if options[:template]
61
+ # optionally override template_body with the original template to preserve formatting (& comments in YAML)
62
+ template_orig = options.delete(:template_orig)
63
+ options[:template_body] = template_orig if options.delete(:preserve)
60
64
  options[:parameters] = Parameters.new(options[:parameters]).to_a if options[:parameters]
61
65
  options[:tags] = normalize_tags(options[:tags]) if options[:tags]
62
66
  options[:capabilities] ||= ["CAPABILITY_NAMED_IAM"]
@@ -70,8 +74,12 @@ module Stackup
70
74
  when /COMPLETE/
71
75
  return current.status
72
76
  when "FAILED"
73
- logger.error(current.status_reason)
74
- raise StackUpdateError, "change-set creation failed" if status == "FAILED"
77
+ if allow_empty_change_set and current.status_reason == "The submitted information didn't contain changes. Submit different information to create a change set."
78
+ return current.status_reason
79
+ else
80
+ logger.error(current.status_reason)
81
+ raise StackUpdateError, "change-set creation failed" if status == "FAILED"
82
+ end
75
83
  end
76
84
  sleep(wait_poll_interval)
77
85
  end
@@ -0,0 +1,561 @@
1
+ require "clamp"
2
+ require "console_logger"
3
+ require "multi_json"
4
+ require "securerandom"
5
+ require "stackup"
6
+ require "stackup/differ"
7
+ require "stackup/source"
8
+ require "stackup/version"
9
+ require "stackup/yaml"
10
+
11
+ module Stackup
12
+
13
+ class MainCommand < Clamp::Command
14
+
15
+ option ["-L", "--list"], :flag, "list stacks" do
16
+ list_stacks
17
+ exit 0
18
+ end
19
+
20
+ option ["-Y", "--yaml"], :flag, "output data in YAML format"
21
+
22
+ option ["--region"], "REGION", "set region" do |arg|
23
+ raise ArgumentError, "#{arg.inspect} doesn't look like a region" unless arg =~ /^[a-z]{2}-[a-z]+-\d$/
24
+
25
+ arg
26
+ end
27
+
28
+ option ["--with-role"], "ROLE_ARN", "assume this role",
29
+ :attribute_name => :role_arn
30
+
31
+ option ["--retry-limit"], "N", "maximum number of retries for API calls",
32
+ :environment_variable => "AWS_API_RETRY_LIMIT" do |arg|
33
+ Integer(arg)
34
+ end
35
+
36
+ option ["--[no-]wait"], :flag, "wait for stack updates to complete",
37
+ :default => true
38
+
39
+ option ["--wait-poll-interval"], "N", "polling interval (in seconds) while waiting for updates",
40
+ :default => 5, &method(:Integer)
41
+
42
+ option "--debug", :flag, "enable debugging"
43
+
44
+ option ["--version"], :flag, "display version" do
45
+ puts "stackup v#{Stackup::VERSION}"
46
+ exit 0
47
+ end
48
+
49
+ parameter "NAME", "Name of stack", :attribute_name => :stack_name
50
+
51
+ def run(arguments)
52
+ super(arguments)
53
+ rescue Stackup::Source::ReadError => e
54
+ signal_error e.message
55
+ rescue Stackup::ServiceError => e
56
+ signal_error e.message
57
+ rescue Aws::Errors::MissingCredentialsError
58
+ signal_error "no credentials provided"
59
+ rescue Aws::Errors::ServiceError => e
60
+ signal_error e.message
61
+ end
62
+
63
+ private
64
+
65
+ def logger
66
+ @logger ||= ConsoleLogger.new($stdout, debug?)
67
+ end
68
+
69
+ def format_data(data)
70
+ if yaml?
71
+ YAML.dump(data)
72
+ else
73
+ MultiJson.dump(data, :pretty => true)
74
+ end
75
+ end
76
+
77
+ def display_data(data)
78
+ puts format_data(data)
79
+ end
80
+
81
+ def role_arn=(arg)
82
+ raise ArgumentError, "#{arg.inspect} doesn't look like a role ARN" unless arg =~ %r{^arn:aws:iam::\d+:role/}
83
+
84
+ @role_arn = arg
85
+ end
86
+
87
+ def stackup
88
+ Stackup(aws_config)
89
+ end
90
+
91
+ def base_aws_config
92
+ {
93
+ :log_level => :debug,
94
+ :logger => logger,
95
+ :region => region,
96
+ :retry_limit => retry_limit
97
+ }.reject { |_k, v| v.nil? }
98
+ end
99
+
100
+ def aws_config
101
+ return base_aws_config unless role_arn
102
+
103
+ assumed_credentials = Aws::AssumeRoleCredentials.new(
104
+ :client => Aws::STS::Client.new(base_aws_config),
105
+ :role_arn => role_arn,
106
+ :role_session_name => "stackup-#{SecureRandom.hex(8)}"
107
+ )
108
+ base_aws_config.merge(:credentials => assumed_credentials)
109
+ end
110
+
111
+ def stack
112
+ stackup.stack(stack_name, :wait => wait?, :wait_poll_interval => wait_poll_interval)
113
+ end
114
+
115
+ def list_stacks
116
+ stackup.stack_names.each do |name|
117
+ puts name
118
+ end
119
+ end
120
+
121
+ def report_change
122
+ final_status = yield
123
+ puts final_status unless final_status.nil?
124
+ end
125
+
126
+ subcommand "status", "Print stack status." do
127
+
128
+ def execute
129
+ puts stack.status
130
+ end
131
+
132
+ end
133
+
134
+ module HasParameters
135
+
136
+ extend Clamp::Option::Declaration
137
+
138
+ option ["-p", "--parameters"], "FILE", "parameters file (last wins)",
139
+ :multivalued => true,
140
+ :attribute_name => :parameter_sources,
141
+ &Stackup::Source.method(:new)
142
+
143
+ option ["-o", "--override"], "PARAM=VALUE", "parameter overrides",
144
+ :multivalued => true,
145
+ :attribute_name => :override_list
146
+
147
+ private
148
+
149
+ def parameters
150
+ parameters_from_files.merge(parameter_overrides)
151
+ end
152
+
153
+ def parameters_from_files
154
+ parameter_sources.map do |src|
155
+ Stackup::Parameters.new(src.data).to_hash
156
+ end.inject({}, :merge)
157
+ end
158
+
159
+ def parameter_overrides
160
+ {}.tap do |result|
161
+ override_list.each do |override|
162
+ key, value = override.split("=", 2)
163
+ result[key] = value
164
+ end
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ subcommand "up", "Create/update the stack." do
171
+
172
+ option ["-t", "--template"], "FILE", "template source",
173
+ :attribute_name => :template_source,
174
+ &Stackup::Source.method(:new)
175
+
176
+ option ["-T", "--use-previous-template"], :flag,
177
+ "reuse the existing template"
178
+
179
+ option ["-P", "--preserve-template-formatting"], :flag,
180
+ "do not normalise the template when calling the Cloudformation APIs; useful for preserving YAML and comments"
181
+
182
+ include HasParameters
183
+
184
+ option "--tags", "FILE", "stack tags file",
185
+ :attribute_name => :tag_source,
186
+ &Stackup::Source.method(:new)
187
+
188
+ option "--policy", "FILE", "stack policy file",
189
+ :attribute_name => :policy_source,
190
+ &Stackup::Source.method(:new)
191
+
192
+ option "--service-role-arn", "SERVICE_ROLE_ARN", "cloudformation service role ARN" do |arg|
193
+ raise ArgumentError, "#{arg.inspect} doesn't look like a role ARN" unless arg =~ %r{^arn:aws:iam::\d+:role/}
194
+
195
+ arg
196
+ end
197
+
198
+ option "--on-failure", "ACTION",
199
+ "when stack creation fails: DO_NOTHING, ROLLBACK, or DELETE",
200
+ :default => "ROLLBACK"
201
+
202
+ option "--capability", "CAPABILITY", "cloudformation capability",
203
+ :multivalued => true, :default => ["CAPABILITY_NAMED_IAM"]
204
+
205
+ def execute
206
+ unless template_source || use_previous_template?
207
+ signal_usage_error "Specify either --template or --use-previous-template"
208
+ end
209
+ options = {}
210
+ if template_source
211
+ if template_source.s3?
212
+ options[:template_url] = template_source.location
213
+ else
214
+ options[:template] = template_source.data
215
+ options[:template_orig] = template_source.body
216
+ end
217
+ end
218
+ options[:on_failure] = on_failure
219
+ options[:parameters] = parameters
220
+ options[:tags] = tag_source.data if tag_source
221
+ if policy_source
222
+ if policy_source.s3?
223
+ options[:stack_policy_url] = policy_source.location
224
+ else
225
+ options[:stack_policy] = policy_source.data
226
+ end
227
+ end
228
+ options[:role_arn] = service_role_arn if service_role_arn
229
+ options[:use_previous_template] = use_previous_template?
230
+ options[:capabilities] = capability_list
231
+ options[:preserve] = preserve_template_formatting?
232
+ report_change do
233
+ stack.create_or_update(options)
234
+ end
235
+ end
236
+
237
+ end
238
+
239
+ subcommand ["change-sets"], "List change-sets." do
240
+
241
+ def execute
242
+ stack.change_set_summaries.each do |change_set|
243
+ puts [
244
+ pad(change_set.change_set_name, 36),
245
+ pad(change_set.status, 20),
246
+ pad(change_set.execution_status, 24)
247
+ ].join(" ")
248
+ end
249
+ end
250
+
251
+ private
252
+
253
+ def pad(s, width)
254
+ (s || "").ljust(width)
255
+ end
256
+
257
+ end
258
+
259
+ subcommand ["change-set"], "Change-set operations." do
260
+
261
+ option "--name", "NAME", "Name of change-set",
262
+ :attribute_name => :change_set_name,
263
+ :default => "pending"
264
+
265
+ subcommand "create", "Create a change-set." do
266
+
267
+ option ["-d", "--description"], "DESC",
268
+ "Change-set description"
269
+
270
+ option ["-t", "--template"], "FILE", "template source",
271
+ :attribute_name => :template_source,
272
+ &Stackup::Source.method(:new)
273
+
274
+ option ["-T", "--use-previous-template"], :flag,
275
+ "reuse the existing template"
276
+
277
+ option ["-P", "--preserve-template-formatting"], :flag,
278
+ "do not normalise the template when calling the Cloudformation APIs; useful for preserving YAML and comments"
279
+
280
+ option ["--force"], :flag,
281
+ "replace existing change-set of the same name"
282
+
283
+ option ["--no-fail-on-empty-change-set"], :flag, "don't fail on empty change-set",
284
+ :attribute_name => :allow_empty_change_set
285
+
286
+ include HasParameters
287
+
288
+ option "--tags", "FILE", "stack tags file",
289
+ :attribute_name => :tag_source,
290
+ &Stackup::Source.method(:new)
291
+
292
+ option "--service-role-arn", "SERVICE_ROLE_ARN", "cloudformation service role ARN" do |arg|
293
+ raise ArgumentError, "#{arg.inspect} doesn't look like a role ARN" unless arg =~ %r{^arn:aws:iam::\d+:role/}
294
+
295
+ arg
296
+ end
297
+
298
+ option "--capability", "CAPABILITY", "cloudformation capability",
299
+ :multivalued => true, :default => ["CAPABILITY_NAMED_IAM"]
300
+
301
+ def execute
302
+ unless template_source || use_previous_template?
303
+ signal_usage_error "Specify either --template or --use-previous-template"
304
+ end
305
+ options = {}
306
+ if template_source
307
+ if template_source.s3?
308
+ options[:template_url] = template_source.location
309
+ else
310
+ options[:template] = template_source.data
311
+ options[:template_orig] = template_source.body
312
+ end
313
+ end
314
+ options[:parameters] = parameters
315
+ options[:description] = description if description
316
+ options[:tags] = tag_source.data if tag_source
317
+ options[:role_arn] = service_role_arn if service_role_arn
318
+ options[:use_previous_template] = use_previous_template?
319
+ options[:force] = force?
320
+ options[:allow_empty_change_set] = allow_empty_change_set?
321
+ options[:capabilities] = capability_list
322
+ options[:preserve] = preserve_template_formatting?
323
+ report_change do
324
+ change_set.create(options)
325
+ end
326
+ end
327
+
328
+ end
329
+
330
+ subcommand "changes", "Describe the change-set." do
331
+
332
+ def execute
333
+ display_data(change_set.describe.changes.map(&:to_h))
334
+ end
335
+
336
+ end
337
+
338
+ subcommand "inspect", "Show full change-set details." do
339
+
340
+ def execute
341
+ display_data(change_set.describe.to_h)
342
+ end
343
+
344
+ end
345
+
346
+ subcommand ["apply", "execute"], "Apply the change-set." do
347
+
348
+ def execute
349
+ report_change do
350
+ change_set.execute
351
+ end
352
+ end
353
+
354
+ end
355
+
356
+ subcommand "delete", "Delete the change-set." do
357
+
358
+ def execute
359
+ report_change do
360
+ change_set.delete
361
+ end
362
+ end
363
+
364
+ end
365
+
366
+ private
367
+
368
+ def change_set
369
+ stack.change_set(change_set_name)
370
+ end
371
+
372
+ end
373
+
374
+ subcommand "diff", "Compare template/params to current stack." do
375
+
376
+ option "--diff-format", "FORMAT", "'text', 'color', or 'html'", :default => "color"
377
+
378
+ option ["-C", "--context-lines"], "LINES", "number of lines of context to show", :default => 10_000
379
+
380
+ option ["-t", "--template"], "FILE", "template source",
381
+ :attribute_name => :template_source,
382
+ &Stackup::Source.method(:new)
383
+
384
+ include HasParameters
385
+
386
+ option "--tags", "FILE", "stack tags file",
387
+ :attribute_name => :tag_source,
388
+ &Stackup::Source.method(:new)
389
+
390
+ def execute
391
+ current = {}
392
+ planned = {}
393
+ if template_source
394
+ current["Template"] = stack.template
395
+ planned["Template"] = template_source.data
396
+ end
397
+ unless parameter_sources.empty?
398
+ current["Parameters"] = existing_parameters.sort.to_h
399
+ planned["Parameters"] = new_parameters.sort.to_h
400
+ end
401
+ if tag_source
402
+ current["Tags"] = stack.tags.sort.to_h
403
+ planned["Tags"] = tag_source.data.sort.to_h
404
+ end
405
+ signal_usage_error "specify '--template' or '--parameters'" if planned.empty?
406
+ puts differ.diff(current, planned, context_lines)
407
+ end
408
+
409
+ private
410
+
411
+ def differ
412
+ Stackup::Differ.new(diff_format, &method(:format_data))
413
+ end
414
+
415
+ def existing_parameters
416
+ @existing_parameters ||= stack.parameters
417
+ end
418
+
419
+ def new_parameters
420
+ existing_parameters.merge(parameters)
421
+ end
422
+
423
+ end
424
+
425
+ subcommand ["down", "delete"], "Remove the stack." do
426
+
427
+ def execute
428
+ report_change do
429
+ stack.delete
430
+ end
431
+ end
432
+
433
+ end
434
+
435
+ subcommand "cancel-update", "Cancel the update in-progress." do
436
+
437
+ def execute
438
+ report_change do
439
+ stack.cancel_update
440
+ end
441
+ end
442
+
443
+ end
444
+
445
+ subcommand "wait", "Wait until stack is stable." do
446
+
447
+ def execute
448
+ puts stack.wait
449
+ end
450
+
451
+ end
452
+
453
+ subcommand "events", "List stack events." do
454
+
455
+ option ["-f", "--follow"], :flag, "follow new events"
456
+ option ["--data"], :flag, "display events as data"
457
+
458
+ def execute
459
+ stack.watch(false) do |watcher|
460
+ loop do
461
+ watcher.each_new_event do |event|
462
+ display_event(event)
463
+ end
464
+ break unless follow?
465
+
466
+ sleep 5
467
+ end
468
+ end
469
+ end
470
+
471
+ private
472
+
473
+ def display_event(e)
474
+ if data?
475
+ display_data(event_data(e))
476
+ else
477
+ puts event_summary(e)
478
+ end
479
+ end
480
+
481
+ def event_data(e)
482
+ {
483
+ "timestamp" => e.timestamp.localtime,
484
+ "logical_resource_id" => e.logical_resource_id,
485
+ "physical_resource_id" => e.physical_resource_id,
486
+ "resource_status" => e.resource_status,
487
+ "resource_status_reason" => e.resource_status_reason
488
+ }.reject { |_k, v| blank?(v) }
489
+ end
490
+
491
+ def blank?(v)
492
+ v.nil? || v.respond_to?(:empty?) && v.empty?
493
+ end
494
+
495
+ def event_summary(e)
496
+ summary = "[#{e.timestamp.localtime.iso8601}] #{e.logical_resource_id}"
497
+ summary += " - #{e.resource_status}"
498
+ summary += " - #{e.resource_status_reason}" if e.resource_status_reason
499
+ summary
500
+ end
501
+
502
+ end
503
+
504
+ subcommand "template", "Display stack template." do
505
+
506
+ def execute
507
+ display_data(stack.template)
508
+ end
509
+
510
+ end
511
+
512
+ subcommand ["parameters", "params"], "Display stack parameters." do
513
+
514
+ def execute
515
+ display_data(stack.parameters)
516
+ end
517
+
518
+ end
519
+
520
+ subcommand "tags", "Display stack tags." do
521
+
522
+ def execute
523
+ display_data(stack.tags)
524
+ end
525
+
526
+ end
527
+
528
+ subcommand "resources", "Display stack resources." do
529
+
530
+ def execute
531
+ display_data(stack.resources)
532
+ end
533
+
534
+ end
535
+
536
+ subcommand "outputs", "Display stack outputs." do
537
+
538
+ def execute
539
+ display_data(stack.outputs)
540
+ end
541
+
542
+ end
543
+
544
+ subcommand "inspect", "Display stack particulars." do
545
+
546
+ def execute
547
+ data = {
548
+ "Status" => stack.status,
549
+ "Parameters" => stack.parameters,
550
+ "Tags" => stack.tags,
551
+ "Resources" => stack.resources,
552
+ "Outputs" => stack.outputs
553
+ }
554
+ display_data(data)
555
+ end
556
+
557
+ end
558
+
559
+ end
560
+
561
+ end