stack_master 2.4.0 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 676259d50dc1b2634f669f98625e3203f57d39eb77d6e6bf55f0e3b13318174b
4
- data.tar.gz: f759222c480eed85f359c9b6f713f865664f156078a065bd4ad16fcd2db47417
3
+ metadata.gz: 7d401f198bf80974977c8b031f27422ea84aa171f18747fa6064cf6d9d2af784
4
+ data.tar.gz: dd930994d8034f57d60ba1a77d8bfee0f524301501306a3d8cee64502c27029e
5
5
  SHA512:
6
- metadata.gz: 482ccf3ecd0511921404744dd8f081f6f1c17c954918bc8fef5e5d6b2bf4ddebafd74d942c22b858062abb9e38fa238031d0fb310c8a5dbd02482833b6119641
7
- data.tar.gz: f9fdaf391a317ccaa1cad9e527b8bd4b529c7446b6bc13335a31fc8fde7504878f6f9183959742710e9a5b637ad3a783b302f2955f7ed6c2b67ba2876ef6fb62
6
+ metadata.gz: de26f0ce262ef2b4b11cced8789a8093896d9bd7db6950008ad7e0e469ea43a9752fbbcb0e4b57271656442aa7bb13dc159697690d0a0754fcbc9ecce39d07b0
7
+ data.tar.gz: 0ef954037a433821d09b90acbbd39da5a881044f305353267cba379a07e77bd2ffe7645700b8ce927d15d7f518c823aa17a42f0c9971ce3c37b29f5164a8b219
@@ -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:
@@ -661,6 +688,7 @@ stacks:
661
688
 
662
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
 
691
+
664
692
  ## Commands
665
693
 
666
694
  ```bash
@@ -674,6 +702,7 @@ stack_master apply # Create or update all stacks
674
702
  stack_master --changed apply # Create or update all stacks that have changed
675
703
  stack_master --yes apply [region-or-alias] [stack-name] # Create or update a stack non-interactively (forcing yes)
676
704
  stack_master diff [region-or-alias] [stack-name] # Display a stack template and parameter diff
705
+ stack_master drift [region-or-alias] [stack-name] # Detects and displays stack drift using the CloudFormation Drift API
677
706
  stack_master delete [region-or-alias] [stack-name] # Delete a stack
678
707
  stack_master events [region-or-alias] [stack-name] # Display events for a stack
679
708
  stack_master outputs [region-or-alias] [stack-name] # Display outputs for a stack
@@ -682,7 +711,7 @@ stack_master status # Displays the status of each stack
682
711
  stack_master tidy # Find missing or extra templates or parameter files
683
712
  ```
684
713
 
685
- ## Applying updates
714
+ ## Applying updates - `stack_master apply`
686
715
 
687
716
  The apply command does the following:
688
717
 
@@ -699,6 +728,18 @@ Demo:
699
728
 
700
729
  ![Apply Demo](/apply_demo.gif?raw=true)
701
730
 
731
+ ## Drift Detection - `stack_master drift`
732
+
733
+ `stack_master drift us-east-1 mystack` uses the CloudFormation APIs to trigger drift detection and display resources
734
+ that have changed outside of the CloudFormation stack. This can happen if a resource has been updated via the console or
735
+ CLI directly rather than via a stack update.
736
+
737
+ ## Diff - `stack_master diff`
738
+
739
+ `stack_master diff us-east-1 mystack` displays whether the computed parameters or template differ to what was last
740
+ applied in CloudFormation. This can happen if the template or computed parameters have changed in code and the change
741
+ hasn't been applied to this stack.
742
+
702
743
  ## Maintainers
703
744
 
704
745
  - [Steve Hodgkiss](https://github.com/stevehodgkiss)
@@ -8,7 +8,7 @@ require 'aws-sdk-s3'
8
8
  require 'aws-sdk-sns'
9
9
  require 'aws-sdk-ssm'
10
10
  require 'aws-sdk-iam'
11
- require 'colorize'
11
+ require 'rainbow'
12
12
  require 'active_support/core_ext/hash/keys'
13
13
  require 'active_support/core_ext/object/blank'
14
14
  require 'active_support/core_ext/string/inflections'
@@ -24,6 +24,7 @@ module StackMaster
24
24
  autoload :CLI, 'stack_master/cli'
25
25
  autoload :CtrlC, 'stack_master/ctrl_c'
26
26
  autoload :Command, 'stack_master/command'
27
+ autoload :Diff, 'stack_master/diff'
27
28
  autoload :VERSION, 'stack_master/version'
28
29
  autoload :Stack, 'stack_master/stack'
29
30
  autoload :Prompter, 'stack_master/prompter'
@@ -56,6 +57,7 @@ module StackMaster
56
57
  module Commands
57
58
  autoload :TerminalHelper, 'stack_master/commands/terminal_helper'
58
59
  autoload :Apply, 'stack_master/commands/apply'
60
+ autoload :Drift, 'stack_master/commands/drift'
59
61
  autoload :Events, 'stack_master/commands/events'
60
62
  autoload :Outputs, 'stack_master/commands/outputs'
61
63
  autoload :Init, 'stack_master/commands/init'
@@ -130,7 +132,7 @@ module StackMaster
130
132
 
131
133
  def debug(message)
132
134
  return unless debug?
133
- stderr.puts "[DEBUG] #{message}".colorize(:green)
135
+ stderr.puts Rainbow("[DEBUG] #{message}").color(:green)
134
136
  end
135
137
 
136
138
  def quiet!
@@ -198,4 +200,16 @@ module StackMaster
198
200
  def stderr=(io)
199
201
  @stderr = io
200
202
  end
203
+
204
+ def colorize(text, color)
205
+ if colorize?
206
+ Rainbow(text).color(color)
207
+ else
208
+ text
209
+ end
210
+ end
211
+
212
+ def colorize?
213
+ ENV.fetch('COLORIZE') { 'true' } == 'true'
214
+ end
201
215
  end
@@ -28,7 +28,10 @@ module StackMaster
28
28
  :update_stack,
29
29
  :create_stack,
30
30
  :validate_template,
31
- :describe_stacks
31
+ :describe_stacks,
32
+ :detect_stack_drift,
33
+ :describe_stack_drift_detection_status,
34
+ :describe_stack_resource_drifts
32
35
 
33
36
  private
34
37
 
@@ -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)
@@ -214,6 +215,17 @@ module StackMaster
214
215
  end
215
216
  end
216
217
 
218
+ command :drift do |c|
219
+ c.syntax = 'stack_master drift [region_or_alias] [stack_name]'
220
+ c.summary = 'Detects and displays stack drift using the CloudFormation Drift API'
221
+ c.description = 'Detects and displays stack drift'
222
+ c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
223
+ c.action do |args, options|
224
+ options.default config: default_config_file
225
+ execute_stacks_command(StackMaster::Commands::Drift, args, options)
226
+ end
227
+ end
228
+
217
229
  run!
218
230
  end
219
231
 
@@ -0,0 +1,118 @@
1
+ require 'diffy'
2
+
3
+ module StackMaster
4
+ module Commands
5
+ class Drift
6
+ include Command
7
+ include Commander::UI
8
+
9
+ DETECTION_COMPLETE_STATES = [
10
+ 'DETECTION_COMPLETE',
11
+ 'DETECTION_FAILED'
12
+ ]
13
+
14
+ def perform
15
+ detect_stack_drift_result = cf.detect_stack_drift(stack_name: stack_name)
16
+ drift_results = wait_for_drift_results(detect_stack_drift_result.stack_drift_detection_id)
17
+
18
+ puts colorize("Drift Status: #{drift_results.stack_drift_status}", stack_drift_status_color(drift_results.stack_drift_status))
19
+ return if drift_results.stack_drift_status == 'IN_SYNC'
20
+
21
+ failed
22
+
23
+ resp = cf.describe_stack_resource_drifts(stack_name: stack_name)
24
+ resp.stack_resource_drifts.each do |drift|
25
+ display_drift(drift)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def cf
32
+ @cf ||= StackMaster.cloud_formation_driver
33
+ end
34
+
35
+ def display_drift(drift)
36
+ color = drift_color(drift)
37
+ puts colorize([drift.stack_resource_drift_status,
38
+ drift.resource_type,
39
+ drift.logical_resource_id,
40
+ drift.physical_resource_id].join(' '), color)
41
+ return unless drift.stack_resource_drift_status == 'MODIFIED'
42
+
43
+ unless drift.property_differences.empty?
44
+ puts colorize(' Property differences:', color)
45
+ end
46
+ drift.property_differences.each do |property_difference|
47
+ puts colorize(" - #{property_difference.difference_type} #{property_difference.property_path}", color)
48
+ end
49
+ puts colorize(' Resource diff:', color)
50
+ display_resource_drift(drift)
51
+ end
52
+
53
+ def display_resource_drift(drift)
54
+ diff = ::StackMaster::Diff.new(before: prettify_json(drift.expected_properties),
55
+ after: prettify_json(drift.actual_properties))
56
+ diff.display_colorized_diff
57
+ end
58
+
59
+ def prettify_json(string)
60
+ JSON.pretty_generate(JSON.parse(string)) + "\n"
61
+ rescue StandardError => e
62
+ puts "Failed to prettify drifted resource: #{e.message}"
63
+ string
64
+ end
65
+
66
+ def stack_drift_status_color(stack_drift_status)
67
+ case stack_drift_status
68
+ when 'IN_SYNC'
69
+ :green
70
+ when 'DRIFTED'
71
+ :yellow
72
+ else
73
+ :blue
74
+ end
75
+ end
76
+
77
+ def drift_color(drift)
78
+ case drift.stack_resource_drift_status
79
+ when 'IN_SYNC'
80
+ :green
81
+ when 'MODIFIED'
82
+ :yellow
83
+ when 'DELETED'
84
+ :red
85
+ else
86
+ :blue
87
+ end
88
+ end
89
+
90
+ def wait_for_drift_results(detection_id)
91
+ try_count = 0
92
+ resp = nil
93
+ loop do
94
+ if try_count >= 10
95
+ raise 'Failed to wait for stack drift detection after 10 tries'
96
+ end
97
+
98
+ resp = cf.describe_stack_drift_detection_status(stack_drift_detection_id: detection_id)
99
+ break if DETECTION_COMPLETE_STATES.include?(resp.detection_status)
100
+
101
+ try_count += 1
102
+ sleep SLEEP_SECONDS
103
+ end
104
+ resp
105
+ end
106
+
107
+ def puts(string)
108
+ StackMaster.stdout.puts(string)
109
+ end
110
+
111
+ extend Forwardable
112
+ def_delegators :@stack_definition, :stack_name, :region
113
+ def_delegators :StackMaster, :colorize
114
+
115
+ SLEEP_SECONDS = 1
116
+ end
117
+ end
118
+ end
@@ -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)
@@ -0,0 +1,45 @@
1
+ module StackMaster
2
+ class Diff
3
+ def initialize(name: nil, before:, after:, context: 10_000)
4
+ @name = name
5
+ @before = before
6
+ @after = after
7
+ @context = context
8
+ end
9
+
10
+ def display
11
+ stdout.print "#{@name} diff: "
12
+ if diff == ''
13
+ stdout.puts "No changes"
14
+ else
15
+ stdout.puts
16
+ display_colorized_diff
17
+ end
18
+ end
19
+
20
+ def display_colorized_diff
21
+ diff.each_line do |line|
22
+ if line.start_with?('+')
23
+ stdout.print colorize(line, :green)
24
+ elsif line.start_with?('-')
25
+ stdout.print colorize(line, :red)
26
+ else
27
+ stdout.print line
28
+ end
29
+ end
30
+ end
31
+
32
+ def different?
33
+ diff != ''
34
+ end
35
+
36
+ private
37
+
38
+ def diff
39
+ @diff ||= Diffy::Diff.new(@before, @after, context: @context).to_s
40
+ end
41
+
42
+ extend Forwardable
43
+ def_delegators :StackMaster, :colorize, :stdout
44
+ end
45
+ 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
@@ -9,15 +9,14 @@ module StackMaster
9
9
 
10
10
  def error_message
11
11
  return nil unless missing_parameters?
12
- message = "Empty/blank parameters detected. Please provide values for these parameters:"
12
+ message = "Empty/blank parameters detected. Please provide values for these parameters:\n"
13
13
  missing_parameters.each do |parameter_name|
14
- message << "\n - #{parameter_name}"
14
+ message << " - #{parameter_name}\n"
15
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}"
16
+ if @stack_definition.parameter_files.empty?
17
+ message << message_for_parameter_globs
18
+ else
19
+ message << message_for_parameter_files
21
20
  end
22
21
  message
23
22
  end
@@ -28,6 +27,24 @@ module StackMaster
28
27
 
29
28
  private
30
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
+
31
48
  def missing_parameters
32
49
  @missing_parameters ||=
33
50
  @stack.parameters_with_defaults.select { |_key, value| value.nil? }.keys
@@ -56,7 +56,7 @@ module StackMaster
56
56
  end
57
57
 
58
58
  def self.generate(stack_definition, config)
59
- parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
59
+ parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
60
60
  template_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:template_parameters])
61
61
  compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
62
62
  template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
@@ -75,6 +75,25 @@ module StackMaster
75
75
  stack_policy_body: stack_policy_body)
76
76
  end
77
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
+
78
97
  def max_template_size(use_s3)
79
98
  return TemplateUtils::MAX_S3_TEMPLATE_SIZE if use_s3
80
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
@@ -10,13 +10,13 @@ module StackMaster
10
10
 
11
11
  def proposed_template
12
12
  return @proposed_stack.template_body unless @proposed_stack.template_format == :json
13
- JSON.pretty_generate(JSON.parse(@proposed_stack.template_body))
13
+ JSON.pretty_generate(JSON.parse(@proposed_stack.template_body)) + "\n"
14
14
  end
15
15
 
16
16
  def current_template
17
17
  return '' unless @current_stack
18
18
  return @current_stack.template_body unless @current_stack.template_format == :json
19
- JSON.pretty_generate(TemplateUtils.template_hash(@current_stack.template_body))
19
+ JSON.pretty_generate(TemplateUtils.template_hash(@current_stack.template_body)) + "\n"
20
20
  end
21
21
 
22
22
  def current_parameters
@@ -39,24 +39,30 @@ module StackMaster
39
39
  end
40
40
 
41
41
  def body_different?
42
- body_diff != ''
42
+ body_diff.different?
43
43
  end
44
44
 
45
45
  def body_diff
46
- @body_diff ||= Diffy::Diff.new(current_template, proposed_template, context: 7, include_diff_info: true).to_s
46
+ @body_diff ||= Diff.new(name: 'Stack',
47
+ before: current_template,
48
+ after: proposed_template,
49
+ context: 7)
47
50
  end
48
51
 
49
52
  def params_different?
50
- params_diff != ''
53
+ parameters_diff.different?
51
54
  end
52
55
 
53
- def params_diff
54
- @param_diff ||= Diffy::Diff.new(current_parameters, proposed_parameters, {}).to_s
56
+ def parameters_diff
57
+ @param_diff ||= Diff.new(name: 'Parameters',
58
+ before: current_parameters,
59
+ after: proposed_parameters)
55
60
  end
56
61
 
57
62
  def output_diff
58
- display_diff('Stack', body_diff)
59
- display_diff('Parameters', params_diff)
63
+ body_diff.display
64
+ parameters_diff.display
65
+
60
66
  unless noecho_keys.empty?
61
67
  StackMaster.stdout.puts " * can not tell if NoEcho parameters are different."
62
68
  end
@@ -83,38 +89,8 @@ module StackMaster
83
89
 
84
90
  private
85
91
 
86
- def display_diff(thing, diff)
87
- StackMaster.stdout.print "#{thing} diff: "
88
- if diff == ''
89
- StackMaster.stdout.puts "No changes"
90
- else
91
- StackMaster.stdout.puts
92
- diff.each_line do |line|
93
- if line.start_with?('+')
94
- StackMaster.stdout.print colorize(line, :green)
95
- elsif line.start_with?('-')
96
- StackMaster.stdout.print colorize(line, :red)
97
- else
98
- StackMaster.stdout.print line
99
- end
100
- end
101
- end
102
- end
103
-
104
92
  def sort_params(hash)
105
93
  hash.sort.to_h
106
94
  end
107
-
108
- def colorize(text, color)
109
- if colorize?
110
- text.colorize(color)
111
- else
112
- text
113
- end
114
- end
115
-
116
- def colorize?
117
- ENV.fetch('COLORIZE') { 'true' } == 'true'
118
- end
119
95
  end
120
96
  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,17 +1,18 @@
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
14
  StackMaster.stdout.print "#{@stack_definition.stack_name}: "
14
- if parameter_validator.missing_parameters?
15
+ if validate_template_parameters? && parameter_validator.missing_parameters?
15
16
  StackMaster.stdout.puts "invalid\n#{parameter_validator.error_message}"
16
17
  return false
17
18
  end
@@ -25,12 +26,20 @@ 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
31
36
 
32
37
  def stack
33
- @stack ||= Stack.generate(@stack_definition, @config)
38
+ @stack ||= if validate_template_parameters?
39
+ Stack.generate(@stack_definition, @config)
40
+ else
41
+ Stack.generate_without_parameters(@stack_definition, @config)
42
+ end
34
43
  end
35
44
 
36
45
  def parameter_validator
@@ -1,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "2.4.0"
2
+ VERSION = "2.8.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.4.0
4
+ version: 2.8.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-04-03 00:00:00.000000000 Z
12
+ date: 2020-06-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -298,7 +298,7 @@ dependencies:
298
298
  - !ruby/object:Gem::Version
299
299
  version: '0'
300
300
  - !ruby/object:Gem::Dependency
301
- name: colorize
301
+ name: rainbow
302
302
  requirement: !ruby/object:Gem::Requirement
303
303
  requirements:
304
304
  - - ">="
@@ -446,6 +446,7 @@ executables:
446
446
  extensions: []
447
447
  extra_rdoc_files: []
448
448
  files:
449
+ - LICENSE.txt
449
450
  - README.md
450
451
  - bin/stack_master
451
452
  - lib/stack_master.rb
@@ -458,6 +459,7 @@ files:
458
459
  - lib/stack_master/commands/compile.rb
459
460
  - lib/stack_master/commands/delete.rb
460
461
  - lib/stack_master/commands/diff.rb
462
+ - lib/stack_master/commands/drift.rb
461
463
  - lib/stack_master/commands/events.rb
462
464
  - lib/stack_master/commands/init.rb
463
465
  - lib/stack_master/commands/lint.rb
@@ -470,6 +472,7 @@ files:
470
472
  - lib/stack_master/commands/validate.rb
471
473
  - lib/stack_master/config.rb
472
474
  - lib/stack_master/ctrl_c.rb
475
+ - lib/stack_master/diff.rb
473
476
  - lib/stack_master/identity.rb
474
477
  - lib/stack_master/paged_response_accumulator.rb
475
478
  - lib/stack_master/parameter_loader.rb
@@ -538,8 +541,8 @@ licenses:
538
541
  metadata:
539
542
  bug_tracker_uri: https://github.com/envato/stack_master/issues
540
543
  changelog_uri: https://github.com/envato/stack_master/blob/master/CHANGELOG.md
541
- documentation_uri: https://www.rubydoc.info/gems/stack_master/2.4.0
542
- source_code_uri: https://github.com/envato/stack_master/tree/v2.4.0
544
+ documentation_uri: https://www.rubydoc.info/gems/stack_master/2.8.0
545
+ source_code_uri: https://github.com/envato/stack_master/tree/v2.8.0
543
546
  post_install_message:
544
547
  rdoc_options: []
545
548
  require_paths:
@@ -555,7 +558,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
555
558
  - !ruby/object:Gem::Version
556
559
  version: '0'
557
560
  requirements: []
558
- rubygems_version: 3.1.2
561
+ rubygems_version: 3.0.3
559
562
  signing_key:
560
563
  specification_version: 4
561
564
  summary: StackMaster is a sure-footed way of creating, updating and keeping track