stack_master 2.2.0 → 2.6.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +22 -0
  3. data/README.md +21 -23
  4. data/bin/stack_master +0 -1
  5. data/lib/stack_master.rb +7 -3
  6. data/lib/stack_master/change_set.rb +2 -2
  7. data/lib/stack_master/cli.rb +22 -19
  8. data/lib/stack_master/command.rb +25 -0
  9. data/lib/stack_master/commands/apply.rb +5 -14
  10. data/lib/stack_master/commands/compile.rb +0 -5
  11. data/lib/stack_master/commands/delete.rb +2 -1
  12. data/lib/stack_master/commands/diff.rb +0 -5
  13. data/lib/stack_master/commands/events.rb +0 -6
  14. data/lib/stack_master/commands/init.rb +5 -9
  15. data/lib/stack_master/commands/lint.rb +0 -5
  16. data/lib/stack_master/commands/list_stacks.rb +0 -4
  17. data/lib/stack_master/commands/outputs.rb +0 -5
  18. data/lib/stack_master/commands/resources.rb +0 -5
  19. data/lib/stack_master/commands/status.rb +3 -3
  20. data/lib/stack_master/commands/tidy.rb +0 -4
  21. data/lib/stack_master/commands/validate.rb +1 -6
  22. data/lib/stack_master/identity.rb +25 -4
  23. data/lib/stack_master/parameter_resolvers/latest_ami.rb +1 -1
  24. data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +1 -1
  25. data/lib/stack_master/parameter_validator.rb +36 -0
  26. data/lib/stack_master/role_assumer.rb +1 -0
  27. data/lib/stack_master/sparkle_formation/template_file.rb +1 -1
  28. data/lib/stack_master/stack.rb +19 -6
  29. data/lib/stack_master/stack_definition.rb +14 -11
  30. data/lib/stack_master/stack_differ.rb +1 -1
  31. data/lib/stack_master/stack_events/presenter.rb +1 -1
  32. data/lib/stack_master/template_compiler.rb +1 -1
  33. data/lib/stack_master/template_utils.rb +9 -3
  34. data/lib/stack_master/validator.rb +25 -8
  35. data/lib/stack_master/version.rb +1 -1
  36. metadata +21 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 365c8c1252350564177ff9a0827024d27a7600f31c7a76503cac20accfca5e12
4
- data.tar.gz: '088c89fe1880f1879279b66b8eb26d59e6ec723ec842c81641f7e20f457aefb7'
3
+ metadata.gz: 4f146a3b9b4ca20e6c3fdf98d3620886663ec113cb81f7d763918aad28d5d42c
4
+ data.tar.gz: c7ebfef287797cff8cbb1ee74bafcbe583da1eb14079ea4b94d2102fbc48ea18
5
5
  SHA512:
6
- metadata.gz: 6488da7b3d7ad2562e276246f76f2ec304c9b83c43d24469a76ccde609da290c648ac417232e42864b163dbb1b3111a06052322a8ba9037626eef7f0feb956ab
7
- data.tar.gz: 0f7006a2c2f37f25e771727be69a5823407163ed9ac6a7970ea95a3a8ce07c25fc4f150c9a6fce46c0e5dae9219516699afd095962fdb08f745770a36eccea78
6
+ metadata.gz: 45ca7571feb3989ba167eec7b7dc27ba3ed003aec6890e6c172c4ced6dc432fb29bcc618481880604783f02138ca644b17d9f329f73dcab81d73585104a768c9
7
+ data.tar.gz: dead78c859b1d6cda3148a045c99d31c116a2849ccb2112d231561ae708e619236e4951754d7cdeecf53e08dd68ce99f2f8e242f255c6a168257c6b7cdaec021
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Steve Hodgkiss
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -53,7 +53,7 @@ Stacks are defined inside a `stack_master.yml` YAML file. When running
53
53
  directory, or that the file is passed in with `--config
54
54
  /path/to/stack_master.yml`. Here's an example configuration file:
55
55
 
56
- ```
56
+ ```yaml
57
57
  region_aliases:
58
58
  production: us-east-1
59
59
  staging: ap-southeast-2
@@ -157,18 +157,16 @@ template_compilers:
157
157
 
158
158
  Parameters are loaded from multiple YAML files, merged from the following lookup paths from bottom to top:
159
159
 
160
- - parameters/[underscored_stack_name].yaml
161
- - parameters/[underscored_stack_name].yml
162
- - parameters/[region]/[underscored_stack_name].yaml
163
- - parameters/[region]/[underscored_stack_name].yml
164
- - parameters/[region_alias]/[underscored_stack_name].yaml
165
- - parameters/[region_alias]/[underscored_stack_name].yml
166
-
167
- **Note:** The file names must be underscored, not hyphenated, even if the stack names are hyphenated.
160
+ - parameters/[stack_name].yaml
161
+ - parameters/[stack_name].yml
162
+ - parameters/[region]/[stack_name].yaml
163
+ - parameters/[region]/[stack_name].yml
164
+ - parameters/[region_alias]/[stack_name].yaml
165
+ - parameters/[region_alias]/[stack_name].yml
168
166
 
169
167
  A simple parameter file could look like this:
170
168
 
171
- ```
169
+ ```yaml
172
170
  key_name: myapp-us-east-1
173
171
  ```
174
172
 
@@ -179,7 +177,7 @@ allows you to use the [Compile Time Parameters](http://www.sparkleformation.io/d
179
177
 
180
178
  A simple example looks like this
181
179
 
182
- ```
180
+ ```yaml
183
181
  vpc_cidr: 10.0.0.0/16
184
182
  compile_time_parameters:
185
183
  subnet_cidrs:
@@ -288,8 +286,8 @@ db_password:
288
286
  An alternative to the secrets store is accessing 1password secrets using the 1password cli (`op`).
289
287
  You declare a 1password lookup with the following parameters in your parameters file:
290
288
 
291
- ```
292
- parameters/database.yml
289
+ ```yaml
290
+ # parameters/database.yml
293
291
  database_password:
294
292
  one_password:
295
293
  title: production database
@@ -479,7 +477,7 @@ name of the original resolver.
479
477
 
480
478
  When creating a new resolver, one can automatically create the array resolver by adding a `array_resolver` statement
481
479
  in the class definition, with an optional class name if different from the default one.
482
- ```
480
+ ```ruby
483
481
  module StackMaster
484
482
  module ParameterResolvers
485
483
  class MyResolver < Resolver
@@ -490,7 +488,7 @@ module StackMaster
490
488
  end
491
489
  ```
492
490
  In that example, using the array resolver would look like:
493
- ```
491
+ ```yaml
494
492
  my_parameter:
495
493
  my_custom_array_resolver:
496
494
  - value1
@@ -500,13 +498,13 @@ my_parameter:
500
498
  Array parameter values can include nested parameter resolvers.
501
499
 
502
500
  For example, given the following parameter definition:
503
- ```
501
+ ```yaml
504
502
  my_parameter:
505
503
  - stack_output: my-stack/output # value resolves to 'value1'
506
504
  - value2
507
505
  ```
508
506
  The parameter value will resolve to:
509
- ```
507
+ ```yaml
510
508
  my_parameter: 'value1,value2'
511
509
  ```
512
510
 
@@ -522,7 +520,7 @@ ROLE=<%= role %>
522
520
 
523
521
  And used like this in SparkleFormation templates:
524
522
 
525
- ```
523
+ ```ruby
526
524
  # templates/app.rb
527
525
  user_data user_data_file!('app.erb', role: :worker)
528
526
  ```
@@ -535,7 +533,7 @@ my_variable=<%= ref!(:foo) %>
535
533
  my_other_variable=<%= account_id! %>
536
534
  ```
537
535
 
538
- ```
536
+ ```ruby
539
537
  # templates/ecs_task.rb
540
538
  container_definitions array!(
541
539
  -> {
@@ -567,7 +565,7 @@ project-root
567
565
 
568
566
  Your env-1/stack_master.yml files can reference common templates by setting:
569
567
 
570
- ```
568
+ ```yaml
571
569
  template_dir: ../../sparkle/templates
572
570
  stack_defaults:
573
571
  compiler_options:
@@ -627,7 +625,7 @@ stacks:
627
625
 
628
626
  ## Allowed accounts
629
627
 
630
- The AWS account the command is executing in can be restricted to a specific list of allowed accounts. This is useful in reducing the possibility of applying non-production changes in a production account. Each stack definition can specify the `allowed_accounts` property with an array of AWS account IDs the stack is allowed to work with.
628
+ The AWS account the command is executing in can be restricted to a specific list of allowed accounts. This is useful in reducing the possibility of applying non-production changes in a production account. Each stack definition can specify the `allowed_accounts` property with an array of AWS account IDs or aliases the stack is allowed to work with.
631
629
 
632
630
  This is an opt-in feature which is enabled by specifying at least one account to allow.
633
631
 
@@ -646,7 +644,7 @@ stacks:
646
644
  template: myapp_db.rb
647
645
  allowed_accounts: # only allow these accounts (overrides the stack defaults)
648
646
  - '1234567890'
649
- - '9876543210'
647
+ - my-account-alias
650
648
  tags:
651
649
  purpose: back-end
652
650
  myapp-web:
@@ -661,7 +659,7 @@ stacks:
661
659
  purpose: back-end
662
660
  ```
663
661
 
664
- In the cases where you want to bypass the account check, there is StackMaster flag `--skip-account-check` that can be used.
662
+ In the cases where you want to bypass the account check, there is the StackMaster flag `--skip-account-check` that can be used.
665
663
 
666
664
  ## Commands
667
665
 
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
3
  require 'stack_master'
5
4
 
6
5
  if ENV['STUB_AWS'] == 'true'
@@ -7,8 +7,11 @@ require 'aws-sdk-ecr'
7
7
  require 'aws-sdk-s3'
8
8
  require 'aws-sdk-sns'
9
9
  require 'aws-sdk-ssm'
10
- require 'colorize'
11
- require 'active_support/core_ext/string'
10
+ require 'aws-sdk-iam'
11
+ require 'rainbow'
12
+ require 'active_support/core_ext/hash/keys'
13
+ require 'active_support/core_ext/object/blank'
14
+ require 'active_support/core_ext/string/inflections'
12
15
  require 'multi_json'
13
16
 
14
17
  MultiJson.use :json_gem
@@ -43,6 +46,7 @@ module StackMaster
43
46
 
44
47
  autoload :StackDiffer, 'stack_master/stack_differ'
45
48
  autoload :Validator, 'stack_master/validator'
49
+ autoload :ParameterValidator, 'stack_master/parameter_validator'
46
50
 
47
51
  require 'stack_master/template_compilers/sparkle_formation'
48
52
  require 'stack_master/template_compilers/json'
@@ -126,7 +130,7 @@ module StackMaster
126
130
 
127
131
  def debug(message)
128
132
  return unless debug?
129
- stderr.puts "[DEBUG] #{message}".colorize(:green)
133
+ stderr.puts Rainbow("[DEBUG] #{message}").color(:green)
130
134
  end
131
135
 
132
136
  def quiet!
@@ -75,7 +75,7 @@ io.puts "========================================"
75
75
  end
76
76
  message = "#{action_name} #{resource_change.resource_type} #{resource_change.logical_resource_id}"
77
77
  color = action_color(action_name)
78
- io.puts message.colorize(color)
78
+ io.puts Rainbow(message).color(color)
79
79
  resource_change.details.each do |detail|
80
80
  display_resource_change_detail(io, action_name, color, detail)
81
81
  end
@@ -92,7 +92,7 @@ io.puts "========================================"
92
92
  triggered_by << "(#{detail.evaluation})"
93
93
  end
94
94
  detail_messages << "Triggered by: #{triggered_by}"
95
- io.puts "- #{detail_messages.join('. ')}. ".colorize(color)
95
+ io.puts Rainbow("- #{detail_messages.join('. ')}. ").color(color)
96
96
  end
97
97
 
98
98
  def action_color(action_name)
@@ -46,7 +46,7 @@ module StackMaster
46
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
47
  c.option '--yes-param PARAM_NAME', String, "Auto-approve stack updates when only parameter PARAM_NAME changes"
48
48
  c.action do |args, options|
49
- options.defaults config: default_config_file
49
+ options.default config: default_config_file
50
50
  execute_stacks_command(StackMaster::Commands::Apply, args, options)
51
51
  end
52
52
  end
@@ -56,7 +56,7 @@ module StackMaster
56
56
  c.summary = 'Displays outputs for a stack'
57
57
  c.description = "Displays outputs for a stack"
58
58
  c.action do |args, options|
59
- options.defaults config: default_config_file
59
+ options.default config: default_config_file
60
60
  execute_stacks_command(StackMaster::Commands::Outputs, args, options)
61
61
  end
62
62
  end
@@ -67,11 +67,11 @@ module StackMaster
67
67
  c.description = 'Initialises the expected directory structure and stack_master.yml file'
68
68
  c.option('--overwrite', 'Overwrite existing files')
69
69
  c.action do |args, options|
70
- options.defaults config: default_config_file
70
+ options.default config: default_config_file
71
71
  unless args.size == 2
72
72
  say "Invalid arguments. stack_master init [region] [stack_name]"
73
73
  else
74
- StackMaster::Commands::Init.perform(options.overwrite, *args)
74
+ StackMaster::Commands::Init.perform(options, *args)
75
75
  end
76
76
  end
77
77
  end
@@ -82,7 +82,7 @@ module StackMaster
82
82
  c.description = "Shows a diff of the proposed stack's template and parameters"
83
83
  c.example 'diff a stack named myapp-vpc in us-east-1', 'stack_master diff us-east-1 myapp-vpc'
84
84
  c.action do |args, options|
85
- options.defaults config: default_config_file
85
+ options.default config: default_config_file
86
86
  execute_stacks_command(StackMaster::Commands::Diff, args, options)
87
87
  end
88
88
  end
@@ -96,7 +96,7 @@ module StackMaster
96
96
  c.option '--all', 'Show all events'
97
97
  c.option '--tail', 'Tail events'
98
98
  c.action do |args, options|
99
- options.defaults config: default_config_file
99
+ options.default config: default_config_file
100
100
  execute_stacks_command(StackMaster::Commands::Events, args, options)
101
101
  end
102
102
  end
@@ -106,7 +106,7 @@ module StackMaster
106
106
  c.summary = "Shows stack resources"
107
107
  c.description = "Shows stack resources"
108
108
  c.action do |args, options|
109
- options.defaults config: default_config_file
109
+ options.default config: default_config_file
110
110
  execute_stacks_command(StackMaster::Commands::Resources, args, options)
111
111
  end
112
112
  end
@@ -116,10 +116,10 @@ module StackMaster
116
116
  c.summary = 'List stack definitions'
117
117
  c.description = 'List stack definitions'
118
118
  c.action do |args, options|
119
- options.defaults config: default_config_file
119
+ options.default config: default_config_file
120
120
  say "Invalid arguments." if args.size > 0
121
121
  config = load_config(options.config)
122
- StackMaster::Commands::ListStacks.perform(config)
122
+ StackMaster::Commands::ListStacks.perform(config, nil, options)
123
123
  end
124
124
  end
125
125
 
@@ -128,8 +128,9 @@ module StackMaster
128
128
  c.summary = 'Validate a template'
129
129
  c.description = 'Validate a template'
130
130
  c.example 'validate a stack named myapp-vpc in us-east-1', 'stack_master validate us-east-1 myapp-vpc'
131
+ c.option '--[no-]validate-template-parameters', 'Validate template parameters. Default: validate'
131
132
  c.action do |args, options|
132
- options.defaults config: default_config_file
133
+ options.default config: default_config_file, validate_template_parameters: true
133
134
  execute_stacks_command(StackMaster::Commands::Validate, args, options)
134
135
  end
135
136
  end
@@ -140,7 +141,7 @@ module StackMaster
140
141
  c.description = "Runs cfn-lint on the template which would be sent to AWS on apply"
141
142
  c.example 'run cfn-lint on stack myapp-vpc with us-east-1 settings', 'stack_master lint us-east-1 myapp-vpc'
142
143
  c.action do |args, options|
143
- options.defaults config: default_config_file
144
+ options.default config: default_config_file
144
145
  execute_stacks_command(StackMaster::Commands::Lint, args, options)
145
146
  end
146
147
  end
@@ -151,7 +152,7 @@ module StackMaster
151
152
  c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS"
152
153
  c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc'
153
154
  c.action do |args, options|
154
- options.defaults config: default_config_file
155
+ options.default config: default_config_file
155
156
  execute_stacks_command(StackMaster::Commands::Compile, args, options)
156
157
  end
157
158
  end
@@ -162,10 +163,10 @@ module StackMaster
162
163
  c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.'
163
164
  c.example 'description', 'Check the status of all stack definitions'
164
165
  c.action do |args, options|
165
- options.defaults config: default_config_file
166
+ options.default config: default_config_file
166
167
  say "Invalid arguments. stack_master status" and return unless args.size == 0
167
168
  config = load_config(options.config)
168
- StackMaster::Commands::Status.perform(config)
169
+ StackMaster::Commands::Status.perform(config, nil, options)
169
170
  end
170
171
  end
171
172
 
@@ -175,10 +176,10 @@ module StackMaster
175
176
  c.description = 'Cross references stack_master.yml with the template and parameter directories to identify extra or missing files.'
176
177
  c.example 'description', 'Check for missing or extra files'
177
178
  c.action do |args, options|
178
- options.defaults config: default_config_file
179
+ options.default config: default_config_file
179
180
  say "Invalid arguments. stack_master tidy" and return unless args.size == 0
180
181
  config = load_config(options.config)
181
- StackMaster::Commands::Tidy.perform(config)
182
+ StackMaster::Commands::Tidy.perform(config, nil, options)
182
183
  end
183
184
  end
184
185
 
@@ -208,7 +209,7 @@ module StackMaster
208
209
 
209
210
  success = execute_if_allowed_account(allowed_accounts) do
210
211
  StackMaster.cloud_formation_driver.set_region(region)
211
- StackMaster::Commands::Delete.perform(region, stack_name).success?
212
+ StackMaster::Commands::Delete.perform(region, stack_name, options).success?
212
213
  end
213
214
  @kernel.exit false unless success
214
215
  end
@@ -262,13 +263,15 @@ module StackMaster
262
263
  if running_in_allowed_account?(allowed_accounts)
263
264
  block.call
264
265
  else
265
- StackMaster.stdout.puts "Account '#{identity.account}' is not an allowed account. Allowed accounts are #{allowed_accounts}."
266
+ account_text = "'#{identity.account}'"
267
+ account_text << " (#{identity.account_aliases.join(', ')})" if identity.account_aliases.any?
268
+ StackMaster.stdout.puts "Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
266
269
  false
267
270
  end
268
271
  end
269
272
 
270
273
  def running_in_allowed_account?(allowed_accounts)
271
- StackMaster.skip_account_check? || identity.running_in_allowed_account?(allowed_accounts)
274
+ StackMaster.skip_account_check? || identity.running_in_account?(allowed_accounts)
272
275
  end
273
276
 
274
277
  def identity
@@ -27,6 +27,12 @@ module StackMaster
27
27
  end
28
28
  end
29
29
 
30
+ def initialize(config, stack_definition = nil, options = Commander::Command::Options.new)
31
+ @config = config
32
+ @stack_definition = stack_definition
33
+ @options = options
34
+ end
35
+
30
36
  def success?
31
37
  @failed != true
32
38
  end
@@ -36,9 +42,24 @@ module StackMaster
36
42
  def error_message(e)
37
43
  msg = "#{e.class} #{e.message}"
38
44
  msg << "\n Caused by: #{e.cause.class} #{e.cause.message}" if e.cause
45
+ if options.trace
46
+ msg << "\n#{backtrace(e)}"
47
+ else
48
+ msg << "\n Use --trace to view backtrace"
49
+ end
39
50
  msg
40
51
  end
41
52
 
53
+ def backtrace(error)
54
+ if error.respond_to?(:full_message)
55
+ error.full_message
56
+ else
57
+ # full_message was introduced in Ruby 2.5
58
+ # remove this conditional when StackMaster no longer supports Ruby 2.4
59
+ error.backtrace.join("\n")
60
+ end
61
+ end
62
+
42
63
  def failed(message = nil)
43
64
  StackMaster.stderr.puts(message) if message
44
65
  @failed = true
@@ -53,5 +74,9 @@ module StackMaster
53
74
  StackMaster.stdout.puts(message) if message
54
75
  throw :halt
55
76
  end
77
+
78
+ def options
79
+ @options ||= Commander::Command::Options.new
80
+ end
56
81
  end
57
82
  end
@@ -6,14 +6,10 @@ module StackMaster
6
6
  include StackMaster::Prompter
7
7
  TEMPLATE_TOO_LARGE_ERROR_MESSAGE = 'The (space compressed) stack is larger than the limit set by AWS. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html'.freeze
8
8
 
9
- def initialize(config, stack_definition, options = Commander::Command::Options.new)
10
- @config = config
11
- @s3_config = stack_definition.s3
12
- @stack_definition = stack_definition
9
+ def initialize(*_args)
10
+ super
11
+ @s3_config = @stack_definition.s3
13
12
  @from_time = Time.now
14
- @options = options
15
- @options.on_failure ||= nil
16
- @options.yes_param ||= nil
17
13
  end
18
14
 
19
15
  def perform
@@ -207,13 +203,8 @@ module StackMaster
207
203
  end
208
204
 
209
205
  def ensure_valid_parameters!
210
- if @proposed_stack.missing_parameters?
211
- StackMaster.stderr.puts "Empty/blank parameters detected, ensure values exist for those parameters. Parameters will be read from the following locations:"
212
- @stack_definition.parameter_files.each do |parameter_file|
213
- StackMaster.stderr.puts " - #{parameter_file}"
214
- end
215
- halt!
216
- end
206
+ pv = ParameterValidator.new(stack: @proposed_stack, stack_definition: @stack_definition)
207
+ failed!(pv.error_message) if pv.missing_parameters?
217
208
  end
218
209
 
219
210
  def ensure_valid_template_body_size!
@@ -4,11 +4,6 @@ module StackMaster
4
4
  include Command
5
5
  include Commander::UI
6
6
 
7
- def initialize(config, stack_definition, options = {})
8
- @config = config
9
- @stack_definition = stack_definition
10
- end
11
-
12
7
  def perform
13
8
  puts(proposed_stack.template_body)
14
9
  end
@@ -4,7 +4,8 @@ module StackMaster
4
4
  include Command
5
5
  include StackMaster::Prompter
6
6
 
7
- def initialize(region, stack_name)
7
+ def initialize(region, stack_name, options)
8
+ super(nil, nil, options)
8
9
  @region = region
9
10
  @stack_name = stack_name
10
11
  @from_time = Time.now
@@ -4,11 +4,6 @@ module StackMaster
4
4
  include Command
5
5
  include Commander::UI
6
6
 
7
- def initialize(config, stack_definition, options = {})
8
- @config = config
9
- @stack_definition = stack_definition
10
- end
11
-
12
7
  def perform
13
8
  StackMaster::StackDiffer.new(proposed_stack, stack).output_diff
14
9
  end
@@ -4,12 +4,6 @@ module StackMaster
4
4
  include Command
5
5
  include Commander::UI
6
6
 
7
- def initialize(config, stack_definition, options = {})
8
- @config = config
9
- @stack_definition = stack_definition
10
- @options = options
11
- end
12
-
13
7
  def perform
14
8
  events = StackEvents::Fetcher.fetch(@stack_definition.stack_name, @stack_definition.region)
15
9
  filter_events(events).each do |event|
@@ -5,8 +5,8 @@ module StackMaster
5
5
  class Init
6
6
  include Command
7
7
 
8
- def initialize(overwrite, region, stack_name)
9
- @overwrite = overwrite
8
+ def initialize(options, region, stack_name)
9
+ super(nil, nil, options)
10
10
  @region = region
11
11
  @stack_name = stack_name
12
12
  end
@@ -24,10 +24,10 @@ module StackMaster
24
24
  def check_files
25
25
  @stack_master_filename = "stack_master.yml"
26
26
  @stack_json_filename = "templates/#{@stack_name}.json"
27
- @parameters_filename = File.join("parameters", "#{underscored_stack_name}.yml")
28
- @region_parameters_filename = File.join("parameters", @region, "#{underscored_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 !@overwrite
30
+ if !@options.overwrite
31
31
  [@stack_master_filename, @stack_json_filename, @parameters_filename, @region_parameters_filename].each do |filename|
32
32
  if File.exists?(filename)
33
33
  StackMaster.stderr.puts("Aborting: #{filename} already exists. Use --overwrite to force overwriting file.")
@@ -89,10 +89,6 @@ module StackMaster
89
89
  File.join(StackMaster.base_dir, "stacktemplates", "parameter_region.yml")
90
90
  end
91
91
 
92
- def underscored_stack_name
93
- @stack_name.gsub('-', '_')
94
- end
95
-
96
92
  def render(renderer)
97
93
  binding = InitBinding.new(region: @region, stack_name: @stack_name).get_binding
98
94
  renderer.result(binding)
@@ -6,11 +6,6 @@ module StackMaster
6
6
  include Command
7
7
  include Commander::UI
8
8
 
9
- def initialize(config, stack_definition, options = {})
10
- @config = config
11
- @stack_definition = stack_definition
12
- end
13
-
14
9
  def perform
15
10
  unless cfn_lint_available
16
11
  failed! 'Failed to run cfn-lint. You may need to install it using'\
@@ -7,10 +7,6 @@ module StackMaster
7
7
  include Commander::UI
8
8
  include StackMaster::Commands::TerminalHelper
9
9
 
10
- def initialize(config)
11
- @config = config
12
- end
13
-
14
10
  def perform
15
11
  tp.set :max_width, self.window_size
16
12
  tp @config.stacks, :region, :stack_name
@@ -7,11 +7,6 @@ module StackMaster
7
7
  include Commander::UI
8
8
  include StackMaster::Commands::TerminalHelper
9
9
 
10
- def initialize(config, stack_definition, options = {})
11
- @config = config
12
- @stack_definition = stack_definition
13
- end
14
-
15
10
  def perform
16
11
  if stack
17
12
  tp.set :max_width, self.window_size
@@ -6,11 +6,6 @@ module StackMaster
6
6
  include Command
7
7
  include Commander::UI
8
8
 
9
- def initialize(config, stack_definition, options = {})
10
- @config = config
11
- @stack_definition = stack_definition
12
- end
13
-
14
9
  def perform
15
10
  if stack_resources
16
11
  tp stack_resources, :logical_resource_id, :resource_type, :timestamp, :resource_status, :resource_status_reason, :description
@@ -7,8 +7,8 @@ module StackMaster
7
7
  include Command
8
8
  include StackMaster::Commands::TerminalHelper
9
9
 
10
- def initialize(config, show_progress = true)
11
- @config = config
10
+ def initialize(config, options, show_progress = true)
11
+ super(config, nil, options)
12
12
  @show_progress = show_progress
13
13
  end
14
14
 
@@ -44,7 +44,7 @@ module StackMaster
44
44
  end
45
45
 
46
46
  def running_in_allowed_account?(allowed_accounts)
47
- StackMaster.skip_account_check? || identity.running_in_allowed_account?(allowed_accounts)
47
+ StackMaster.skip_account_check? || identity.running_in_account?(allowed_accounts)
48
48
  end
49
49
 
50
50
  def identity
@@ -4,10 +4,6 @@ module StackMaster
4
4
  include Command
5
5
  include StackMaster::Commands::TerminalHelper
6
6
 
7
- def initialize(config)
8
- @config = config
9
- end
10
-
11
7
  def perform
12
8
  used_templates = []
13
9
  used_parameter_files = []
@@ -4,13 +4,8 @@ module StackMaster
4
4
  include Command
5
5
  include Commander::UI
6
6
 
7
- def initialize(config, stack_definition, options = {})
8
- @config = config
9
- @stack_definition = stack_definition
10
- end
11
-
12
7
  def perform
13
- failed unless Validator.valid?(@stack_definition, @config)
8
+ failed unless Validator.valid?(@stack_definition, @config, @options)
14
9
  end
15
10
  end
16
11
  end
@@ -1,16 +1,25 @@
1
1
  module StackMaster
2
2
  class Identity
3
- def running_in_allowed_account?(allowed_accounts)
4
- allowed_accounts.nil? || allowed_accounts.empty? || allowed_accounts.include?(account)
3
+ MissingIamPermissionsError = Class.new(StandardError)
4
+
5
+ def running_in_account?(accounts)
6
+ accounts.nil? ||
7
+ accounts.empty? ||
8
+ contains_account_id?(accounts) ||
9
+ contains_account_alias?(accounts)
5
10
  end
6
11
 
7
12
  def account
8
13
  @account ||= sts.get_caller_identity.account
9
14
  end
10
15
 
11
- private
16
+ def account_aliases
17
+ @aliases ||= iam.list_account_aliases.account_aliases
18
+ rescue Aws::IAM::Errors::AccessDenied
19
+ raise MissingIamPermissionsError, 'Failed to retrieve account aliases. Missing required IAM permission: iam:ListAccountAliases'
20
+ end
12
21
 
13
- attr_reader :sts
22
+ private
14
23
 
15
24
  def region
16
25
  @region ||= ENV['AWS_REGION'] || Aws.config[:region] || Aws.shared_config.region || 'us-east-1'
@@ -19,5 +28,17 @@ module StackMaster
19
28
  def sts
20
29
  @sts ||= Aws::STS::Client.new(region: region)
21
30
  end
31
+
32
+ def iam
33
+ @iam ||= Aws::IAM::Client.new(region: region)
34
+ end
35
+
36
+ def contains_account_id?(ids)
37
+ ids.include?(account)
38
+ end
39
+
40
+ def contains_account_alias?(aliases)
41
+ account_aliases.any? { |account_alias| aliases.include?(account_alias) }
42
+ end
22
43
  end
23
44
  end
@@ -12,7 +12,7 @@ module StackMaster
12
12
  owners = Array(value.fetch('owners', 'self').to_s)
13
13
  ami_finder = AmiFinder.new(@stack_definition.region)
14
14
  filters = ami_finder.build_filters_from_hash(value.fetch('filters'))
15
- ami_finder.find_latest_ami(filters, owners).try(:image_id)
15
+ ami_finder.find_latest_ami(filters, owners)&.image_id
16
16
  end
17
17
  end
18
18
  end
@@ -11,7 +11,7 @@ module StackMaster
11
11
 
12
12
  def resolve(value)
13
13
  filters = @ami_finder.build_filters_from_string(value, prefix = "tag")
14
- @ami_finder.find_latest_ami(filters).try(:image_id)
14
+ @ami_finder.find_latest_ami(filters)&.image_id
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,36 @@
1
+ require 'pathname'
2
+
3
+ module StackMaster
4
+ class ParameterValidator
5
+ def initialize(stack:, stack_definition:)
6
+ @stack = stack
7
+ @stack_definition = stack_definition
8
+ end
9
+
10
+ def error_message
11
+ return nil unless missing_parameters?
12
+ message = "Empty/blank parameters detected. Please provide values for these parameters:"
13
+ missing_parameters.each do |parameter_name|
14
+ message << "\n - #{parameter_name}"
15
+ end
16
+ message << "\nParameters will be read from files matching the following globs:"
17
+ base_dir = Pathname.new(@stack_definition.base_dir)
18
+ @stack_definition.parameter_file_globs.each do |glob|
19
+ parameter_file = Pathname.new(glob).relative_path_from(base_dir)
20
+ message << "\n - #{parameter_file}"
21
+ end
22
+ message
23
+ end
24
+
25
+ def missing_parameters?
26
+ missing_parameters.any?
27
+ end
28
+
29
+ private
30
+
31
+ def missing_parameters
32
+ @missing_parameters ||=
33
+ @stack.parameters_with_defaults.select { |_key, value| value.nil? }.keys
34
+ end
35
+ end
36
+ end
@@ -45,6 +45,7 @@ module StackMaster
45
45
  credentials_key = "#{account}:#{role}"
46
46
  @credentials.fetch(credentials_key) do
47
47
  @credentials[credentials_key] = Aws::AssumeRoleCredentials.new(
48
+ region: StackMaster.cloud_formation_driver.region,
48
49
  role_arn: "arn:aws:iam::#{account}:role/#{role}",
49
50
  role_session_name: "stack-master-role-assumer"
50
51
  )
@@ -71,7 +71,7 @@ module StackMaster
71
71
  newlines = lines.split("\n").map do |line|
72
72
  "#{line}#{newlines.pop}"
73
73
  end
74
- if lines.starts_with?("\n")
74
+ if lines.start_with?("\n")
75
75
  newlines.insert(0, "\n")
76
76
  end
77
77
  newlines
@@ -27,12 +27,6 @@ module StackMaster
27
27
  template_default_parameters.merge(parameters)
28
28
  end
29
29
 
30
- def missing_parameters?
31
- parameters_with_defaults.any? do |key, value|
32
- value == nil
33
- end
34
- end
35
-
36
30
  def self.find(region, stack_name)
37
31
  cf = StackMaster.cloud_formation_driver
38
32
  cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
@@ -81,6 +75,25 @@ module StackMaster
81
75
  stack_policy_body: stack_policy_body)
82
76
  end
83
77
 
78
+ def self.generate_without_parameters(stack_definition, config)
79
+ parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
80
+ compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
81
+ template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
82
+ template_format = TemplateUtils.identify_template_format(template_body)
83
+ stack_policy_body = if stack_definition.stack_policy_file_path
84
+ File.read(stack_definition.stack_policy_file_path)
85
+ end
86
+ new(region: stack_definition.region,
87
+ stack_name: stack_definition.stack_name,
88
+ tags: stack_definition.tags,
89
+ parameters: {},
90
+ template_body: template_body,
91
+ template_format: template_format,
92
+ role_arn: stack_definition.role_arn,
93
+ notification_arns: stack_definition.notification_arns,
94
+ stack_policy_body: stack_policy_body)
95
+ end
96
+
84
97
  def max_template_size(use_s3)
85
98
  return TemplateUtils::MAX_S3_TEMPLATE_SIZE if use_s3
86
99
  TemplateUtils::MAX_TEMPLATE_SIZE
@@ -23,7 +23,6 @@ module StackMaster
23
23
  include Utils::Initializable
24
24
 
25
25
  def initialize(attributes = {})
26
- @additional_parameter_lookup_dirs = []
27
26
  @compiler_options = {}
28
27
  @notification_arns = []
29
28
  @s3 = {}
@@ -32,6 +31,7 @@ module StackMaster
32
31
  @ejson_file_kms = true
33
32
  @compiler = nil
34
33
  super
34
+ @additional_parameter_lookup_dirs ||= []
35
35
  @template_dir ||= File.join(@base_dir, 'templates')
36
36
  @allowed_accounts = Array(@allowed_accounts)
37
37
  end
@@ -86,7 +86,11 @@ module StackMaster
86
86
  end
87
87
 
88
88
  def parameter_files
89
- [ default_parameter_file_path, region_parameter_file_path, additional_parameter_lookup_file_paths ].flatten.compact
89
+ parameter_file_globs.map(&Dir.method(:glob)).flatten
90
+ end
91
+
92
+ def parameter_file_globs
93
+ [ default_parameter_glob, region_parameter_glob ] + additional_parameter_lookup_globs
90
94
  end
91
95
 
92
96
  def stack_policy_file_path
@@ -99,23 +103,22 @@ module StackMaster
99
103
 
100
104
  private
101
105
 
102
- def additional_parameter_lookup_file_paths
103
- return unless additional_parameter_lookup_dirs
106
+ def additional_parameter_lookup_globs
104
107
  additional_parameter_lookup_dirs.map do |a|
105
- Dir.glob(File.join(base_dir, 'parameters', a, "#{underscored_stack_name}.y*ml"))
108
+ File.join(base_dir, 'parameters', a, "#{stack_name_glob}.y*ml")
106
109
  end
107
110
  end
108
111
 
109
- def region_parameter_file_path
110
- Dir.glob(File.join(base_dir, 'parameters', "#{region}", "#{underscored_stack_name}.y*ml"))
112
+ def region_parameter_glob
113
+ File.join(base_dir, 'parameters', "#{region}", "#{stack_name_glob}.y*ml")
111
114
  end
112
115
 
113
- def default_parameter_file_path
114
- Dir.glob(File.join(base_dir, 'parameters', "#{underscored_stack_name}.y*ml"))
116
+ def default_parameter_glob
117
+ File.join(base_dir, 'parameters', "#{stack_name_glob}.y*ml")
115
118
  end
116
119
 
117
- def underscored_stack_name
118
- stack_name.gsub('-', '_')
120
+ def stack_name_glob
121
+ stack_name.gsub('-', '[-_]')
119
122
  end
120
123
  end
121
124
  end
@@ -107,7 +107,7 @@ module StackMaster
107
107
 
108
108
  def colorize(text, color)
109
109
  if colorize?
110
- text.colorize(color)
110
+ Rainbow(text).color(color)
111
111
  else
112
112
  text
113
113
  end
@@ -10,7 +10,7 @@ module StackMaster
10
10
  end
11
11
 
12
12
  def print_event(event)
13
- @io.puts "#{event.timestamp.localtime} #{event.logical_resource_id} #{event.resource_type} #{event.resource_status} #{event.resource_status_reason}".colorize(event_colour(event))
13
+ @io.puts Rainbow("#{event.timestamp.localtime} #{event.logical_resource_id} #{event.resource_type} #{event.resource_status} #{event.resource_status_reason}").color(event_colour(event))
14
14
  end
15
15
 
16
16
  def event_colour(event)
@@ -11,7 +11,7 @@ module StackMaster
11
11
  compiler.require_dependencies
12
12
  compiler.compile(template_dir, template, compile_time_parameters, compiler_options)
13
13
  rescue StandardError => e
14
- raise TemplateCompilationFailed.new("Failed to compile #{template} with error #{e}.\n#{e.backtrace}")
14
+ raise TemplateCompilationFailed, "Failed to compile #{template}"
15
15
  end
16
16
 
17
17
  def self.register(name, klass)
@@ -2,12 +2,18 @@ module StackMaster
2
2
  module TemplateUtils
3
3
  MAX_TEMPLATE_SIZE = 51200
4
4
  MAX_S3_TEMPLATE_SIZE = 460800
5
+ # Matches if the first non-whitespace character is a '{', handling cases
6
+ # with leading whitespace and extra (whitespace-only) lines.
7
+ JSON_IDENTIFICATION_PATTERN = Regexp.new('\A\s*{', Regexp::MULTILINE)
5
8
 
6
9
  extend self
7
10
 
8
11
  def identify_template_format(template_body)
9
- return :json if template_body =~ /^{/x # ignore leading whitespaces
10
- :yaml
12
+ if template_body =~ JSON_IDENTIFICATION_PATTERN
13
+ :json
14
+ else
15
+ :yaml
16
+ end
11
17
  end
12
18
 
13
19
  def template_hash(template_body=nil)
@@ -28,4 +34,4 @@ module StackMaster
28
34
  JSON.dump(template_hash(template_body))
29
35
  end
30
36
  end
31
- end
37
+ end
@@ -1,21 +1,22 @@
1
1
  module StackMaster
2
2
  class Validator
3
- def self.valid?(stack_definition, config)
4
- new(stack_definition, config).perform
3
+ def self.valid?(stack_definition, config, options)
4
+ new(stack_definition, config, options).perform
5
5
  end
6
6
 
7
- def initialize(stack_definition, config)
7
+ def initialize(stack_definition, config, options)
8
8
  @stack_definition = stack_definition
9
9
  @config = config
10
+ @options = options
10
11
  end
11
12
 
12
13
  def perform
13
- parameter_hash = ParameterLoader.load(@stack_definition.parameter_files)
14
- compile_time_parameters = ParameterResolver.resolve(@config, @stack_definition, parameter_hash[:compile_time_parameters])
15
-
16
14
  StackMaster.stdout.print "#{@stack_definition.stack_name}: "
17
- template_body = TemplateCompiler.compile(@config, @stack_definition.compiler, @stack_definition.template_dir, @stack_definition.template, compile_time_parameters, @stack_definition.compiler_options)
18
- cf.validate_template(template_body: TemplateUtils.maybe_compressed_template_body(template_body))
15
+ if validate_template_parameters? && parameter_validator.missing_parameters?
16
+ StackMaster.stdout.puts "invalid\n#{parameter_validator.error_message}"
17
+ return false
18
+ end
19
+ cf.validate_template(template_body: TemplateUtils.maybe_compressed_template_body(stack.template_body))
19
20
  StackMaster.stdout.puts "valid"
20
21
  true
21
22
  rescue Aws::CloudFormation::Errors::ValidationError => e
@@ -25,8 +26,24 @@ module StackMaster
25
26
 
26
27
  private
27
28
 
29
+ def validate_template_parameters?
30
+ @options.validate_template_parameters
31
+ end
32
+
28
33
  def cf
29
34
  @cf ||= StackMaster.cloud_formation_driver
30
35
  end
36
+
37
+ def stack
38
+ @stack ||= if validate_template_parameters?
39
+ Stack.generate(@stack_definition, @config)
40
+ else
41
+ Stack.generate_without_parameters(@stack_definition, @config)
42
+ end
43
+ end
44
+
45
+ def parameter_validator
46
+ @parameter_validator ||= ParameterValidator.new(stack: stack, stack_definition: @stack_definition)
47
+ end
31
48
  end
32
49
  end
@@ -1,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "2.2.0"
2
+ VERSION = "2.6.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stack_master
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Hodgkiss
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-03-13 00:00:00.000000000 Z
12
+ date: 2020-05-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -255,6 +255,20 @@ dependencies:
255
255
  - - "~>"
256
256
  - !ruby/object:Gem::Version
257
257
  version: '1'
258
+ - !ruby/object:Gem::Dependency
259
+ name: aws-sdk-iam
260
+ requirement: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - "~>"
263
+ - !ruby/object:Gem::Version
264
+ version: '1'
265
+ type: :runtime
266
+ prerelease: false
267
+ version_requirements: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - "~>"
270
+ - !ruby/object:Gem::Version
271
+ version: '1'
258
272
  - !ruby/object:Gem::Dependency
259
273
  name: diffy
260
274
  requirement: !ruby/object:Gem::Requirement
@@ -284,7 +298,7 @@ dependencies:
284
298
  - !ruby/object:Gem::Version
285
299
  version: '0'
286
300
  - !ruby/object:Gem::Dependency
287
- name: colorize
301
+ name: rainbow
288
302
  requirement: !ruby/object:Gem::Requirement
289
303
  requirements:
290
304
  - - ">="
@@ -432,6 +446,7 @@ executables:
432
446
  extensions: []
433
447
  extra_rdoc_files: []
434
448
  files:
449
+ - LICENSE.txt
435
450
  - README.md
436
451
  - bin/stack_master
437
452
  - lib/stack_master.rb
@@ -472,6 +487,7 @@ files:
472
487
  - lib/stack_master/parameter_resolvers/security_group.rb
473
488
  - lib/stack_master/parameter_resolvers/sns_topic_name.rb
474
489
  - lib/stack_master/parameter_resolvers/stack_output.rb
490
+ - lib/stack_master/parameter_validator.rb
475
491
  - lib/stack_master/prompter.rb
476
492
  - lib/stack_master/resolver_array.rb
477
493
  - lib/stack_master/role_assumer.rb
@@ -523,8 +539,8 @@ licenses:
523
539
  metadata:
524
540
  bug_tracker_uri: https://github.com/envato/stack_master/issues
525
541
  changelog_uri: https://github.com/envato/stack_master/blob/master/CHANGELOG.md
526
- documentation_uri: https://www.rubydoc.info/gems/stack_master/2.2.0
527
- source_code_uri: https://github.com/envato/stack_master/tree/v2.2.0
542
+ documentation_uri: https://www.rubydoc.info/gems/stack_master/2.6.1
543
+ source_code_uri: https://github.com/envato/stack_master/tree/v2.6.1
528
544
  post_install_message:
529
545
  rdoc_options: []
530
546
  require_paths: