stack_master 2.17.1 → 2.18.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/bin/stack_master +3 -5
  4. data/lib/stack_master/aws_driver/cloud_formation.rb +4 -4
  5. data/lib/stack_master/aws_driver/s3.rb +7 -7
  6. data/lib/stack_master/change_set.rb +7 -11
  7. data/lib/stack_master/cli.rb +32 -26
  8. data/lib/stack_master/command.rb +1 -5
  9. data/lib/stack_master/commands/apply.rb +34 -19
  10. data/lib/stack_master/commands/delete.rb +4 -4
  11. data/lib/stack_master/commands/drift.rb +5 -9
  12. data/lib/stack_master/commands/events.rb +4 -6
  13. data/lib/stack_master/commands/init.rb +14 -14
  14. data/lib/stack_master/commands/lint.rb +1 -1
  15. data/lib/stack_master/commands/list_stacks.rb +1 -1
  16. data/lib/stack_master/commands/outputs.rb +1 -1
  17. data/lib/stack_master/commands/status.rb +4 -4
  18. data/lib/stack_master/commands/terminal_helper.rb +3 -3
  19. data/lib/stack_master/commands/tidy.rb +14 -16
  20. data/lib/stack_master/config.rb +8 -11
  21. data/lib/stack_master/diff.rb +2 -2
  22. data/lib/stack_master/parameter_loader.rb +1 -2
  23. data/lib/stack_master/parameter_resolver.rb +14 -17
  24. data/lib/stack_master/parameter_resolvers/ami_finder.rb +1 -2
  25. data/lib/stack_master/parameter_resolvers/ejson.rb +4 -4
  26. data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +1 -1
  27. data/lib/stack_master/parameter_resolvers/latest_container.rb +3 -3
  28. data/lib/stack_master/parameter_resolvers/one_password.rb +5 -5
  29. data/lib/stack_master/parameter_resolvers/sso_group_id.rb +1 -1
  30. data/lib/stack_master/parameter_resolvers/stack_output.rb +7 -11
  31. data/lib/stack_master/parameter_validator.rb +1 -5
  32. data/lib/stack_master/prompter.rb +1 -1
  33. data/lib/stack_master/resolver_array.rb +2 -2
  34. data/lib/stack_master/role_assumer.rb +1 -1
  35. data/lib/stack_master/security_group_finder.rb +4 -4
  36. data/lib/stack_master/sns_topic_finder.rb +1 -1
  37. data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +1 -1
  38. data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +1 -1
  39. data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +7 -7
  40. data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +1 -3
  41. data/lib/stack_master/sparkle_formation/template_file.rb +2 -2
  42. data/lib/stack_master/sso_group_id_finder.rb +3 -3
  43. data/lib/stack_master/stack.rb +6 -4
  44. data/lib/stack_master/stack_definition.rb +3 -4
  45. data/lib/stack_master/stack_differ.rb +32 -9
  46. data/lib/stack_master/template_compilers/json.rb +1 -1
  47. data/lib/stack_master/template_compilers/sparkle_formation.rb +2 -2
  48. data/lib/stack_master/template_utils.rb +2 -2
  49. data/lib/stack_master/test_driver/cloud_formation.rb +11 -3
  50. data/lib/stack_master/test_driver/s3.rb +2 -3
  51. data/lib/stack_master/utils.rb +3 -6
  52. data/lib/stack_master/validator.rb +1 -1
  53. data/lib/stack_master/version.rb +1 -1
  54. data/lib/stack_master.rb +1 -1
  55. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e73f1b3d79a230203a083898d14d256ac7c61fb0dd4a49627af097759341ca96
4
- data.tar.gz: 193c5824b8972d6ddc36f66dd23d83360c1af43562a3b038c413bb6ff624626a
3
+ metadata.gz: ffa37da4e3a6d6d80804a60be1b868180a67470009472b02a0c838354028efb6
4
+ data.tar.gz: e04ddf265d427425f0a1a60973eec74e2e6cc42e3cc47f00b8cbf584f1cdac93
5
5
  SHA512:
6
- metadata.gz: 41d00dda80224d66ae46e73746b43f1e0738ef075ad8ce3122494db579c545b6cd2b15497622cf5aa683efa2bf6f67f411a031c1cb5063c0a94b40dcef8261a0
7
- data.tar.gz: e26a3fa7eeae1b2be191fa579e585f34f8f781c4b94e68d42c0619a3d1effbe86cd0761b119a79058a86d98f113d6db573dfbfd0b733792ca9d21554de98684c
6
+ metadata.gz: fb887f7ebe035ec7ffd808af93ad5cd69329a161dfcef8f6986e1eb79d975ce81c8b7d344403d21ca9bcaa3faafa9e91d1ef50b17cbdf79d35f88fd4cf69926f
7
+ data.tar.gz: d59f604ca4b6881c928525ea5c8c88f3d07a8dde8899b27ed427223f6102d78c27a59e1cbcb51588e39653221c3c90fceaa758b809ab2d161df8d6bfed3da036
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/stack_master/blob/master/LICENSE.md)
4
4
  [![Gem Version](https://badge.fury.io/rb/stack_master.svg)](https://badge.fury.io/rb/stack_master)
5
- [![Build Status](https://github.com/envato/stack_master/workflows/tests/badge.svg?branch=master)](https://github.com/envato/stack_master/actions?query=workflow%3Atests+branch%3Amaster)
5
+ [![Build Status](https://github.com/envato/stack_master/actions/workflows/test.yml/badge.svg)](https://github.com/envato/stack_master/actions/workflows/test.yml)
6
6
 
7
7
  StackMaster is a CLI tool to manage [CloudFormation](https://aws.amazon.com/cloudformation/) stacks, with the following features:
8
8
 
data/bin/stack_master CHANGED
@@ -2,14 +2,12 @@
2
2
 
3
3
  require 'stack_master'
4
4
 
5
- if ENV['STUB_AWS'] == 'true'
6
- require 'stack_master/testing'
7
- end
5
+ require 'stack_master/testing' if ENV['STUB_AWS'] == 'true'
8
6
 
9
- trap("SIGINT") { raise StackMaster::CtrlC }
7
+ trap('SIGINT') { raise StackMaster::CtrlC }
10
8
 
11
9
  begin
12
10
  StackMaster::CLI.new(ARGV.dup).execute!
13
11
  rescue StackMaster::CtrlC
14
- StackMaster.stdout.puts "Exiting..."
12
+ StackMaster.stdout.puts 'Exiting...'
15
13
  end
@@ -8,10 +8,10 @@ module StackMaster
8
8
  end
9
9
 
10
10
  def set_region(value)
11
- if region != value
12
- @region = value
13
- @cf = nil
14
- end
11
+ return if region == value
12
+
13
+ @region = value
14
+ @cf = nil
15
15
  end
16
16
 
17
17
  def_delegators(
@@ -24,11 +24,11 @@ module StackMaster
24
24
  prefix: prefix,
25
25
  bucket: bucket
26
26
  }
27
- ).map(&:contents).flatten.inject({}) { |h, obj|
27
+ ).map(&:contents).flatten.inject({}) do |h, obj|
28
28
  h.merge(obj.key => obj)
29
- }
29
+ end
30
30
 
31
- StackMaster.stdout.puts "Uploading files to S3:"
31
+ StackMaster.stdout.puts 'Uploading files to S3:'
32
32
 
33
33
  files.each do |template, file|
34
34
  body = file.fetch(:body)
@@ -36,7 +36,7 @@ module StackMaster
36
36
  object_key = template.dup
37
37
  object_key.prepend("#{prefix}/") if prefix
38
38
  compiled_template_md5 = Digest::MD5.hexdigest(body).to_s
39
- s3_md5 = current_objects[object_key] ? current_objects[object_key].etag.gsub("\"", '') : nil
39
+ s3_md5 = current_objects[object_key] ? current_objects[object_key].etag.gsub('"', '') : nil
40
40
 
41
41
  next if compiled_template_md5 == s3_md5
42
42
 
@@ -51,14 +51,14 @@ module StackMaster
51
51
  metadata: { md5: compiled_template_md5 }
52
52
  }
53
53
  )
54
- StackMaster.stdout.puts "done."
54
+ StackMaster.stdout.puts 'done.'
55
55
  end
56
56
  end
57
57
 
58
58
  def url(bucket:, prefix:, region:, template:)
59
59
  if region == 'us-east-1'
60
- ["https://s3.amazonaws.com", bucket, prefix, template].compact.join('/')
61
- elsif region.start_with? "cn-"
60
+ ['https://s3.amazonaws.com', bucket, prefix, template].compact.join('/')
61
+ elsif region.start_with? 'cn-'
62
62
  ["https://s3.#{region}.amazonaws.com.cn", bucket, prefix, template].compact.join('/')
63
63
  else
64
64
  ["https://s3-#{region}.amazonaws.com", bucket, prefix, template].compact.join('/')
@@ -1,9 +1,9 @@
1
1
  module StackMaster
2
2
  class ChangeSet
3
- END_STATES = [
4
- 'CREATE_COMPLETE',
5
- 'DELETE_COMPLETE',
6
- 'FAILED'
3
+ END_STATES = %w[
4
+ CREATE_COMPLETE
5
+ DELETE_COMPLETE
6
+ FAILED
7
7
  ]
8
8
 
9
9
  def self.generate_change_set_name(stack_name)
@@ -50,7 +50,7 @@ module StackMaster
50
50
  @response.changes.each do |change|
51
51
  display_resource_change(io, change.resource_change)
52
52
  end
53
- io.puts "========================================"
53
+ io.puts '========================================'
54
54
  end
55
55
 
56
56
  def failed?
@@ -84,13 +84,9 @@ module StackMaster
84
84
  def display_resource_change_detail(io, action_name, color, detail)
85
85
  target_name = [detail.target.attribute, detail.target.name].compact.join('.')
86
86
  detail_messages = [target_name]
87
- if action_name == 'Replace'
88
- detail_messages << "#{detail.target.requires_recreation} requires recreation"
89
- end
87
+ detail_messages << "#{detail.target.requires_recreation} requires recreation" if action_name == 'Replace'
90
88
  triggered_by = [detail.change_source, detail.causing_entity].compact.join('.')
91
- if detail.evaluation != 'Static'
92
- triggered_by << "(#{detail.evaluation})"
93
- end
89
+ triggered_by << "(#{detail.evaluation})" if detail.evaluation != 'Static'
94
90
  detail_messages << "Triggered by: #{triggered_by}"
95
91
  io.puts Rainbow("- #{detail_messages.join('. ')}. ").color(color)
96
92
  end
@@ -6,7 +6,11 @@ module StackMaster
6
6
  include Commander::Methods
7
7
 
8
8
  def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
9
- @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
9
+ @argv = argv
10
+ @stdin = stdin
11
+ @stdout = stdout
12
+ @stderr = stderr
13
+ @kernel = kernel
10
14
  Commander::Runner.instance_variable_set('@instance', Commander::Runner.new(argv))
11
15
  StackMaster.stdout = @stdout
12
16
  StackMaster.stderr = @stderr
@@ -49,7 +53,7 @@ module StackMaster
49
53
  'Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. ' \
50
54
  "Default: ROLLBACK\n" \
51
55
  'Note: You cannot use this option with Serverless Application Model (SAM) templates.'
52
- c.option '--yes-param PARAM_NAME', String, "Auto-approve stack updates when only parameter PARAM_NAME changes"
56
+ c.option '--yes-param PARAM_NAME', String, 'Auto-approve stack updates when only parameter PARAM_NAME changes'
53
57
  c.action do |args, options|
54
58
  options.default config: default_config_file
55
59
  execute_stacks_command(StackMaster::Commands::Apply, args, options)
@@ -59,7 +63,7 @@ module StackMaster
59
63
  command :outputs do |c|
60
64
  c.syntax = 'stack_master outputs [region_or_alias] [stack_name]'
61
65
  c.summary = 'Displays outputs for a stack'
62
- c.description = "Displays outputs for a stack"
66
+ c.description = 'Displays outputs for a stack'
63
67
  c.action do |args, options|
64
68
  options.default config: default_config_file
65
69
  execute_stacks_command(StackMaster::Commands::Outputs, args, options)
@@ -73,10 +77,10 @@ module StackMaster
73
77
  c.option('--overwrite', 'Overwrite existing files')
74
78
  c.action do |args, options|
75
79
  options.default config: default_config_file
76
- unless args.size == 2
77
- say "Invalid arguments. stack_master init [region] [stack_name]"
78
- else
80
+ if args.size == 2
79
81
  StackMaster::Commands::Init.perform(options, *args)
82
+ else
83
+ say 'Invalid arguments. stack_master init [region] [stack_name]'
80
84
  end
81
85
  end
82
86
  end
@@ -94,8 +98,8 @@ module StackMaster
94
98
 
95
99
  command :events do |c|
96
100
  c.syntax = 'stack_master events [region_or_alias] [stack_name]'
97
- c.summary = "Shows events for a stack"
98
- c.description = "Shows events for a stack"
101
+ c.summary = 'Shows events for a stack'
102
+ c.description = 'Shows events for a stack'
99
103
  c.example 'show events for myapp-vpc in us-east-1', 'stack_master events us-east-1 myapp-vpc'
100
104
  c.option '--number Integer', Integer, 'Number of recent events to show'
101
105
  c.option '--all', 'Show all events'
@@ -108,8 +112,8 @@ module StackMaster
108
112
 
109
113
  command :resources do |c|
110
114
  c.syntax = 'stack_master resources [region] [stack_name]'
111
- c.summary = "Shows stack resources"
112
- c.description = "Shows stack resources"
115
+ c.summary = 'Shows stack resources'
116
+ c.description = 'Shows stack resources'
113
117
  c.action do |args, options|
114
118
  options.default config: default_config_file
115
119
  execute_stacks_command(StackMaster::Commands::Resources, args, options)
@@ -122,7 +126,7 @@ module StackMaster
122
126
  c.description = 'List stack definitions'
123
127
  c.action do |args, options|
124
128
  options.default config: default_config_file
125
- say "Invalid arguments." if args.size > 0
129
+ say 'Invalid arguments.' if args.size > 0
126
130
  config = load_config(options.config)
127
131
  StackMaster::Commands::ListStacks.perform(config, nil, options)
128
132
  end
@@ -142,8 +146,8 @@ module StackMaster
142
146
 
143
147
  command :lint do |c|
144
148
  c.syntax = 'stack_master lint [region_or_alias] [stack_name]'
145
- c.summary = "Check the stack definition locally"
146
- c.description = "Runs cfn-lint on the template which would be sent to AWS on apply"
149
+ c.summary = 'Check the stack definition locally'
150
+ c.description = 'Runs cfn-lint on the template which would be sent to AWS on apply'
147
151
  c.example 'run cfn-lint on stack myapp-vpc with us-east-1 settings', 'stack_master lint us-east-1 myapp-vpc'
148
152
  c.action do |args, options|
149
153
  options.default config: default_config_file
@@ -154,7 +158,7 @@ module StackMaster
154
158
  command :nag do |c|
155
159
  c.syntax = 'stack_master nag [region_or_alias] [stack_name]'
156
160
  c.summary = "Check this stack's template with cfn_nag"
157
- c.description = "Runs SAST scan cfn_nag on the template"
161
+ c.description = 'Runs SAST scan cfn_nag on the template'
158
162
  c.example 'run cfn_nag on stack myapp-vpc with us-east-1 settings', 'stack_master nag us-east-1 myapp-vpc'
159
163
  c.action do |args, options|
160
164
  options.default config: default_config_file
@@ -164,7 +168,7 @@ module StackMaster
164
168
 
165
169
  command :compile do |c|
166
170
  c.syntax = 'stack_master compile [region_or_alias] [stack_name]'
167
- c.summary = "Print the compiled version of a given stack"
171
+ c.summary = 'Print the compiled version of a given stack'
168
172
  c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS"
169
173
  c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc'
170
174
  c.action do |args, options|
@@ -181,7 +185,7 @@ module StackMaster
181
185
  c.example 'description', 'Check the status of all stack definitions'
182
186
  c.action do |args, options|
183
187
  options.default config: default_config_file
184
- say "Invalid arguments. stack_master status" and return unless args.size == 0
188
+ say 'Invalid arguments. stack_master status' and return unless args.size == 0
185
189
 
186
190
  config = load_config(options.config)
187
191
  StackMaster::Commands::Status.perform(config, nil, options)
@@ -196,7 +200,7 @@ module StackMaster
196
200
  c.example 'description', 'Check for missing or extra files'
197
201
  c.action do |args, options|
198
202
  options.default config: default_config_file
199
- say "Invalid arguments. stack_master tidy" and return unless args.size == 0
203
+ say 'Invalid arguments. stack_master tidy' and return unless args.size == 0
200
204
 
201
205
  config = load_config(options.config)
202
206
  StackMaster::Commands::Tidy.perform(config, nil, options)
@@ -211,7 +215,7 @@ module StackMaster
211
215
  c.action do |args, options|
212
216
  options.default config: default_config_file
213
217
  unless args.size == 2
214
- say "Invalid arguments. stack_master delete [region] [stack_name]"
218
+ say 'Invalid arguments. stack_master delete [region] [stack_name]'
215
219
  return
216
220
  end
217
221
 
@@ -239,7 +243,7 @@ module StackMaster
239
243
  c.syntax = 'stack_master drift [region_or_alias] [stack_name]'
240
244
  c.summary = 'Detects and displays stack drift using the CloudFormation Drift API'
241
245
  c.description = 'Detects and displays stack drift'
242
- c.option '--timeout SECONDS', Integer, "The number of seconds to wait for drift detection to complete"
246
+ c.option '--timeout SECONDS', Integer, 'The number of seconds to wait for drift detection to complete'
243
247
  c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
244
248
  c.action do |args, options|
245
249
  options.default config: default_config_file, timeout: 120
@@ -253,7 +257,7 @@ module StackMaster
253
257
  private
254
258
 
255
259
  def default_config_file
256
- "stack_master.yml"
260
+ 'stack_master.yml'
257
261
  end
258
262
 
259
263
  def load_config(file)
@@ -277,10 +281,12 @@ module StackMaster
277
281
  show_other_region_candidates(config, stack_name)
278
282
  success = false
279
283
  end
280
- stack_definitions = stack_definitions.select do |stack_definition|
281
- running_in_allowed_account?(stack_definition.allowed_accounts) &&
282
- StackStatus.new(config, stack_definition).changed?
283
- end if options.changed
284
+ if options.changed
285
+ stack_definitions = stack_definitions.select do |stack_definition|
286
+ running_in_allowed_account?(stack_definition.allowed_accounts) &&
287
+ StackStatus.new(config, stack_definition).changed?
288
+ end
289
+ end
284
290
  stack_definitions.each do |stack_definition|
285
291
  StackMaster.cloud_formation_driver.set_region(stack_definition.region)
286
292
  StackMaster.stdout.puts(
@@ -295,14 +301,14 @@ module StackMaster
295
301
  end
296
302
 
297
303
  def show_other_region_candidates(config, stack_name)
298
- candidates = config.filter(region = "", stack_name = stack_name)
304
+ candidates = config.filter(region = '', stack_name = stack_name)
299
305
  return if candidates.empty?
300
306
 
301
307
  StackMaster.stdout.puts "Stack name #{stack_name} exists in regions: #{candidates.map(&:region).join(', ')}"
302
308
  end
303
309
 
304
310
  def execute_if_allowed_account(allowed_accounts, &block)
305
- raise ArgumentError, "Block required to execute this method" unless block_given?
311
+ raise ArgumentError, 'Block required to execute this method' unless block_given?
306
312
 
307
313
  if running_in_allowed_account?(allowed_accounts)
308
314
  block.call
@@ -43,11 +43,7 @@ module StackMaster
43
43
  msg = "#{e.class} #{e.message}"
44
44
  msg << "\n Caused by: #{e.cause.class} #{e.cause.message}" if e.cause
45
45
  msg << "\n at #{e.cause.backtrace[0..3].join("\n ")}\n ..." if e.cause && !options.trace
46
- if options.trace
47
- msg << "\n#{backtrace(e)}"
48
- else
49
- msg << "\n Use --trace to view backtrace"
50
- end
46
+ msg << (options.trace ? "\n#{backtrace(e)}" : "\n Use --trace to view backtrace")
51
47
  msg
52
48
  end
53
49
 
@@ -44,10 +44,10 @@ module StackMaster
44
44
  end
45
45
 
46
46
  def abort_if_review_in_progress
47
- if stack_exists? && stack.stack_status == "REVIEW_IN_PROGRESS"
48
- StackMaster.stderr.puts "Stack currently exists and is in #{stack.stack_status}"
49
- failed! "You will need to delete the stack (#{stack.stack_name}) before continuing"
50
- end
47
+ return unless stack_exists? && stack.stack_status == 'REVIEW_IN_PROGRESS'
48
+
49
+ StackMaster.stderr.puts "Stack currently exists and is in #{stack.stack_status}"
50
+ failed! "You will need to delete the stack (#{stack.stack_name}) before continuing"
51
51
  end
52
52
 
53
53
  def use_s3?
@@ -89,7 +89,7 @@ module StackMaster
89
89
  @change_set = ChangeSet.create(stack_options.merge(change_set_type: 'CREATE'))
90
90
  if @change_set.failed?
91
91
  ChangeSet.delete(@change_set.id)
92
- halt!(@change_set.status_reason)
92
+ halt!(user_friendly_changeset_error(@change_set.status_reason))
93
93
  end
94
94
 
95
95
  @change_set.display(StackMaster.stdout)
@@ -111,11 +111,11 @@ module StackMaster
111
111
  end
112
112
 
113
113
  def ask_to_cancel_stack_update
114
- if ask?("Cancel stack update?")
115
- StackMaster.stdout.puts "Attempting to cancel stack update"
116
- cf.cancel_update_stack(stack_name: stack_name)
117
- tail_stack_events
118
- end
114
+ return unless ask?('Cancel stack update?')
115
+
116
+ StackMaster.stdout.puts 'Attempting to cancel stack update'
117
+ cf.cancel_update_stack(stack_name: stack_name)
118
+ tail_stack_events
119
119
  end
120
120
 
121
121
  def update_stack
@@ -123,7 +123,7 @@ module StackMaster
123
123
  @change_set = ChangeSet.create(stack_options)
124
124
  if @change_set.failed?
125
125
  ChangeSet.delete(@change_set.id)
126
- halt!(@change_set.status_reason)
126
+ halt!(user_friendly_changeset_error(@change_set.status_reason))
127
127
  end
128
128
 
129
129
  @change_set.display(StackMaster.stdout)
@@ -136,10 +136,10 @@ module StackMaster
136
136
  end
137
137
 
138
138
  def ask_update_confirmation!
139
- unless ask?("Apply change set (y/n)? ")
140
- ChangeSet.delete(@change_set.id)
141
- halt! "Stack update aborted"
142
- end
139
+ return if ask?('Apply change set (y/n)? ')
140
+
141
+ ChangeSet.delete(@change_set.id)
142
+ halt! 'Stack update aborted'
143
143
  end
144
144
 
145
145
  def upload_files
@@ -177,7 +177,7 @@ module StackMaster
177
177
  stack_name: stack_name,
178
178
  parameters: proposed_stack.aws_parameters,
179
179
  tags: proposed_stack.aws_tags,
180
- capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'],
180
+ capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND],
181
181
  role_arn: proposed_stack.role_arn,
182
182
  notification_arns: proposed_stack.notification_arns,
183
183
  template_method => template_value
@@ -211,9 +211,9 @@ module StackMaster
211
211
  end
212
212
 
213
213
  def ensure_valid_template_body_size!
214
- if proposed_stack.too_big?(use_s3?)
215
- failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
216
- end
214
+ return unless proposed_stack.too_big?(use_s3?)
215
+
216
+ failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
217
217
  end
218
218
 
219
219
  def set_stack_policy
@@ -230,6 +230,21 @@ module StackMaster
230
230
  StackMaster.stdout.puts 'done.'
231
231
  end
232
232
 
233
+ def user_friendly_changeset_error(status_reason)
234
+ # CloudFormation returns various messages when there are no changes to apply
235
+ if status_reason =~ /didn'?t contain changes|no changes|no updates are to be performed/i
236
+ <<~MESSAGE.chomp
237
+ #{status_reason}
238
+
239
+ While there may be differences in the template file (e.g., whitespace, comments, or
240
+ formatting), CloudFormation has determined that no actual resource changes are needed.
241
+ The stack is already in the desired state.
242
+ MESSAGE
243
+ else
244
+ status_reason
245
+ end
246
+ end
247
+
233
248
  extend Forwardable
234
249
  def_delegators :@stack_definition, :stack_name, :region
235
250
  end
@@ -15,7 +15,7 @@ module StackMaster
15
15
  return unless check_exists
16
16
 
17
17
  unless ask?("Really delete stack #{@stack_name} (y/n)? ")
18
- StackMaster.stdout.puts "Stack update aborted"
18
+ StackMaster.stdout.puts 'Stack update aborted'
19
19
  return
20
20
  end
21
21
 
@@ -33,7 +33,7 @@ module StackMaster
33
33
  cf.describe_stacks({ stack_name: @stack_name })
34
34
  true
35
35
  rescue Aws::CloudFormation::Errors::ValidationError
36
- failed("Stack does not exist")
36
+ failed('Stack does not exist')
37
37
  false
38
38
  end
39
39
 
@@ -43,10 +43,10 @@ module StackMaster
43
43
 
44
44
  def tail_stack_events
45
45
  StackEvents::Streamer.stream(@stack_name, @region, io: StackMaster.stdout, from: @from_time)
46
- StackMaster.stdout.puts "Stack deleted"
46
+ StackMaster.stdout.puts 'Stack deleted'
47
47
  rescue Aws::CloudFormation::Errors::ValidationError
48
48
  # Unfortunately the stack as a tendency of going away before we get the final delete event.
49
- StackMaster.stdout.puts "Stack deleted"
49
+ StackMaster.stdout.puts 'Stack deleted'
50
50
  end
51
51
  end
52
52
  end
@@ -6,9 +6,9 @@ module StackMaster
6
6
  include Command
7
7
  include Commander::UI
8
8
 
9
- DETECTION_COMPLETE_STATES = [
10
- 'DETECTION_COMPLETE',
11
- 'DETECTION_FAILED'
9
+ DETECTION_COMPLETE_STATES = %w[
10
+ DETECTION_COMPLETE
11
+ DETECTION_FAILED
12
12
  ]
13
13
 
14
14
  def perform
@@ -41,9 +41,7 @@ module StackMaster
41
41
  drift.physical_resource_id].join(' '), color)
42
42
  return unless drift.stack_resource_drift_status == 'MODIFIED'
43
43
 
44
- unless drift.property_differences.empty?
45
- puts colorize(' Property differences:', color)
46
- end
44
+ puts colorize(' Property differences:', color) unless drift.property_differences.empty?
47
45
  drift.property_differences.each do |property_difference|
48
46
  puts colorize(" - #{property_difference.difference_type} #{property_difference.property_path}", color)
49
47
  end
@@ -98,9 +96,7 @@ module StackMaster
98
96
  break if DETECTION_COMPLETE_STATES.include?(resp.detection_status)
99
97
 
100
98
  elapsed_time = Time.now - start_time
101
- if elapsed_time > @options.timeout
102
- raise "Timeout waiting for stack drift detection"
103
- end
99
+ raise 'Timeout waiting for stack drift detection' if elapsed_time > @options.timeout
104
100
 
105
101
  sleep SLEEP_SECONDS
106
102
  end
@@ -9,9 +9,9 @@ module StackMaster
9
9
  filter_events(events).each do |event|
10
10
  StackEvents::Presenter.print_event(StackMaster.stdout, event)
11
11
  end
12
- if @options.tail
13
- StackEvents::Streamer.stream(@stack_definition.stack_name, @stack_definition.region, io: StackMaster.stdout)
14
- end
12
+ return unless @options.tail
13
+
14
+ StackEvents::Streamer.stream(@stack_definition.stack_name, @stack_definition.region, io: StackMaster.stdout)
15
15
  end
16
16
 
17
17
  private
@@ -22,9 +22,7 @@ module StackMaster
22
22
  else
23
23
  n = @options.number || 25
24
24
  from = events.count - n
25
- if from < 0
26
- from = 0
27
- end
25
+ from = 0 if from < 0
28
26
  events[from..-1]
29
27
  end
30
28
  end
@@ -1,4 +1,4 @@
1
- require "erb"
1
+ require 'erb'
2
2
 
3
3
  module StackMaster
4
4
  module Commands
@@ -12,22 +12,22 @@ module StackMaster
12
12
  end
13
13
 
14
14
  def perform
15
- if check_files
16
- create_stack_master_yml
17
- create_stack_json_yml
18
- create_parameters_yml
19
- end
15
+ return unless check_files
16
+
17
+ create_stack_master_yml
18
+ create_stack_json_yml
19
+ create_parameters_yml
20
20
  end
21
21
 
22
22
  private
23
23
 
24
24
  def check_files
25
- @stack_master_filename = "stack_master.yml"
25
+ @stack_master_filename = 'stack_master.yml'
26
26
  @stack_json_filename = "templates/#{@stack_name}.json"
27
- @parameters_filename = File.join("parameters", "#{@stack_name}.yml")
28
- @region_parameters_filename = File.join("parameters", @region, "#{@stack_name}.yml")
27
+ @parameters_filename = File.join('parameters', "#{@stack_name}.yml")
28
+ @region_parameters_filename = File.join('parameters', @region, "#{@stack_name}.yml")
29
29
 
30
- if !@options.overwrite
30
+ unless @options.overwrite
31
31
  [@stack_master_filename, @stack_json_filename, @parameters_filename,
32
32
  @region_parameters_filename].each do |filename|
33
33
  next unless File.exist?(filename)
@@ -50,7 +50,7 @@ module StackMaster
50
50
  end
51
51
 
52
52
  def stack_json_template
53
- File.join(StackMaster.base_dir, "stacktemplates", "stack.json.erb")
53
+ File.join(StackMaster.base_dir, 'stacktemplates', 'stack.json.erb')
54
54
  end
55
55
 
56
56
  def create_stack_master_yml
@@ -63,7 +63,7 @@ module StackMaster
63
63
  end
64
64
 
65
65
  def stack_master_template
66
- File.join(StackMaster.base_dir, "stacktemplates", "stack_master.yml.erb")
66
+ File.join(StackMaster.base_dir, 'stacktemplates', 'stack_master.yml.erb')
67
67
  end
68
68
 
69
69
  def create_parameters_yml
@@ -83,11 +83,11 @@ module StackMaster
83
83
  end
84
84
 
85
85
  def parameter_stack_name_template
86
- File.join(StackMaster.base_dir, "stacktemplates", "parameter_stack_name.yml")
86
+ File.join(StackMaster.base_dir, 'stacktemplates', 'parameter_stack_name.yml')
87
87
  end
88
88
 
89
89
  def parameter_region_template
90
- File.join(StackMaster.base_dir, "stacktemplates", "parameter_region.yml")
90
+ File.join(StackMaster.base_dir, 'stacktemplates', 'parameter_region.yml')
91
91
  end
92
92
 
93
93
  def render(renderer)
@@ -19,7 +19,7 @@ module StackMaster
19
19
  f.write(proposed_stack.template_body)
20
20
  f.flush
21
21
  system('cfn-lint', f.path)
22
- puts "cfn-lint run complete"
22
+ puts 'cfn-lint run complete'
23
23
  end
24
24
  end
25
25
 
@@ -8,7 +8,7 @@ module StackMaster
8
8
  include StackMaster::Commands::TerminalHelper
9
9
 
10
10
  def perform
11
- tp.set :max_width, self.window_size
11
+ tp.set :max_width, window_size
12
12
  tp @config.stacks, :region, :stack_name
13
13
  end
14
14
  end
@@ -9,7 +9,7 @@ module StackMaster
9
9
 
10
10
  def perform
11
11
  if stack
12
- tp.set :max_width, self.window_size
12
+ tp.set :max_width, window_size
13
13
  tp stack.outputs, :output_key, :output_value, :description
14
14
  else
15
15
  failed("Stack doesn't exist")
@@ -21,11 +21,11 @@ module StackMaster
21
21
  {
22
22
  region: stack_definition.region,
23
23
  stack_name: stack_definition.stack_name,
24
- stack_status: running_in_allowed_account?(allowed_accounts) ? stack_status.status : "Disallowed account",
25
- different: running_in_allowed_account?(allowed_accounts) ? stack_status.changed_message : "N/A",
24
+ stack_status: running_in_allowed_account?(allowed_accounts) ? stack_status.status : 'Disallowed account',
25
+ different: running_in_allowed_account?(allowed_accounts) ? stack_status.changed_message : 'N/A'
26
26
  }
27
27
  end
28
- tp.set :max_width, self.window_size
28
+ tp.set :max_width, window_size
29
29
  tp.set :io, StackMaster.stdout
30
30
  tp status
31
31
  StackMaster.stdout.puts " * No echo parameters can't be diffed"
@@ -34,7 +34,7 @@ module StackMaster
34
34
  private
35
35
 
36
36
  def progress
37
- @progress ||= ProgressBar.create(title: "Fetching stack information",
37
+ @progress ||= ProgressBar.create(title: 'Fetching stack information',
38
38
  total: @config.stacks.size,
39
39
  output: StackMaster.stdout)
40
40
  end