stackup 1.4.4 → 1.6.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.
@@ -57,6 +57,9 @@ module Stackup
57
57
  options[:change_set_type] = stack.exists? ? "UPDATE" : "CREATE"
58
58
  force = options.delete(:force)
59
59
  options[:template_body] = MultiJson.dump(options.delete(:template)) if options[:template]
60
+ # optionally override template_body with the original template to preserve formatting (& comments in YAML)
61
+ template_orig = options.delete(:template_orig)
62
+ options[:template_body] = template_orig if options.delete(:preserve)
60
63
  options[:parameters] = Parameters.new(options[:parameters]).to_a if options[:parameters]
61
64
  options[:tags] = normalize_tags(options[:tags]) if options[:tags]
62
65
  options[:capabilities] ||= ["CAPABILITY_NAMED_IAM"]
@@ -0,0 +1,557 @@
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
+ include HasParameters
284
+
285
+ option "--tags", "FILE", "stack tags file",
286
+ :attribute_name => :tag_source,
287
+ &Stackup::Source.method(:new)
288
+
289
+ option "--service-role-arn", "SERVICE_ROLE_ARN", "cloudformation service role ARN" do |arg|
290
+ raise ArgumentError, "#{arg.inspect} doesn't look like a role ARN" unless arg =~ %r{^arn:aws:iam::\d+:role/}
291
+
292
+ arg
293
+ end
294
+
295
+ option "--capability", "CAPABILITY", "cloudformation capability",
296
+ :multivalued => true, :default => ["CAPABILITY_NAMED_IAM"]
297
+
298
+ def execute
299
+ unless template_source || use_previous_template?
300
+ signal_usage_error "Specify either --template or --use-previous-template"
301
+ end
302
+ options = {}
303
+ if template_source
304
+ if template_source.s3?
305
+ options[:template_url] = template_source.location
306
+ else
307
+ options[:template] = template_source.data
308
+ options[:template_orig] = template_source.body
309
+ end
310
+ end
311
+ options[:parameters] = parameters
312
+ options[:description] = description if description
313
+ options[:tags] = tag_source.data if tag_source
314
+ options[:role_arn] = service_role_arn if service_role_arn
315
+ options[:use_previous_template] = use_previous_template?
316
+ options[:force] = force?
317
+ options[:capabilities] = capability_list
318
+ options[:preserve] = preserve_template_formatting?
319
+ report_change do
320
+ change_set.create(options)
321
+ end
322
+ end
323
+
324
+ end
325
+
326
+ subcommand "changes", "Describe the change-set." do
327
+
328
+ def execute
329
+ display_data(change_set.describe.changes.map(&:to_h))
330
+ end
331
+
332
+ end
333
+
334
+ subcommand "inspect", "Show full change-set details." do
335
+
336
+ def execute
337
+ display_data(change_set.describe.to_h)
338
+ end
339
+
340
+ end
341
+
342
+ subcommand ["apply", "execute"], "Apply the change-set." do
343
+
344
+ def execute
345
+ report_change do
346
+ change_set.execute
347
+ end
348
+ end
349
+
350
+ end
351
+
352
+ subcommand "delete", "Delete the change-set." do
353
+
354
+ def execute
355
+ report_change do
356
+ change_set.delete
357
+ end
358
+ end
359
+
360
+ end
361
+
362
+ private
363
+
364
+ def change_set
365
+ stack.change_set(change_set_name)
366
+ end
367
+
368
+ end
369
+
370
+ subcommand "diff", "Compare template/params to current stack." do
371
+
372
+ option "--diff-format", "FORMAT", "'text', 'color', or 'html'", :default => "color"
373
+
374
+ option ["-C", "--context-lines"], "LINES", "number of lines of context to show", :default => 10_000
375
+
376
+ option ["-t", "--template"], "FILE", "template source",
377
+ :attribute_name => :template_source,
378
+ &Stackup::Source.method(:new)
379
+
380
+ include HasParameters
381
+
382
+ option "--tags", "FILE", "stack tags file",
383
+ :attribute_name => :tag_source,
384
+ &Stackup::Source.method(:new)
385
+
386
+ def execute
387
+ current = {}
388
+ planned = {}
389
+ if template_source
390
+ current["Template"] = stack.template
391
+ planned["Template"] = template_source.data
392
+ end
393
+ unless parameter_sources.empty?
394
+ current["Parameters"] = existing_parameters.sort.to_h
395
+ planned["Parameters"] = new_parameters.sort.to_h
396
+ end
397
+ if tag_source
398
+ current["Tags"] = stack.tags.sort.to_h
399
+ planned["Tags"] = tag_source.data.sort.to_h
400
+ end
401
+ signal_usage_error "specify '--template' or '--parameters'" if planned.empty?
402
+ puts differ.diff(current, planned, context_lines)
403
+ end
404
+
405
+ private
406
+
407
+ def differ
408
+ Stackup::Differ.new(diff_format, &method(:format_data))
409
+ end
410
+
411
+ def existing_parameters
412
+ @existing_parameters ||= stack.parameters
413
+ end
414
+
415
+ def new_parameters
416
+ existing_parameters.merge(parameters)
417
+ end
418
+
419
+ end
420
+
421
+ subcommand ["down", "delete"], "Remove the stack." do
422
+
423
+ def execute
424
+ report_change do
425
+ stack.delete
426
+ end
427
+ end
428
+
429
+ end
430
+
431
+ subcommand "cancel-update", "Cancel the update in-progress." do
432
+
433
+ def execute
434
+ report_change do
435
+ stack.cancel_update
436
+ end
437
+ end
438
+
439
+ end
440
+
441
+ subcommand "wait", "Wait until stack is stable." do
442
+
443
+ def execute
444
+ puts stack.wait
445
+ end
446
+
447
+ end
448
+
449
+ subcommand "events", "List stack events." do
450
+
451
+ option ["-f", "--follow"], :flag, "follow new events"
452
+ option ["--data"], :flag, "display events as data"
453
+
454
+ def execute
455
+ stack.watch(false) do |watcher|
456
+ loop do
457
+ watcher.each_new_event do |event|
458
+ display_event(event)
459
+ end
460
+ break unless follow?
461
+
462
+ sleep 5
463
+ end
464
+ end
465
+ end
466
+
467
+ private
468
+
469
+ def display_event(e)
470
+ if data?
471
+ display_data(event_data(e))
472
+ else
473
+ puts event_summary(e)
474
+ end
475
+ end
476
+
477
+ def event_data(e)
478
+ {
479
+ "timestamp" => e.timestamp.localtime,
480
+ "logical_resource_id" => e.logical_resource_id,
481
+ "physical_resource_id" => e.physical_resource_id,
482
+ "resource_status" => e.resource_status,
483
+ "resource_status_reason" => e.resource_status_reason
484
+ }.reject { |_k, v| blank?(v) }
485
+ end
486
+
487
+ def blank?(v)
488
+ v.nil? || v.respond_to?(:empty?) && v.empty?
489
+ end
490
+
491
+ def event_summary(e)
492
+ summary = "[#{e.timestamp.localtime.iso8601}] #{e.logical_resource_id}"
493
+ summary += " - #{e.resource_status}"
494
+ summary += " - #{e.resource_status_reason}" if e.resource_status_reason
495
+ summary
496
+ end
497
+
498
+ end
499
+
500
+ subcommand "template", "Display stack template." do
501
+
502
+ def execute
503
+ display_data(stack.template)
504
+ end
505
+
506
+ end
507
+
508
+ subcommand ["parameters", "params"], "Display stack parameters." do
509
+
510
+ def execute
511
+ display_data(stack.parameters)
512
+ end
513
+
514
+ end
515
+
516
+ subcommand "tags", "Display stack tags." do
517
+
518
+ def execute
519
+ display_data(stack.tags)
520
+ end
521
+
522
+ end
523
+
524
+ subcommand "resources", "Display stack resources." do
525
+
526
+ def execute
527
+ display_data(stack.resources)
528
+ end
529
+
530
+ end
531
+
532
+ subcommand "outputs", "Display stack outputs." do
533
+
534
+ def execute
535
+ display_data(stack.outputs)
536
+ end
537
+
538
+ end
539
+
540
+ subcommand "inspect", "Display stack particulars." do
541
+
542
+ def execute
543
+ data = {
544
+ "Status" => stack.status,
545
+ "Parameters" => stack.parameters,
546
+ "Tags" => stack.tags,
547
+ "Resources" => stack.resources,
548
+ "Outputs" => stack.outputs
549
+ }
550
+ display_data(data)
551
+ end
552
+
553
+ end
554
+
555
+ end
556
+
557
+ end