stack_master 2.3.0 → 2.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b2fd9ecb9882d2a0b00adee84d08975f5ae51b72297a449c5bd5218efbf8a17
4
- data.tar.gz: c2eb23ba0ae38e6db17cc296a4a151aaaf7785bbff222d75ec671251ac398f1f
3
+ metadata.gz: f94d2bab69225428425d5e3bc426ca572c781aa5d9e069420c198c86640f84c5
4
+ data.tar.gz: f4f164e4390d568e9a7511013a33796ad04862d18510ec5683c2024b9952781f
5
5
  SHA512:
6
- metadata.gz: e5832578334d932b9750b9aa2914c6c0fc9d2b391606c3ab8fc60868b151c5b9fa996d2b89636d14515aff3b9892c8a913c1e0531ac57262a67072931a2a8f1c
7
- data.tar.gz: 87dd2728b566b6d775a0b8cb7137acc856f1ee854436e35789578c1c129798594375bc9ad06018be2285605878918cf89d872c3df72fe20a3260380407e32b25
6
+ metadata.gz: 81122997265f0aa25d24f54ee3ccbb5a0a3689b362b37445bd0c12435dc0fcbb24e0d8ce277c949c1e3af5c01d3b5ff938227f6b6f051b97cc8da66f400519d6
7
+ data.tar.gz: b5f8114a90fc7cc402e07e34676b7142b3849c4e8eecdc43cb67651358e5e7e6228ab04b68ef5b769b98ff91e7efd2ec9667a5c197699bccbfb96d49848585ed
@@ -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
@@ -123,6 +123,7 @@ stack_defaults:
123
123
  ```
124
124
 
125
125
  Additional files can be configured to be uploaded to S3 alongside the templates:
126
+
126
127
  ```yaml
127
128
  stacks:
128
129
  production:
@@ -131,6 +132,7 @@ stacks:
131
132
  files:
132
133
  - userdata.sh
133
134
  ```
135
+
134
136
  ## Directories
135
137
 
136
138
  - `templates` - CloudFormation, SparkleFormation or CfnDsl templates.
@@ -155,7 +157,8 @@ template_compilers:
155
157
 
156
158
  ## Parameters
157
159
 
158
- Parameters are loaded from multiple YAML files, merged from the following lookup paths from bottom to top:
160
+ By default, parameters are loaded from multiple YAML files, merged from the
161
+ following lookup paths from bottom to top:
159
162
 
160
163
  - parameters/[stack_name].yaml
161
164
  - parameters/[stack_name].yml
@@ -166,10 +169,34 @@ Parameters are loaded from multiple YAML files, merged from the following lookup
166
169
 
167
170
  A simple parameter file could look like this:
168
171
 
169
- ```
172
+ ```yaml
170
173
  key_name: myapp-us-east-1
171
174
  ```
172
175
 
176
+ Alternatively, a `parameter_files` array can be defined to explicitly list
177
+ parameter files that will be loaded. If `parameter_files` are defined, the
178
+ automatic search locations will not be used.
179
+
180
+ ```yaml
181
+ parameters_dir: parameters # the default
182
+ stacks:
183
+ us-east-1:
184
+ my-app:
185
+ parameter_files:
186
+ - my-app.yml # parameters/my-app.yml
187
+ ```
188
+
189
+ Parameters can also be defined inline with stack definitions:
190
+
191
+ ```yaml
192
+ stacks:
193
+ us-east-1:
194
+ my-app:
195
+ parameters:
196
+ VpcId:
197
+ stack_output: my-vpc/VpcId
198
+ ```
199
+
173
200
  ### Compile Time Parameters
174
201
 
175
202
  Compile time parameters can be used for [SparkleFormation](http://www.sparkleformation.io) templates. It conforms and
@@ -177,7 +204,7 @@ allows you to use the [Compile Time Parameters](http://www.sparkleformation.io/d
177
204
 
178
205
  A simple example looks like this
179
206
 
180
- ```
207
+ ```yaml
181
208
  vpc_cidr: 10.0.0.0/16
182
209
  compile_time_parameters:
183
210
  subnet_cidrs:
@@ -286,8 +313,8 @@ db_password:
286
313
  An alternative to the secrets store is accessing 1password secrets using the 1password cli (`op`).
287
314
  You declare a 1password lookup with the following parameters in your parameters file:
288
315
 
289
- ```
290
- parameters/database.yml
316
+ ```yaml
317
+ # parameters/database.yml
291
318
  database_password:
292
319
  one_password:
293
320
  title: production database
@@ -477,7 +504,7 @@ name of the original resolver.
477
504
 
478
505
  When creating a new resolver, one can automatically create the array resolver by adding a `array_resolver` statement
479
506
  in the class definition, with an optional class name if different from the default one.
480
- ```
507
+ ```ruby
481
508
  module StackMaster
482
509
  module ParameterResolvers
483
510
  class MyResolver < Resolver
@@ -488,7 +515,7 @@ module StackMaster
488
515
  end
489
516
  ```
490
517
  In that example, using the array resolver would look like:
491
- ```
518
+ ```yaml
492
519
  my_parameter:
493
520
  my_custom_array_resolver:
494
521
  - value1
@@ -498,13 +525,13 @@ my_parameter:
498
525
  Array parameter values can include nested parameter resolvers.
499
526
 
500
527
  For example, given the following parameter definition:
501
- ```
528
+ ```yaml
502
529
  my_parameter:
503
530
  - stack_output: my-stack/output # value resolves to 'value1'
504
531
  - value2
505
532
  ```
506
533
  The parameter value will resolve to:
507
- ```
534
+ ```yaml
508
535
  my_parameter: 'value1,value2'
509
536
  ```
510
537
 
@@ -520,7 +547,7 @@ ROLE=<%= role %>
520
547
 
521
548
  And used like this in SparkleFormation templates:
522
549
 
523
- ```
550
+ ```ruby
524
551
  # templates/app.rb
525
552
  user_data user_data_file!('app.erb', role: :worker)
526
553
  ```
@@ -533,7 +560,7 @@ my_variable=<%= ref!(:foo) %>
533
560
  my_other_variable=<%= account_id! %>
534
561
  ```
535
562
 
536
- ```
563
+ ```ruby
537
564
  # templates/ecs_task.rb
538
565
  container_definitions array!(
539
566
  -> {
@@ -565,7 +592,7 @@ project-root
565
592
 
566
593
  Your env-1/stack_master.yml files can reference common templates by setting:
567
594
 
568
- ```
595
+ ```yaml
569
596
  template_dir: ../../sparkle/templates
570
597
  stack_defaults:
571
598
  compiler_options:
@@ -625,7 +652,7 @@ stacks:
625
652
 
626
653
  ## Allowed accounts
627
654
 
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 the stack is allowed to work with.
655
+ 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.
629
656
 
630
657
  This is an opt-in feature which is enabled by specifying at least one account to allow.
631
658
 
@@ -644,7 +671,7 @@ stacks:
644
671
  template: myapp_db.rb
645
672
  allowed_accounts: # only allow these accounts (overrides the stack defaults)
646
673
  - '1234567890'
647
- - '9876543210'
674
+ - my-account-alias
648
675
  tags:
649
676
  purpose: back-end
650
677
  myapp-web:
@@ -659,7 +686,7 @@ stacks:
659
686
  purpose: back-end
660
687
  ```
661
688
 
662
- In the cases where you want to bypass the account check, there is StackMaster flag `--skip-account-check` that can be used.
689
+ In the cases where you want to bypass the account check, there is the StackMaster flag `--skip-account-check` that can be used.
663
690
 
664
691
  ## Commands
665
692
 
@@ -7,7 +7,8 @@ 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'
10
+ require 'aws-sdk-iam'
11
+ require 'rainbow'
11
12
  require 'active_support/core_ext/hash/keys'
12
13
  require 'active_support/core_ext/object/blank'
13
14
  require 'active_support/core_ext/string/inflections'
@@ -45,6 +46,7 @@ module StackMaster
45
46
 
46
47
  autoload :StackDiffer, 'stack_master/stack_differ'
47
48
  autoload :Validator, 'stack_master/validator'
49
+ autoload :ParameterValidator, 'stack_master/parameter_validator'
48
50
 
49
51
  require 'stack_master/template_compilers/sparkle_formation'
50
52
  require 'stack_master/template_compilers/json'
@@ -128,7 +130,7 @@ module StackMaster
128
130
 
129
131
  def debug(message)
130
132
  return unless debug?
131
- stderr.puts "[DEBUG] #{message}".colorize(:green)
133
+ stderr.puts Rainbow("[DEBUG] #{message}").color(:green)
132
134
  end
133
135
 
134
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,7 +67,7 @@ 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
@@ -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,7 +116,7 @@ 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
122
  StackMaster::Commands::ListStacks.perform(config, nil, options)
@@ -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,7 +163,7 @@ 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
169
  StackMaster::Commands::Status.perform(config, nil, options)
@@ -175,7 +176,7 @@ 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
182
  StackMaster::Commands::Tidy.perform(config, nil, options)
@@ -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
@@ -1,5 +1,3 @@
1
- require 'pathname'
2
-
3
1
  module StackMaster
4
2
  module Commands
5
3
  class Apply
@@ -205,18 +203,8 @@ module StackMaster
205
203
  end
206
204
 
207
205
  def ensure_valid_parameters!
208
- if @proposed_stack.missing_parameters?
209
- message = <<~MESSAGE
210
- Empty/blank parameters detected, ensure values exist for those parameters.
211
- Parameters will be read from the following locations:
212
- MESSAGE
213
- base_dir = Pathname.new(@stack_definition.base_dir)
214
- @stack_definition.parameter_file_globs.each do |glob|
215
- parameter_file = Pathname.new(glob).relative_path_from(base_dir)
216
- message << " - #{parameter_file}\n"
217
- end
218
- failed!(message)
219
- end
206
+ pv = ParameterValidator.new(stack: @proposed_stack, stack_definition: @stack_definition)
207
+ failed!(pv.error_message) if pv.missing_parameters?
220
208
  end
221
209
 
222
210
  def ensure_valid_template_body_size!
@@ -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
@@ -12,7 +12,7 @@ module StackMaster
12
12
  parameter_files = Set.new(find_parameter_files())
13
13
 
14
14
  status = @config.stacks.each do |stack_definition|
15
- parameter_files.subtract(stack_definition.parameter_files)
15
+ parameter_files.subtract(stack_definition.parameter_files_from_globs)
16
16
  template = File.absolute_path(stack_definition.template_file_path)
17
17
 
18
18
  if template
@@ -5,7 +5,7 @@ module StackMaster
5
5
  include Commander::UI
6
6
 
7
7
  def perform
8
- failed unless Validator.valid?(@stack_definition, @config)
8
+ failed unless Validator.valid?(@stack_definition, @config, @options)
9
9
  end
10
10
  end
11
11
  end
@@ -17,6 +17,7 @@ module StackMaster
17
17
  attr_accessor :stacks,
18
18
  :base_dir,
19
19
  :template_dir,
20
+ :parameters_dir,
20
21
  :stack_defaults,
21
22
  :region_defaults,
22
23
  :region_aliases,
@@ -39,6 +40,7 @@ module StackMaster
39
40
  @config = config
40
41
  @base_dir = base_dir
41
42
  @template_dir = config.fetch('template_dir', nil)
43
+ @parameters_dir = config.fetch('parameters_dir', nil)
42
44
  @stack_defaults = config.fetch('stack_defaults', {})
43
45
  @region_aliases = Utils.underscore_keys_to_hyphen(config.fetch('region_aliases', {}))
44
46
  @region_to_aliases = @region_aliases.inject({}) do |hash, (key, value)|
@@ -115,6 +117,7 @@ module StackMaster
115
117
  'stack_name' => stack_name,
116
118
  'base_dir' => @base_dir,
117
119
  'template_dir' => @template_dir,
120
+ 'parameters_dir' => @parameters_dir,
118
121
  'additional_parameter_lookup_dirs' => @region_to_aliases[region])
119
122
  stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts']
120
123
  @stacks << StackDefinition.new(stack_attributes)
@@ -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
@@ -5,10 +5,10 @@ module StackMaster
5
5
 
6
6
  COMPILE_TIME_PARAMETERS_KEY = 'compile_time_parameters'
7
7
 
8
- def self.load(parameter_files)
8
+ def self.load(parameter_files: [], parameters: {})
9
9
  StackMaster.debug 'Searching for parameter files...'
10
- parameter_files.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, file_name|
11
- parameters = load_parameters(file_name)
10
+ all_parameters = parameter_files.map { |file_name| load_parameters(file_name) } + [parameters]
11
+ all_parameters.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, parameters|
12
12
  template_parameters = create_template_parameters(parameters)
13
13
  compile_time_parameters = create_compile_time_parameters(parameters)
14
14
 
@@ -16,7 +16,6 @@ module StackMaster
16
16
  merge_and_camelize(hash[:compile_time_parameters], compile_time_parameters)
17
17
  hash
18
18
  end
19
-
20
19
  end
21
20
 
22
21
  private
@@ -0,0 +1,53 @@
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:\n"
13
+ missing_parameters.each do |parameter_name|
14
+ message << " - #{parameter_name}\n"
15
+ end
16
+ if @stack_definition.parameter_files.empty?
17
+ message << message_for_parameter_globs
18
+ else
19
+ message << message_for_parameter_files
20
+ end
21
+ message
22
+ end
23
+
24
+ def missing_parameters?
25
+ missing_parameters.any?
26
+ end
27
+
28
+ private
29
+
30
+ def message_for_parameter_files
31
+ "Parameters are configured to be read from the following files:\n".tap do |message|
32
+ @stack_definition.parameter_files.each do |parameter_file|
33
+ message << " - #{parameter_file}\n"
34
+ end
35
+ end
36
+ end
37
+
38
+ def message_for_parameter_globs
39
+ "Parameters will be read from files matching the following globs:\n".tap do |message|
40
+ base_dir = Pathname.new(@stack_definition.base_dir)
41
+ @stack_definition.parameter_file_globs.each do |glob|
42
+ parameter_file = Pathname.new(glob).relative_path_from(base_dir)
43
+ message << " - #{parameter_file}\n"
44
+ end
45
+ end
46
+ end
47
+
48
+ def missing_parameters
49
+ @missing_parameters ||=
50
+ @stack.parameters_with_defaults.select { |_key, value| value.nil? }.keys
51
+ end
52
+ end
53
+ 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
  )
@@ -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
@@ -62,7 +56,7 @@ module StackMaster
62
56
  end
63
57
 
64
58
  def self.generate(stack_definition, config)
65
- parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
59
+ parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
66
60
  template_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:template_parameters])
67
61
  compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
68
62
  template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
@@ -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(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
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
@@ -16,7 +16,10 @@ module StackMaster
16
16
  :additional_parameter_lookup_dirs,
17
17
  :s3,
18
18
  :files,
19
- :compiler_options
19
+ :compiler_options,
20
+ :parameters_dir,
21
+ :parameters,
22
+ :parameter_files
20
23
 
21
24
  attr_reader :compiler
22
25
 
@@ -32,8 +35,12 @@ module StackMaster
32
35
  @compiler = nil
33
36
  super
34
37
  @additional_parameter_lookup_dirs ||= []
38
+ @base_dir ||= ""
35
39
  @template_dir ||= File.join(@base_dir, 'templates')
40
+ @parameters_dir ||= File.join(@base_dir, 'parameters')
36
41
  @allowed_accounts = Array(@allowed_accounts)
42
+ @parameters ||= {}
43
+ @parameter_files ||= []
37
44
  end
38
45
 
39
46
  def ==(other)
@@ -56,13 +63,9 @@ module StackMaster
56
63
  @compiler_options == other.compiler_options
57
64
  end
58
65
 
59
- def compiler=(compiler)
60
- @compiler = compiler.&to_sym
61
- end
62
-
63
66
  def template_file_path
64
67
  return unless template
65
- File.expand_path(File.join(template_dir, template))
68
+ File.expand_path(template, template_dir)
66
69
  end
67
70
 
68
71
  def files_dir
@@ -85,7 +88,15 @@ module StackMaster
85
88
  Utils.change_extension(template, 'json')
86
89
  end
87
90
 
88
- def parameter_files
91
+ def all_parameter_files
92
+ if parameter_files.empty?
93
+ parameter_files_from_globs
94
+ else
95
+ parameter_files
96
+ end
97
+ end
98
+
99
+ def parameter_files_from_globs
89
100
  parameter_file_globs.map(&Dir.method(:glob)).flatten
90
101
  end
91
102
 
@@ -101,20 +112,26 @@ module StackMaster
101
112
  !s3.nil?
102
113
  end
103
114
 
115
+ def parameter_files
116
+ Array(@parameter_files).map do |file|
117
+ File.expand_path(file, parameters_dir)
118
+ end
119
+ end
120
+
104
121
  private
105
122
 
106
123
  def additional_parameter_lookup_globs
107
124
  additional_parameter_lookup_dirs.map do |a|
108
- File.join(base_dir, 'parameters', a, "#{stack_name_glob}.y*ml")
125
+ File.join(parameters_dir, a, "#{stack_name_glob}.y*ml")
109
126
  end
110
127
  end
111
128
 
112
129
  def region_parameter_glob
113
- File.join(base_dir, 'parameters', "#{region}", "#{stack_name_glob}.y*ml")
130
+ File.join(parameters_dir, "#{region}", "#{stack_name_glob}.y*ml")
114
131
  end
115
132
 
116
133
  def default_parameter_glob
117
- File.join(base_dir, 'parameters', "#{stack_name_glob}.y*ml")
134
+ File.join(parameters_dir, "#{stack_name_glob}.y*ml")
118
135
  end
119
136
 
120
137
  def stack_name_glob
@@ -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)
@@ -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.3.0"
2
+ VERSION = "2.7.0"
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.3.0
4
+ version: 2.7.0
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-18 00:00:00.000000000 Z
12
+ date: 2020-06-15 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.3.0
527
- source_code_uri: https://github.com/envato/stack_master/tree/v2.3.0
542
+ documentation_uri: https://www.rubydoc.info/gems/stack_master/2.7.0
543
+ source_code_uri: https://github.com/envato/stack_master/tree/v2.7.0
528
544
  post_install_message:
529
545
  rdoc_options: []
530
546
  require_paths:
@@ -540,7 +556,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
540
556
  - !ruby/object:Gem::Version
541
557
  version: '0'
542
558
  requirements: []
543
- rubygems_version: 3.1.2
559
+ rubygems_version: 3.0.3
544
560
  signing_key:
545
561
  specification_version: 4
546
562
  summary: StackMaster is a sure-footed way of creating, updating and keeping track