stack_master 2.3.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +22 -0
  3. data/README.md +100 -23
  4. data/lib/stack_master/aws_driver/cloud_formation.rb +5 -2
  5. data/lib/stack_master/aws_driver/s3.rb +5 -5
  6. data/lib/stack_master/change_set.rb +5 -5
  7. data/lib/stack_master/cli.rb +49 -15
  8. data/lib/stack_master/cloudformation_interpolating_eruby.rb +57 -0
  9. data/lib/stack_master/cloudformation_template_eruby.rb +32 -0
  10. data/lib/stack_master/commands/apply.rb +2 -14
  11. data/lib/stack_master/commands/compile.rb +1 -1
  12. data/lib/stack_master/commands/drift.rb +118 -0
  13. data/lib/stack_master/commands/init.rb +1 -1
  14. data/lib/stack_master/commands/nag.rb +30 -0
  15. data/lib/stack_master/commands/resources.rb +1 -1
  16. data/lib/stack_master/commands/status.rb +1 -1
  17. data/lib/stack_master/commands/tidy.rb +1 -1
  18. data/lib/stack_master/commands/validate.rb +1 -1
  19. data/lib/stack_master/config.rb +9 -1
  20. data/lib/stack_master/diff.rb +45 -0
  21. data/lib/stack_master/identity.rb +37 -5
  22. data/lib/stack_master/parameter_loader.rb +4 -5
  23. data/lib/stack_master/parameter_resolvers/acm_certificate.rb +2 -2
  24. data/lib/stack_master/parameter_resolvers/ami_finder.rb +3 -3
  25. data/lib/stack_master/parameter_resolvers/latest_container.rb +1 -1
  26. data/lib/stack_master/parameter_resolvers/parameter_store.rb +3 -3
  27. data/lib/stack_master/parameter_resolvers/stack_output.rb +1 -1
  28. data/lib/stack_master/parameter_validator.rb +53 -0
  29. data/lib/stack_master/role_assumer.rb +3 -2
  30. data/lib/stack_master/security_group_finder.rb +1 -1
  31. data/lib/stack_master/sns_topic_finder.rb +1 -1
  32. data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +1 -1
  33. data/lib/stack_master/sparkle_formation/template_file.rb +2 -50
  34. data/lib/stack_master/stack.rb +23 -10
  35. data/lib/stack_master/stack_definition.rb +27 -10
  36. data/lib/stack_master/stack_differ.rb +15 -39
  37. data/lib/stack_master/stack_events/presenter.rb +1 -1
  38. data/lib/stack_master/template_compilers/cfndsl.rb +3 -2
  39. data/lib/stack_master/template_compilers/sparkle_formation.rb +1 -1
  40. data/lib/stack_master/template_compilers/yaml_erb.rb +19 -0
  41. data/lib/stack_master/template_utils.rb +9 -3
  42. data/lib/stack_master/validator.rb +25 -8
  43. data/lib/stack_master/version.rb +1 -1
  44. data/lib/stack_master.rb +23 -2
  45. metadata +73 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b2fd9ecb9882d2a0b00adee84d08975f5ae51b72297a449c5bd5218efbf8a17
4
- data.tar.gz: c2eb23ba0ae38e6db17cc296a4a151aaaf7785bbff222d75ec671251ac398f1f
3
+ metadata.gz: bacf542a4b317cf98eb9e39a70740e393ddea1eb9363e65340975e568d3191e1
4
+ data.tar.gz: 4582583ac6fa9681edf9f3a8bf4ed478e728b09e0aaebce244234ab50ff0376c
5
5
  SHA512:
6
- metadata.gz: e5832578334d932b9750b9aa2914c6c0fc9d2b391606c3ab8fc60868b151c5b9fa996d2b89636d14515aff3b9892c8a913c1e0531ac57262a67072931a2a8f1c
7
- data.tar.gz: 87dd2728b566b6d775a0b8cb7137acc856f1ee854436e35789578c1c129798594375bc9ad06018be2285605878918cf89d872c3df72fe20a3260380407e32b25
6
+ metadata.gz: bce540f043c5010594155cfd41fab249b531e80febb96cc46cebce7b957e8de2f7aca20f8b0dc09cf13050338e0455d3438ec36341d49d838002ab9fa8b21905
7
+ data.tar.gz: 61dae834acf176052b3fe8c2bb53443fc44dc0b68dbf33f7b053365088624b664aa6ba504697e839a18a63c5192a961e80d1736ce7efbdc82351616faf9bb8d7
data/LICENSE.txt ADDED
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/stack_master/blob/master/LICENSE.md)
4
4
  [![Gem Version](https://badge.fury.io/rb/stack_master.svg)](https://badge.fury.io/rb/stack_master)
5
- [![Build Status](https://travis-ci.org/envato/stack_master.svg?branch=master)](https://travis-ci.org/envato/stack_master)
5
+ [![Build Status](https://github.com/envato/stack_master/workflows/tests/badge.svg?branch=master)](https://github.com/envato/stack_master/actions?query=workflow%3Atests+branch%3Amaster)
6
6
 
7
7
  StackMaster is a CLI tool to manage [CloudFormation](https://aws.amazon.com/cloudformation/) stacks, with the following features:
8
8
 
@@ -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.
@@ -141,7 +143,8 @@ stacks:
141
143
  ## Templates
142
144
 
143
145
  StackMaster supports CloudFormation templates in plain JSON or YAML. Any `.yml` or `.yaml` file will be processed as
144
- YAML, while any `.json` file will be processed as JSON.
146
+ YAML, while any `.json` file will be processed as JSON. Additionally, YAML files can be pre-processed using ERB and
147
+ compile-time parameters.
145
148
 
146
149
  ### Ruby DSLs
147
150
  By default, any template ending with `.rb` will be processed as a [SparkleFormation](https://github.com/sparkleformation/sparkle_formation)
@@ -155,7 +158,8 @@ template_compilers:
155
158
 
156
159
  ## Parameters
157
160
 
158
- Parameters are loaded from multiple YAML files, merged from the following lookup paths from bottom to top:
161
+ By default, parameters are loaded from multiple YAML files, merged from the
162
+ following lookup paths from bottom to top:
159
163
 
160
164
  - parameters/[stack_name].yaml
161
165
  - parameters/[stack_name].yml
@@ -166,18 +170,43 @@ Parameters are loaded from multiple YAML files, merged from the following lookup
166
170
 
167
171
  A simple parameter file could look like this:
168
172
 
169
- ```
173
+ ```yaml
170
174
  key_name: myapp-us-east-1
171
175
  ```
172
176
 
173
- ### Compile Time Parameters
177
+ Alternatively, a `parameter_files` array can be defined to explicitly list
178
+ parameter files that will be loaded. If `parameter_files` are defined, the
179
+ automatic search locations will not be used.
174
180
 
175
- Compile time parameters can be used for [SparkleFormation](http://www.sparkleformation.io) templates. It conforms and
176
- allows you to use the [Compile Time Parameters](http://www.sparkleformation.io/docs/sparkle_formation/compile-time-parameters.html) feature.
181
+ ```yaml
182
+ parameters_dir: parameters # the default
183
+ stacks:
184
+ us-east-1:
185
+ my-app:
186
+ parameter_files:
187
+ - my-app.yml # parameters/my-app.yml
188
+ ```
177
189
 
178
- A simple example looks like this
190
+ Parameters can also be defined inline with stack definitions:
179
191
 
192
+ ```yaml
193
+ stacks:
194
+ us-east-1:
195
+ my-app:
196
+ parameters:
197
+ VpcId:
198
+ stack_output: my-vpc/VpcId
180
199
  ```
200
+
201
+ ### Compile Time Parameters
202
+
203
+ Compile time parameters can be defined in a stack's parameters file, using the key `compile_time_parameters`. Keys in
204
+ parameter files are automatically converted to camel case.
205
+
206
+ As an example:
207
+
208
+ ```yaml
209
+ # parameters/some_stack.yml
181
210
  vpc_cidr: 10.0.0.0/16
182
211
  compile_time_parameters:
183
212
  subnet_cidrs:
@@ -185,7 +214,37 @@ compile_time_parameters:
185
214
  - 10.0.2.0/28
186
215
  ```
187
216
 
188
- Keys in parameter files are automatically converted to camel case.
217
+ #### SparkleFormation
218
+
219
+ Compile time parameters can be used for [SparkleFormation](http://www.sparkleformation.io) templates. It conforms and
220
+ allows you to use the [Compile Time Parameters](http://www.sparkleformation.io/docs/sparkle_formation/compile-time-parameters.html) feature.
221
+
222
+ #### CloudFormation YAML ERB
223
+
224
+ Compile time parameters can be used to pre-process YAML CloudFormation templates. An example template:
225
+
226
+ ```yaml
227
+ # templates/some_stack_template.yml.erb
228
+ Parameters:
229
+ VpcCidr:
230
+ Type: String
231
+ Resources:
232
+ Vpc:
233
+ Type: AWS::EC2::VPC
234
+ Properties:
235
+ CidrBlock: !Ref VpcCidr
236
+ # Given the two subnet_cidrs parameters, this creates two resources:
237
+ # SubnetPrivate0 with a CidrBlock of 10.0.0.0/28, and
238
+ # SubnetPrivate1 with a CidrBlock of 10.0.2.0/28
239
+ <% params["SubnetCidrs"].each_with_index do |cidr, index| %>
240
+ SubnetPrivate<%= index %>:
241
+ Type: AWS::EC2::Subnet
242
+ Properties:
243
+ VpcId: !Ref Vpc
244
+ AvailabilityZone: ap-southeast-2
245
+ CidrBlock: <%= cidr %>
246
+ <% end %>
247
+ ```
189
248
 
190
249
  ## Parameter Resolvers
191
250
 
@@ -286,8 +345,8 @@ db_password:
286
345
  An alternative to the secrets store is accessing 1password secrets using the 1password cli (`op`).
287
346
  You declare a 1password lookup with the following parameters in your parameters file:
288
347
 
289
- ```
290
- parameters/database.yml
348
+ ```yaml
349
+ # parameters/database.yml
291
350
  database_password:
292
351
  one_password:
293
352
  title: production database
@@ -477,7 +536,7 @@ name of the original resolver.
477
536
 
478
537
  When creating a new resolver, one can automatically create the array resolver by adding a `array_resolver` statement
479
538
  in the class definition, with an optional class name if different from the default one.
480
- ```
539
+ ```ruby
481
540
  module StackMaster
482
541
  module ParameterResolvers
483
542
  class MyResolver < Resolver
@@ -488,7 +547,7 @@ module StackMaster
488
547
  end
489
548
  ```
490
549
  In that example, using the array resolver would look like:
491
- ```
550
+ ```yaml
492
551
  my_parameter:
493
552
  my_custom_array_resolver:
494
553
  - value1
@@ -498,13 +557,13 @@ my_parameter:
498
557
  Array parameter values can include nested parameter resolvers.
499
558
 
500
559
  For example, given the following parameter definition:
501
- ```
560
+ ```yaml
502
561
  my_parameter:
503
562
  - stack_output: my-stack/output # value resolves to 'value1'
504
563
  - value2
505
564
  ```
506
565
  The parameter value will resolve to:
507
- ```
566
+ ```yaml
508
567
  my_parameter: 'value1,value2'
509
568
  ```
510
569
 
@@ -520,7 +579,7 @@ ROLE=<%= role %>
520
579
 
521
580
  And used like this in SparkleFormation templates:
522
581
 
523
- ```
582
+ ```ruby
524
583
  # templates/app.rb
525
584
  user_data user_data_file!('app.erb', role: :worker)
526
585
  ```
@@ -533,7 +592,7 @@ my_variable=<%= ref!(:foo) %>
533
592
  my_other_variable=<%= account_id! %>
534
593
  ```
535
594
 
536
- ```
595
+ ```ruby
537
596
  # templates/ecs_task.rb
538
597
  container_definitions array!(
539
598
  -> {
@@ -565,7 +624,7 @@ project-root
565
624
 
566
625
  Your env-1/stack_master.yml files can reference common templates by setting:
567
626
 
568
- ```
627
+ ```yaml
569
628
  template_dir: ../../sparkle/templates
570
629
  stack_defaults:
571
630
  compiler_options:
@@ -625,7 +684,7 @@ stacks:
625
684
 
626
685
  ## Allowed accounts
627
686
 
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.
687
+ 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
688
 
630
689
  This is an opt-in feature which is enabled by specifying at least one account to allow.
631
690
 
@@ -644,7 +703,7 @@ stacks:
644
703
  template: myapp_db.rb
645
704
  allowed_accounts: # only allow these accounts (overrides the stack defaults)
646
705
  - '1234567890'
647
- - '9876543210'
706
+ - my-account-alias
648
707
  tags:
649
708
  purpose: back-end
650
709
  myapp-web:
@@ -659,7 +718,8 @@ stacks:
659
718
  purpose: back-end
660
719
  ```
661
720
 
662
- In the cases where you want to bypass the account check, there is StackMaster flag `--skip-account-check` that can be used.
721
+ In the cases where you want to bypass the account check, there is the StackMaster flag `--skip-account-check` that can be used.
722
+
663
723
 
664
724
  ## Commands
665
725
 
@@ -674,15 +734,20 @@ stack_master apply # Create or update all stacks
674
734
  stack_master --changed apply # Create or update all stacks that have changed
675
735
  stack_master --yes apply [region-or-alias] [stack-name] # Create or update a stack non-interactively (forcing yes)
676
736
  stack_master diff [region-or-alias] [stack-name] # Display a stack template and parameter diff
737
+ stack_master drift [region-or-alias] [stack-name] # Detects and displays stack drift using the CloudFormation Drift API
677
738
  stack_master delete [region-or-alias] [stack-name] # Delete a stack
678
739
  stack_master events [region-or-alias] [stack-name] # Display events for a stack
679
740
  stack_master outputs [region-or-alias] [stack-name] # Display outputs for a stack
680
741
  stack_master resources [region-or-alias] [stack-name] # Display outputs for a stack
681
742
  stack_master status # Displays the status of each stack
682
743
  stack_master tidy # Find missing or extra templates or parameter files
744
+ stack_master compile # Print the compiled version of a given stack
745
+ stack_master validate # Validate a template
746
+ stack_master lint # Check the stack definition locally using cfn-lint
747
+ stack_master nag # Check the stack template with cfn_nag
683
748
  ```
684
749
 
685
- ## Applying updates
750
+ ## Applying updates - `stack_master apply`
686
751
 
687
752
  The apply command does the following:
688
753
 
@@ -699,6 +764,18 @@ Demo:
699
764
 
700
765
  ![Apply Demo](/apply_demo.gif?raw=true)
701
766
 
767
+ ## Drift Detection - `stack_master drift`
768
+
769
+ `stack_master drift us-east-1 mystack` uses the CloudFormation APIs to trigger drift detection and display resources
770
+ that have changed outside of the CloudFormation stack. This can happen if a resource has been updated via the console or
771
+ CLI directly rather than via a stack update.
772
+
773
+ ## Diff - `stack_master diff`
774
+
775
+ `stack_master diff us-east-1 mystack` displays whether the computed parameters or template differ to what was last
776
+ applied in CloudFormation. This can happen if the template or computed parameters have changed in code and the change
777
+ hasn't been applied to this stack.
778
+
702
779
  ## Maintainers
703
780
 
704
781
  - [Steve Hodgkiss](https://github.com/stevehodgkiss)
@@ -28,12 +28,15 @@ 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
 
35
38
  def cf
36
- @cf ||= Aws::CloudFormation::Client.new(region: region, retry_limit: 10)
39
+ @cf ||= Aws::CloudFormation::Client.new({ region: region, retry_limit: 10 })
37
40
  end
38
41
 
39
42
  end
@@ -17,10 +17,10 @@ module StackMaster
17
17
 
18
18
  s3 = new_s3_client(region: region)
19
19
 
20
- current_objects = s3.list_objects(
20
+ current_objects = s3.list_objects({
21
21
  prefix: prefix,
22
22
  bucket: bucket
23
- ).map(&:contents).flatten.inject({}){|h,obj|
23
+ }).map(&:contents).flatten.inject({}){|h,obj|
24
24
  h.merge(obj.key => obj)
25
25
  }
26
26
 
@@ -38,12 +38,12 @@ module StackMaster
38
38
  s3_uri = "s3://#{bucket}/#{object_key}"
39
39
  StackMaster.stdout.print "- #{File.basename(path)} => #{s3_uri} "
40
40
 
41
- s3.put_object(
41
+ s3.put_object({
42
42
  bucket: bucket,
43
43
  key: object_key,
44
44
  body: body,
45
45
  metadata: { md5: compiled_template_md5 }
46
- )
46
+ })
47
47
  StackMaster.stdout.puts "done."
48
48
  end
49
49
  end
@@ -61,7 +61,7 @@ module StackMaster
61
61
  private
62
62
 
63
63
  def new_s3_client(region: nil)
64
- Aws::S3::Client.new(region: region || @region)
64
+ Aws::S3::Client.new({ region: region || @region })
65
65
  end
66
66
  end
67
67
  end
@@ -25,12 +25,12 @@ module StackMaster
25
25
  end
26
26
 
27
27
  def self.delete(id)
28
- cf.delete_change_set(change_set_name: id)
28
+ cf.delete_change_set({ change_set_name: id })
29
29
  end
30
30
 
31
31
  def self.execute(id, stack_name)
32
- cf.execute_change_set(change_set_name: id,
33
- stack_name: stack_name)
32
+ cf.execute_change_set({ change_set_name: id,
33
+ stack_name: stack_name })
34
34
  end
35
35
 
36
36
  def self.cf
@@ -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)
@@ -7,7 +7,7 @@ module StackMaster
7
7
 
8
8
  def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
9
9
  @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
10
- Commander::Runner.instance_variable_set('@singleton', Commander::Runner.new(argv))
10
+ Commander::Runner.instance_variable_set('@instance', Commander::Runner.new(argv))
11
11
  StackMaster.stdout = @stdout
12
12
  StackMaster.stderr = @stderr
13
13
  TablePrint::Config.io = StackMaster.stdout
@@ -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,18 +141,29 @@ 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
147
148
 
149
+ command :nag do |c|
150
+ c.syntax = 'stack_master nag [region_or_alias] [stack_name]'
151
+ c.summary = "Check this stack's template with cfn_nag"
152
+ c.description = "Runs SAST scan cfn_nag on the template"
153
+ c.example 'run cfn_nag on stack myapp-vpc with us-east-1 settings', 'stack_master nag us-east-1 myapp-vpc'
154
+ c.action do |args, options|
155
+ options.default config: default_config_file
156
+ execute_stacks_command(StackMaster::Commands::Nag, args, options)
157
+ end
158
+ end
159
+
148
160
  command :compile do |c|
149
161
  c.syntax = 'stack_master compile [region_or_alias] [stack_name]'
150
162
  c.summary = "Print the compiled version of a given stack"
151
163
  c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS"
152
164
  c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc'
153
165
  c.action do |args, options|
154
- options.defaults config: default_config_file
166
+ options.default config: default_config_file
155
167
  execute_stacks_command(StackMaster::Commands::Compile, args, options)
156
168
  end
157
169
  end
@@ -162,7 +174,7 @@ module StackMaster
162
174
  c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.'
163
175
  c.example 'description', 'Check the status of all stack definitions'
164
176
  c.action do |args, options|
165
- options.defaults config: default_config_file
177
+ options.default config: default_config_file
166
178
  say "Invalid arguments. stack_master status" and return unless args.size == 0
167
179
  config = load_config(options.config)
168
180
  StackMaster::Commands::Status.perform(config, nil, options)
@@ -175,7 +187,7 @@ module StackMaster
175
187
  c.description = 'Cross references stack_master.yml with the template and parameter directories to identify extra or missing files.'
176
188
  c.example 'description', 'Check for missing or extra files'
177
189
  c.action do |args, options|
178
- options.defaults config: default_config_file
190
+ options.default config: default_config_file
179
191
  say "Invalid arguments. stack_master tidy" and return unless args.size == 0
180
192
  config = load_config(options.config)
181
193
  StackMaster::Commands::Tidy.perform(config, nil, options)
@@ -214,6 +226,18 @@ module StackMaster
214
226
  end
215
227
  end
216
228
 
229
+ command :drift do |c|
230
+ c.syntax = 'stack_master drift [region_or_alias] [stack_name]'
231
+ c.summary = 'Detects and displays stack drift using the CloudFormation Drift API'
232
+ c.description = 'Detects and displays stack drift'
233
+ c.option '--timeout SECONDS', Integer, "The number of seconds to wait for drift detection to complete"
234
+ c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
235
+ c.action do |args, options|
236
+ options.default config: default_config_file, timeout: 120
237
+ execute_stacks_command(StackMaster::Commands::Drift, args, options)
238
+ end
239
+ end
240
+
217
241
  run!
218
242
  end
219
243
 
@@ -241,6 +265,7 @@ module StackMaster
241
265
  stack_definitions = config.filter(region, stack_name)
242
266
  if stack_definitions.empty?
243
267
  StackMaster.stdout.puts "Could not find stack definition #{stack_name} in region #{region}"
268
+ show_other_region_candidates(config, stack_name)
244
269
  success = false
245
270
  end
246
271
  stack_definitions = stack_definitions.select do |stack_definition|
@@ -257,18 +282,27 @@ module StackMaster
257
282
  @kernel.exit false unless success
258
283
  end
259
284
 
285
+ def show_other_region_candidates(config, stack_name)
286
+ candidates = config.filter(region="", stack_name=stack_name)
287
+ return if candidates.empty?
288
+
289
+ StackMaster.stdout.puts "Stack name #{stack_name} exists in regions: #{candidates.map(&:region).join(', ')}"
290
+ end
291
+
260
292
  def execute_if_allowed_account(allowed_accounts, &block)
261
293
  raise ArgumentError, "Block required to execute this method" unless block_given?
262
294
  if running_in_allowed_account?(allowed_accounts)
263
295
  block.call
264
296
  else
265
- StackMaster.stdout.puts "Account '#{identity.account}' is not an allowed account. Allowed accounts are #{allowed_accounts}."
297
+ account_text = "'#{identity.account}'"
298
+ account_text << " (#{identity.account_aliases.join(', ')})" if identity.account_aliases.any?
299
+ StackMaster.stdout.puts "Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
266
300
  false
267
301
  end
268
302
  end
269
303
 
270
304
  def running_in_allowed_account?(allowed_accounts)
271
- StackMaster.skip_account_check? || identity.running_in_allowed_account?(allowed_accounts)
305
+ StackMaster.skip_account_check? || identity.running_in_account?(allowed_accounts)
272
306
  end
273
307
 
274
308
  def identity
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erubis'
4
+
5
+ module StackMaster
6
+ # This class is a modified version of `Erubis::Eruby`. It allows using
7
+ # `<%= %>` ERB expressions to interpolate values into a source string. We use
8
+ # this capability to enrich user data scripts with data and parameters pulled
9
+ # from the AWS CloudFormation service. The evaluation produces an array of
10
+ # objects ready for use in a CloudFormation `Fn::Join` intrinsic function.
11
+ class CloudFormationInterpolatingEruby < Erubis::Eruby
12
+ include Erubis::ArrayEnhancer
13
+
14
+ # Load a template from a file at the specified path and evaluate it.
15
+ def self.evaluate_file(source_path, context = Erubis::Context.new)
16
+ template_contents = File.read(source_path)
17
+ eruby = new(template_contents)
18
+ eruby.filename = source_path
19
+ eruby.evaluate(context)
20
+ end
21
+
22
+ # @return [Array] The result of evaluating the source: an array of strings
23
+ # from the source intermindled with Hash objects from the ERB
24
+ # expressions. To be included in a CloudFormation template, this
25
+ # value needs to be used in a CloudFormation `Fn::Join` intrinsic
26
+ # function.
27
+ # @see Erubis::Eruby#evaluate
28
+ # @example
29
+ # CloudFormationInterpolatingEruby.new("my_variable=<%= { 'Ref' => 'Param1' } %>;").evaluate
30
+ # #=> ['my_variable=', { 'Ref' => 'Param1' }, ';']
31
+ def evaluate(_context = Erubis::Context.new)
32
+ format_lines_for_cloudformation(super)
33
+ end
34
+
35
+ # @see Erubis::Eruby#add_expr
36
+ def add_expr(src, code, indicator)
37
+ if indicator == '='
38
+ src << " #{@bufvar} << (" << code << ');'
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Split up long strings containing multiple lines. One string per line in the
47
+ # CloudFormation array makes the compiled template and diffs more readable.
48
+ def format_lines_for_cloudformation(source)
49
+ source.flat_map do |lines|
50
+ lines = lines.to_s if lines.is_a?(Symbol)
51
+ next(lines) unless lines.is_a?(String)
52
+
53
+ lines.scan(/[^\n]*\n?/).reject { |x| x == '' }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erubis'
4
+ require 'json'
5
+
6
+ module StackMaster
7
+ # This class is a modified version of `Erubis::Eruby`. It provides extra
8
+ # helper methods to ease the dynamic creation of CloudFormation templates
9
+ # with ERB. These helper methods are available within `<%= %>` expressions.
10
+ class CloudFormationTemplateEruby < Erubis::Eruby
11
+ # Adds the contents of an EC2 userdata script to the CloudFormation
12
+ # template. Allows using the ERB `<%= %>` expressions within the user data
13
+ # script to interpolate CloudFormation values.
14
+ def user_data_file(filepath)
15
+ JSON.pretty_generate({ 'Fn::Base64' => { 'Fn::Join' => ['', user_data_file_as_lines(filepath)] } })
16
+ end
17
+
18
+ # Evaluate the ERB template at the specified filepath and return the result
19
+ # as an array of lines. Allows using ERB `<%= %>` expressions to interpolate
20
+ # CloudFormation objects into the result.
21
+ def user_data_file_as_lines(filepath)
22
+ StackMaster::CloudFormationInterpolatingEruby.evaluate_file(filepath, self)
23
+ end
24
+
25
+ # Add the contents of another file into the CloudFormation template as a
26
+ # string. ERB `<%= %>` expressions within the referenced file are not
27
+ # evaluated.
28
+ def include_file(filepath)
29
+ JSON.pretty_generate(File.read(filepath))
30
+ end
31
+ end
32
+ end