stack_master 2.5.0 → 2.9.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: eb7f5a24127477671e8e06a522daedc090735825f9911168123d0a7173bf2350
4
- data.tar.gz: 2301be2eaf943e22f980c07b75686d5303a77e3475c34f9de0eee1e6654a9bf6
3
+ metadata.gz: 2ba7ee171f7c218998ce73593d18bb62165c40a201f088b51192a7cfc1d8fb10
4
+ data.tar.gz: 368a14a56b4cbd7678d71860902c1e1d360756c6272bd1fc6a7b1b6f5ac8f7c3
5
5
  SHA512:
6
- metadata.gz: 59a9b3bc3d11e90bd4a01c57b76085bc2b702c3c10feee855d66cf3199225068a9298ff7e750dea12fdd55746e91e5b61f673065ebc48b59f22e1d2344c1c6f9
7
- data.tar.gz: bdef690c7b5ea589d060f92c8c09a9fb4f491216ed2997a2c37f3c67bea41a598d70039686a17d44efa26d03e8cc17786ce5a424efd9d41f659af370ee98f8ed
6
+ metadata.gz: 6eb37d06cd15fbf0852f7c798a18b35747b001d8ffe1edfcdf55c25c629e46d731c47dc5466e8af7275512f6f2dac54b492c714069dfded2c78ab6920bea04af
7
+ data.tar.gz: 03c242ee5d588b817a53d1bf74e6808c762d37047805fcd79b18c73d5f2e84ce87ed8114efe7ea583f05d71449b4d64b50f4cce7395b72784d9c44837cb9579d
data/README.md CHANGED
@@ -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
@@ -170,6 +173,30 @@ A simple parameter file could look like this:
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
@@ -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)
@@ -215,6 +215,18 @@ module StackMaster
215
215
  end
216
216
  end
217
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.option '--timeout SECONDS', Integer, "The number of seconds to wait for drift detection to complete"
223
+ c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
224
+ c.action do |args, options|
225
+ options.default config: default_config_file, timeout: 120
226
+ execute_stacks_command(StackMaster::Commands::Drift, args, options)
227
+ end
228
+ end
229
+
218
230
  run!
219
231
  end
220
232
 
@@ -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
+ resp = nil
92
+ start_time = Time.now
93
+ loop do
94
+ resp = cf.describe_stack_drift_detection_status(stack_drift_detection_id: detection_id)
95
+ break if DETECTION_COMPLETE_STATES.include?(resp.detection_status)
96
+
97
+ elapsed_time = Time.now - start_time
98
+ if elapsed_time > @options.timeout
99
+ raise "Timeout waiting for stack drift detection"
100
+ end
101
+
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
@@ -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)
@@ -76,7 +76,7 @@ module StackMaster
76
76
  end
77
77
 
78
78
  def self.generate_without_parameters(stack_definition, config)
79
- parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
79
+ parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters)
80
80
  compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters])
81
81
  template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options)
82
82
  template_format = TemplateUtils.identify_template_format(template_body)
@@ -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,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "2.5.0"
2
+ VERSION = "2.9.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.5.0
4
+ version: 2.9.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-05-08 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
  - - ">="
@@ -459,6 +459,7 @@ files:
459
459
  - lib/stack_master/commands/compile.rb
460
460
  - lib/stack_master/commands/delete.rb
461
461
  - lib/stack_master/commands/diff.rb
462
+ - lib/stack_master/commands/drift.rb
462
463
  - lib/stack_master/commands/events.rb
463
464
  - lib/stack_master/commands/init.rb
464
465
  - lib/stack_master/commands/lint.rb
@@ -471,6 +472,7 @@ files:
471
472
  - lib/stack_master/commands/validate.rb
472
473
  - lib/stack_master/config.rb
473
474
  - lib/stack_master/ctrl_c.rb
475
+ - lib/stack_master/diff.rb
474
476
  - lib/stack_master/identity.rb
475
477
  - lib/stack_master/paged_response_accumulator.rb
476
478
  - lib/stack_master/parameter_loader.rb
@@ -539,8 +541,8 @@ licenses:
539
541
  metadata:
540
542
  bug_tracker_uri: https://github.com/envato/stack_master/issues
541
543
  changelog_uri: https://github.com/envato/stack_master/blob/master/CHANGELOG.md
542
- documentation_uri: https://www.rubydoc.info/gems/stack_master/2.5.0
543
- source_code_uri: https://github.com/envato/stack_master/tree/v2.5.0
544
+ documentation_uri: https://www.rubydoc.info/gems/stack_master/2.9.0
545
+ source_code_uri: https://github.com/envato/stack_master/tree/v2.9.0
544
546
  post_install_message:
545
547
  rdoc_options: []
546
548
  require_paths:
@@ -556,7 +558,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
556
558
  - !ruby/object:Gem::Version
557
559
  version: '0'
558
560
  requirements: []
559
- rubygems_version: 3.1.2
561
+ rubygems_version: 3.0.3
560
562
  signing_key:
561
563
  specification_version: 4
562
564
  summary: StackMaster is a sure-footed way of creating, updating and keeping track