stack_master 2.17.0 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -6
  3. data/bin/stack_master +3 -5
  4. data/lib/stack_master/aws_driver/cloud_formation.rb +25 -23
  5. data/lib/stack_master/aws_driver/s3.rb +24 -17
  6. data/lib/stack_master/change_set.rb +11 -15
  7. data/lib/stack_master/cli.rb +53 -32
  8. data/lib/stack_master/command.rb +1 -5
  9. data/lib/stack_master/commands/apply.rb +39 -20
  10. data/lib/stack_master/commands/delete.rb +6 -7
  11. data/lib/stack_master/commands/drift.rb +11 -12
  12. data/lib/stack_master/commands/events.rb +4 -6
  13. data/lib/stack_master/commands/init.rb +21 -20
  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/nag.rb +0 -1
  17. data/lib/stack_master/commands/outputs.rb +1 -1
  18. data/lib/stack_master/commands/resources.rb +9 -1
  19. data/lib/stack_master/commands/status.rb +4 -4
  20. data/lib/stack_master/commands/terminal_helper.rb +3 -3
  21. data/lib/stack_master/commands/tidy.rb +14 -13
  22. data/lib/stack_master/config.rb +23 -21
  23. data/lib/stack_master/diff.rb +2 -2
  24. data/lib/stack_master/identity.rb +2 -1
  25. data/lib/stack_master/parameter_loader.rb +3 -5
  26. data/lib/stack_master/parameter_resolver.rb +18 -18
  27. data/lib/stack_master/parameter_resolvers/acm_certificate.rb +4 -1
  28. data/lib/stack_master/parameter_resolvers/ami_finder.rb +2 -3
  29. data/lib/stack_master/parameter_resolvers/ejson.rb +9 -6
  30. data/lib/stack_master/parameter_resolvers/env.rb +1 -2
  31. data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +1 -1
  32. data/lib/stack_master/parameter_resolvers/latest_container.rb +9 -7
  33. data/lib/stack_master/parameter_resolvers/one_password.rb +11 -7
  34. data/lib/stack_master/parameter_resolvers/parameter_store.rb +1 -5
  35. data/lib/stack_master/parameter_resolvers/security_group.rb +1 -1
  36. data/lib/stack_master/parameter_resolvers/sso_group_id.rb +3 -2
  37. data/lib/stack_master/parameter_resolvers/stack_output.rb +7 -9
  38. data/lib/stack_master/parameter_validator.rb +2 -5
  39. data/lib/stack_master/prompter.rb +11 -10
  40. data/lib/stack_master/resolver_array.rb +2 -3
  41. data/lib/stack_master/role_assumer.rb +7 -5
  42. data/lib/stack_master/security_group_finder.rb +7 -5
  43. data/lib/stack_master/sns_topic_finder.rb +4 -3
  44. data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +2 -3
  45. data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +2 -3
  46. data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +6 -6
  47. data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +0 -2
  48. data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +1 -2
  49. data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +1 -2
  50. data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +2 -3
  51. data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +2 -3
  52. data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +1 -2
  53. data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +0 -1
  54. data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +0 -2
  55. data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +2 -3
  56. data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +7 -9
  57. data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +3 -6
  58. data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +11 -13
  59. data/lib/stack_master/sparkle_formation/template_file.rb +2 -4
  60. data/lib/stack_master/sso_group_id_finder.rb +15 -12
  61. data/lib/stack_master/stack.rb +51 -18
  62. data/lib/stack_master/stack_definition.rb +6 -5
  63. data/lib/stack_master/stack_differ.rb +36 -9
  64. data/lib/stack_master/stack_events/fetcher.rb +3 -1
  65. data/lib/stack_master/stack_events/presenter.rb +6 -1
  66. data/lib/stack_master/stack_events/streamer.rb +3 -1
  67. data/lib/stack_master/stack_status.rb +1 -0
  68. data/lib/stack_master/template_compilers/json.rb +1 -1
  69. data/lib/stack_master/template_compilers/sparkle_formation.rb +12 -9
  70. data/lib/stack_master/template_utils.rb +8 -4
  71. data/lib/stack_master/test_driver/cloud_formation.rb +34 -9
  72. data/lib/stack_master/test_driver/s3.rb +2 -3
  73. data/lib/stack_master/utils.rb +4 -6
  74. data/lib/stack_master/validator.rb +7 -6
  75. data/lib/stack_master/version.rb +1 -1
  76. data/lib/stack_master.rb +3 -1
  77. metadata +21 -10
  78. data/lib/stack_master/parameter_resolvers/accounts_by_tags.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 637daba2dddc707df22584fad412d87df4ee24aab069885231561d847c762496
4
- data.tar.gz: 7abcea21ecfbc080886b4181dc66392066a9360255ed2186bf2e7d652f6101e2
3
+ metadata.gz: ffa37da4e3a6d6d80804a60be1b868180a67470009472b02a0c838354028efb6
4
+ data.tar.gz: e04ddf265d427425f0a1a60973eec74e2e6cc42e3cc47f00b8cbf584f1cdac93
5
5
  SHA512:
6
- metadata.gz: dba94d8ecf220d837673ef95f68c8d534b979bd31d67b8056e0836355a88f713499ca55420906ffb16855f7c3acb17e320c41ec90cb9c84263c59539f00c4cb2
7
- data.tar.gz: 680f31e8fef7a94d387582acdb6c9381844aae4e2c56e6c4088f36d9c1ac2969feb27b9703c90c21416cef96572efc32455d4621951567611a18cb3481f22f56
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
 
@@ -794,11 +794,6 @@ CLI directly rather than via a stack update.
794
794
  applied in CloudFormation. This can happen if the template or computed parameters have changed in code and the change
795
795
  hasn't been applied to this stack.
796
796
 
797
- ## Maintainers
798
-
799
- - [Steve Hodgkiss](https://github.com/stevehodgkiss)
800
- - [Glen Stampoultzis](https://github.com/gstamp)
801
-
802
797
  ## License
803
798
 
804
799
  StackMaster uses the MIT license. See [LICENSE.txt](https://github.com/envato/stack_master/blob/master/LICENSE.txt) for details.
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,37 +8,39 @@ 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
- def_delegators :cf, :create_change_set,
18
- :describe_change_set,
19
- :execute_change_set,
20
- :delete_change_set,
21
- :delete_stack,
22
- :cancel_update_stack,
23
- :describe_stack_resources,
24
- :get_template,
25
- :get_stack_policy,
26
- :set_stack_policy,
27
- :describe_stack_events,
28
- :update_stack,
29
- :create_stack,
30
- :validate_template,
31
- :describe_stacks,
32
- :detect_stack_drift,
33
- :describe_stack_drift_detection_status,
34
- :describe_stack_resource_drifts
17
+ def_delegators(
18
+ :cf,
19
+ :create_change_set,
20
+ :describe_change_set,
21
+ :execute_change_set,
22
+ :delete_change_set,
23
+ :delete_stack,
24
+ :cancel_update_stack,
25
+ :describe_stack_resources,
26
+ :get_template,
27
+ :get_stack_policy,
28
+ :set_stack_policy,
29
+ :describe_stack_events,
30
+ :update_stack,
31
+ :create_stack,
32
+ :validate_template,
33
+ :describe_stacks,
34
+ :detect_stack_drift,
35
+ :describe_stack_drift_detection_status,
36
+ :describe_stack_resource_drifts
37
+ )
35
38
 
36
39
  private
37
40
 
38
41
  def cf
39
42
  @cf ||= Aws::CloudFormation::Client.new({ region: region, retry_limit: 10 })
40
43
  end
41
-
42
44
  end
43
45
  end
44
46
  end
@@ -11,20 +11,24 @@ module StackMaster
11
11
  end
12
12
 
13
13
  def upload_files(bucket: nil, prefix: nil, region: nil, files: {})
14
- raise StackMaster::AwsDriver::S3ConfigurationError, 'A bucket must be specified in order to use S3' unless bucket
14
+ unless bucket
15
+ raise StackMaster::AwsDriver::S3ConfigurationError, 'A bucket must be specified in order to use S3'
16
+ end
15
17
 
16
18
  return if files.empty?
17
19
 
18
20
  s3 = new_s3_client(region: region)
19
21
 
20
- current_objects = s3.list_objects({
21
- prefix: prefix,
22
- bucket: bucket
23
- }).map(&:contents).flatten.inject({}){|h,obj|
22
+ current_objects = s3.list_objects(
23
+ {
24
+ prefix: prefix,
25
+ bucket: bucket
26
+ }
27
+ ).map(&:contents).flatten.inject({}) do |h, obj|
24
28
  h.merge(obj.key => obj)
25
- }
29
+ end
26
30
 
27
- StackMaster.stdout.puts "Uploading files to S3:"
31
+ StackMaster.stdout.puts 'Uploading files to S3:'
28
32
 
29
33
  files.each do |template, file|
30
34
  body = file.fetch(:body)
@@ -32,26 +36,29 @@ module StackMaster
32
36
  object_key = template.dup
33
37
  object_key.prepend("#{prefix}/") if prefix
34
38
  compiled_template_md5 = Digest::MD5.hexdigest(body).to_s
35
- 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
36
40
 
37
41
  next if compiled_template_md5 == s3_md5
42
+
38
43
  s3_uri = "s3://#{bucket}/#{object_key}"
39
44
  StackMaster.stdout.print "- #{File.basename(path)} => #{s3_uri} "
40
45
 
41
- s3.put_object({
42
- bucket: bucket,
43
- key: object_key,
44
- body: body,
45
- metadata: { md5: compiled_template_md5 }
46
- })
47
- StackMaster.stdout.puts "done."
46
+ s3.put_object(
47
+ {
48
+ bucket: bucket,
49
+ key: object_key,
50
+ body: body,
51
+ metadata: { md5: compiled_template_md5 }
52
+ }
53
+ )
54
+ StackMaster.stdout.puts 'done.'
48
55
  end
49
56
  end
50
57
 
51
58
  def url(bucket:, prefix:, region:, template:)
52
59
  if region == 'us-east-1'
53
- ["https://s3.amazonaws.com", bucket, prefix, template].compact.join('/')
54
- elsif region.start_with? "cn-"
60
+ ['https://s3.amazonaws.com', bucket, prefix, template].compact.join('/')
61
+ elsif region.start_with? 'cn-'
55
62
  ["https://s3.#{region}.amazonaws.com.cn", bucket, prefix, template].compact.join('/')
56
63
  else
57
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)
@@ -42,15 +42,15 @@ module StackMaster
42
42
  end
43
43
 
44
44
  def display(io)
45
- io.puts <<-EOL
45
+ io.puts <<~EOL
46
46
 
47
- ========================================
48
- Proposed change set:
49
- EOL
47
+ ========================================
48
+ Proposed change set:
49
+ EOL
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 @@ io.puts "========================================"
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
@@ -5,8 +5,12 @@ module StackMaster
5
5
  class CLI
6
6
  include Commander::Methods
7
7
 
8
- def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
9
- @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
8
+ def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = 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
@@ -41,10 +45,15 @@ module StackMaster
41
45
  command :apply do |c|
42
46
  c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
43
47
  c.summary = 'Creates or updates a stack'
44
- c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters. Tails stack events until CloudFormation has completed."
48
+ c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters. " \
49
+ 'Tails stack events until CloudFormation has completed.'
45
50
  c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc'
46
- c.option '--on-failure ACTION', String, "Action to take on CREATE_FAILURE. Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. Default: ROLLBACK\nNote: You cannot use this option with Serverless Application Model (SAM) templates."
47
- c.option '--yes-param PARAM_NAME', String, "Auto-approve stack updates when only parameter PARAM_NAME changes"
51
+ c.option '--on-failure ACTION', String,
52
+ 'Action to take on CREATE_FAILURE. ' \
53
+ 'Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. ' \
54
+ "Default: ROLLBACK\n" \
55
+ 'Note: You cannot use this option with Serverless Application Model (SAM) templates.'
56
+ c.option '--yes-param PARAM_NAME', String, 'Auto-approve stack updates when only parameter PARAM_NAME changes'
48
57
  c.action do |args, options|
49
58
  options.default config: default_config_file
50
59
  execute_stacks_command(StackMaster::Commands::Apply, args, options)
@@ -54,7 +63,7 @@ module StackMaster
54
63
  command :outputs do |c|
55
64
  c.syntax = 'stack_master outputs [region_or_alias] [stack_name]'
56
65
  c.summary = 'Displays outputs for a stack'
57
- c.description = "Displays outputs for a stack"
66
+ c.description = 'Displays outputs for a stack'
58
67
  c.action do |args, options|
59
68
  options.default config: default_config_file
60
69
  execute_stacks_command(StackMaster::Commands::Outputs, args, options)
@@ -68,10 +77,10 @@ module StackMaster
68
77
  c.option('--overwrite', 'Overwrite existing files')
69
78
  c.action do |args, options|
70
79
  options.default config: default_config_file
71
- unless args.size == 2
72
- say "Invalid arguments. stack_master init [region] [stack_name]"
73
- else
80
+ if args.size == 2
74
81
  StackMaster::Commands::Init.perform(options, *args)
82
+ else
83
+ say 'Invalid arguments. stack_master init [region] [stack_name]'
75
84
  end
76
85
  end
77
86
  end
@@ -89,8 +98,8 @@ module StackMaster
89
98
 
90
99
  command :events do |c|
91
100
  c.syntax = 'stack_master events [region_or_alias] [stack_name]'
92
- c.summary = "Shows events for a stack"
93
- c.description = "Shows events for a stack"
101
+ c.summary = 'Shows events for a stack'
102
+ c.description = 'Shows events for a stack'
94
103
  c.example 'show events for myapp-vpc in us-east-1', 'stack_master events us-east-1 myapp-vpc'
95
104
  c.option '--number Integer', Integer, 'Number of recent events to show'
96
105
  c.option '--all', 'Show all events'
@@ -103,8 +112,8 @@ module StackMaster
103
112
 
104
113
  command :resources do |c|
105
114
  c.syntax = 'stack_master resources [region] [stack_name]'
106
- c.summary = "Shows stack resources"
107
- c.description = "Shows stack resources"
115
+ c.summary = 'Shows stack resources'
116
+ c.description = 'Shows stack resources'
108
117
  c.action do |args, options|
109
118
  options.default config: default_config_file
110
119
  execute_stacks_command(StackMaster::Commands::Resources, args, options)
@@ -117,7 +126,7 @@ module StackMaster
117
126
  c.description = 'List stack definitions'
118
127
  c.action do |args, options|
119
128
  options.default config: default_config_file
120
- say "Invalid arguments." if args.size > 0
129
+ say 'Invalid arguments.' if args.size > 0
121
130
  config = load_config(options.config)
122
131
  StackMaster::Commands::ListStacks.perform(config, nil, options)
123
132
  end
@@ -137,8 +146,8 @@ module StackMaster
137
146
 
138
147
  command :lint do |c|
139
148
  c.syntax = 'stack_master lint [region_or_alias] [stack_name]'
140
- c.summary = "Check the stack definition locally"
141
- 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'
142
151
  c.example 'run cfn-lint on stack myapp-vpc with us-east-1 settings', 'stack_master lint us-east-1 myapp-vpc'
143
152
  c.action do |args, options|
144
153
  options.default config: default_config_file
@@ -149,7 +158,7 @@ module StackMaster
149
158
  command :nag do |c|
150
159
  c.syntax = 'stack_master nag [region_or_alias] [stack_name]'
151
160
  c.summary = "Check this stack's template with cfn_nag"
152
- c.description = "Runs SAST scan cfn_nag on the template"
161
+ c.description = 'Runs SAST scan cfn_nag on the template'
153
162
  c.example 'run cfn_nag on stack myapp-vpc with us-east-1 settings', 'stack_master nag us-east-1 myapp-vpc'
154
163
  c.action do |args, options|
155
164
  options.default config: default_config_file
@@ -159,7 +168,7 @@ module StackMaster
159
168
 
160
169
  command :compile do |c|
161
170
  c.syntax = 'stack_master compile [region_or_alias] [stack_name]'
162
- c.summary = "Print the compiled version of a given stack"
171
+ c.summary = 'Print the compiled version of a given stack'
163
172
  c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS"
164
173
  c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc'
165
174
  c.action do |args, options|
@@ -171,11 +180,13 @@ module StackMaster
171
180
  command :status do |c|
172
181
  c.syntax = 'stack_master status'
173
182
  c.summary = 'Check the current status stacks.'
174
- c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.'
183
+ c.description = 'Checks the status of all stacks defined in the stack_master.yml file. ' \
184
+ 'Warning this operation can be somewhat slow.'
175
185
  c.example 'description', 'Check the status of all stack definitions'
176
186
  c.action do |args, options|
177
187
  options.default config: default_config_file
178
- 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
189
+
179
190
  config = load_config(options.config)
180
191
  StackMaster::Commands::Status.perform(config, nil, options)
181
192
  end
@@ -184,11 +195,13 @@ module StackMaster
184
195
  command :tidy do |c|
185
196
  c.syntax = 'stack_master tidy'
186
197
  c.summary = 'Try to identify extra & missing files.'
187
- c.description = 'Cross references stack_master.yml with the template and parameter directories to identify extra or missing files.'
198
+ c.description = 'Cross references stack_master.yml with the template ' \
199
+ 'and parameter directories to identify extra or missing files.'
188
200
  c.example 'description', 'Check for missing or extra files'
189
201
  c.action do |args, options|
190
202
  options.default config: default_config_file
191
- 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
204
+
192
205
  config = load_config(options.config)
193
206
  StackMaster::Commands::Tidy.perform(config, nil, options)
194
207
  end
@@ -202,7 +215,7 @@ module StackMaster
202
215
  c.action do |args, options|
203
216
  options.default config: default_config_file
204
217
  unless args.size == 2
205
- say "Invalid arguments. stack_master delete [region] [stack_name]"
218
+ say 'Invalid arguments. stack_master delete [region] [stack_name]'
206
219
  return
207
220
  end
208
221
 
@@ -230,7 +243,7 @@ module StackMaster
230
243
  c.syntax = 'stack_master drift [region_or_alias] [stack_name]'
231
244
  c.summary = 'Detects and displays stack drift using the CloudFormation Drift API'
232
245
  c.description = 'Detects and displays stack drift'
233
- 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'
234
247
  c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
235
248
  c.action do |args, options|
236
249
  options.default config: default_config_file, timeout: 120
@@ -244,7 +257,7 @@ module StackMaster
244
257
  private
245
258
 
246
259
  def default_config_file
247
- "stack_master.yml"
260
+ 'stack_master.yml'
248
261
  end
249
262
 
250
263
  def load_config(file)
@@ -268,12 +281,17 @@ module StackMaster
268
281
  show_other_region_candidates(config, stack_name)
269
282
  success = false
270
283
  end
271
- stack_definitions = stack_definitions.select do |stack_definition|
272
- running_in_allowed_account?(stack_definition.allowed_accounts) && StackStatus.new(config, stack_definition).changed?
273
- 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
274
290
  stack_definitions.each do |stack_definition|
275
291
  StackMaster.cloud_formation_driver.set_region(stack_definition.region)
276
- StackMaster.stdout.puts "Executing #{command.command_name} on #{stack_definition.stack_name} in #{stack_definition.region}"
292
+ StackMaster.stdout.puts(
293
+ "Executing #{command.command_name} on #{stack_definition.stack_name} in #{stack_definition.region}"
294
+ )
277
295
  success = execute_if_allowed_account(stack_definition.allowed_accounts) do
278
296
  command.perform(config, stack_definition, options).success?
279
297
  end
@@ -283,20 +301,23 @@ module StackMaster
283
301
  end
284
302
 
285
303
  def show_other_region_candidates(config, stack_name)
286
- candidates = config.filter(region="", stack_name=stack_name)
304
+ candidates = config.filter(region = '', stack_name = stack_name)
287
305
  return if candidates.empty?
288
306
 
289
307
  StackMaster.stdout.puts "Stack name #{stack_name} exists in regions: #{candidates.map(&:region).join(', ')}"
290
308
  end
291
309
 
292
310
  def execute_if_allowed_account(allowed_accounts, &block)
293
- raise ArgumentError, "Block required to execute this method" unless block_given?
311
+ raise ArgumentError, 'Block required to execute this method' unless block_given?
312
+
294
313
  if running_in_allowed_account?(allowed_accounts)
295
314
  block.call
296
315
  else
297
316
  account_text = "'#{identity.account}'"
298
317
  account_text << " (#{identity.account_aliases.join(', ')})" if identity.account_aliases.any?
299
- StackMaster.stdout.puts "Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
318
+ StackMaster.stdout.puts(
319
+ "Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
320
+ )
300
321
  false
301
322
  end
302
323
  end
@@ -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,14 +136,15 @@ 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
146
146
  return unless use_s3?
147
+
147
148
  s3.upload_files(**s3_options)
148
149
  end
149
150
 
@@ -153,7 +154,8 @@ module StackMaster
153
154
 
154
155
  def template_value
155
156
  if use_s3?
156
- s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'], template: @stack_definition.s3_template_file_name)
157
+ s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'],
158
+ template: @stack_definition.s3_template_file_name)
157
159
  else
158
160
  proposed_stack.template
159
161
  end
@@ -161,6 +163,7 @@ module StackMaster
161
163
 
162
164
  def files_to_upload
163
165
  return {} unless use_s3?
166
+
164
167
  @stack_definition.s3_files.tap do |files|
165
168
  files[@stack_definition.s3_template_file_name] = {
166
169
  path: @stack_definition.template_file_path,
@@ -174,7 +177,7 @@ module StackMaster
174
177
  stack_name: stack_name,
175
178
  parameters: proposed_stack.aws_parameters,
176
179
  tags: proposed_stack.aws_tags,
177
- capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'],
180
+ capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND],
178
181
  role_arn: proposed_stack.role_arn,
179
182
  notification_arns: proposed_stack.notification_arns,
180
183
  template_method => template_value
@@ -208,9 +211,9 @@ module StackMaster
208
211
  end
209
212
 
210
213
  def ensure_valid_template_body_size!
211
- if proposed_stack.too_big?(use_s3?)
212
- failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
213
- end
214
+ return unless proposed_stack.too_big?(use_s3?)
215
+
216
+ failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
214
217
  end
215
218
 
216
219
  def set_stack_policy
@@ -218,6 +221,7 @@ module StackMaster
218
221
  proposed_policy = proposed_stack.stack_policy_body
219
222
  # No need to reset a stack policy if it's nil or not changed
220
223
  return if proposed_policy.nil? || proposed_policy == current_policy
224
+
221
225
  StackMaster.stdout.print 'Setting a stack policy...'
222
226
  cf.set_stack_policy(
223
227
  stack_name: stack_name,
@@ -226,6 +230,21 @@ module StackMaster
226
230
  StackMaster.stdout.puts 'done.'
227
231
  end
228
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
+
229
248
  extend Forwardable
230
249
  def_delegators :@stack_definition, :stack_name, :region
231
250
  end
@@ -12,11 +12,10 @@ module StackMaster
12
12
  end
13
13
 
14
14
  def perform
15
-
16
15
  return unless check_exists
17
16
 
18
17
  unless ask?("Really delete stack #{@stack_name} (y/n)? ")
19
- StackMaster.stdout.puts "Stack update aborted"
18
+ StackMaster.stdout.puts 'Stack update aborted'
20
19
  return
21
20
  end
22
21
 
@@ -27,14 +26,14 @@ module StackMaster
27
26
  private
28
27
 
29
28
  def delete_stack
30
- cf.delete_stack({stack_name: @stack_name})
29
+ cf.delete_stack({ stack_name: @stack_name })
31
30
  end
32
31
 
33
32
  def check_exists
34
- cf.describe_stacks({stack_name: @stack_name})
33
+ cf.describe_stacks({ stack_name: @stack_name })
35
34
  true
36
35
  rescue Aws::CloudFormation::Errors::ValidationError
37
- failed("Stack does not exist")
36
+ failed('Stack does not exist')
38
37
  false
39
38
  end
40
39
 
@@ -44,10 +43,10 @@ module StackMaster
44
43
 
45
44
  def tail_stack_events
46
45
  StackEvents::Streamer.stream(@stack_name, @region, io: StackMaster.stdout, from: @from_time)
47
- StackMaster.stdout.puts "Stack deleted"
46
+ StackMaster.stdout.puts 'Stack deleted'
48
47
  rescue Aws::CloudFormation::Errors::ValidationError
49
48
  # Unfortunately the stack as a tendency of going away before we get the final delete event.
50
- StackMaster.stdout.puts "Stack deleted"
49
+ StackMaster.stdout.puts 'Stack deleted'
51
50
  end
52
51
  end
53
52
  end