stack_master 2.2.0 → 2.6.1

Sign up to get free protection for your applications and to get access to all the features.
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: