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