stack_master 1.13.1 → 1.14.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: 3c61a62b352e2b92b35889b4adbf9f723c65b815cbb04c81f9716cc10000d220
4
- data.tar.gz: 02f8827f12a3f1321c4f51984a23f23e66d1ad5c82e28612c56e64b8c7553cbc
3
+ metadata.gz: c6272dd327e82ec747c26129191c4c8e262a4350d6634941f14035746399cb77
4
+ data.tar.gz: 6dc8058810bee48a3aed457669ffdda94e9d88fdc2ab1e1c12b9a689a3799c46
5
5
  SHA512:
6
- metadata.gz: c0990e86ffb2dd1d8c745a74b2e39c24adfb430fc7678f647ab88bdd6153bbf45a9aa0bd744f00c3a527165bee5ce6b5f6980c161403f86950e0737ab52f4dd9
7
- data.tar.gz: d0941fc85e852593683bfd976cbc5e872c260ab9e5f297938dbacfb16ec1c73b7c56e52eebb4e7b4f897c423383961fa6b4768b7361bb2e6ff1dded2a4d2dc0e
6
+ metadata.gz: 6dad4bcffbc40f9a45377625ef7769f122b5880bbcc61bead8a9ffdd75ee063001f322f9e62079aa00b370f791afd2f76c7acb9b668f6d4f7f41128c63954b65
7
+ data.tar.gz: a12c14c9024e187a5b734020cda2adc1c40d7983f58450022ea5f2301412968a167f4ebd6233f02097b8341541dc5ae50b213b6b59730f540594df90b327fd5f
data/README.md CHANGED
@@ -31,10 +31,18 @@ etc.
31
31
 
32
32
  ## Installation
33
33
 
34
- System-wide: `gem install stack_master`
34
+ ### System-wide
35
35
 
36
- With bundler:
36
+ ```shell
37
+ gem install stack_master
37
38
 
39
+ # if you want linting capabilities:
40
+ pip install cfn-lint
41
+ ```
42
+
43
+ ### Bundler
44
+
45
+ - `pip install cfn-lint` if you need lint functionality
38
46
  - Add `gem 'stack_master'` to your Gemfile.
39
47
  - Run `bundle install`
40
48
  - Run `bundle exec stack_master init` to generate a directory structure and stack_master.yml file
@@ -83,10 +91,14 @@ stacks:
83
91
  staging:
84
92
  myapp-vpc:
85
93
  template: myapp_vpc.rb
94
+ allowed_accounts: '123456789'
86
95
  tags:
87
96
  purpose: front-end
88
97
  myapp-db:
89
98
  template: myapp_db.rb
99
+ allowed_accounts:
100
+ - '1234567890'
101
+ - '9876543210'
90
102
  tags:
91
103
  purpose: back-end
92
104
  myapp-web:
@@ -537,6 +549,44 @@ end
537
549
 
538
550
  Note though that if a dynamic with the same name exists in your `templates/dynamics/` directory it will get loaded since it has higher precedence.
539
551
 
552
+ ## Allowed accounts
553
+
554
+ 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.
555
+
556
+ This is an opt-in feature which is enabled by specifying at least one account to allow.
557
+
558
+ Unlike other stack defaults, the `allowed_accounts` property values specified in the stack definition override values specified in the stack defaults (i.e., other stack property values are merged together with those specified in the stack defaults). This allows specifying allowed accounts in the stack defaults (inherited by all stacks) and override them for specific stacks. See below example config for an example.
559
+
560
+ ```yaml
561
+ stack_defaults:
562
+ allowed_accounts: '555555555'
563
+ stacks:
564
+ us-east-1:
565
+ myapp-vpc: # only allow account 555555555 (inherited from the stack defaults)
566
+ template: myapp_vpc.rb
567
+ tags:
568
+ purpose: front-end
569
+ myapp-db:
570
+ template: myapp_db.rb
571
+ allowed_accounts: # only allow these accounts (overrides the stack defaults)
572
+ - '1234567890'
573
+ - '9876543210'
574
+ tags:
575
+ purpose: back-end
576
+ myapp-web:
577
+ template: myapp_web.rb
578
+ allowed_accounts: [] # allow all accounts (overrides the stack defaults)
579
+ tags:
580
+ purpose: front-end
581
+ myapp-redis:
582
+ template: myapp_redis.rb
583
+ allowed_accounts: '888888888' # only allow this account (overrides the stack defaults)
584
+ tags:
585
+ purpose: back-end
586
+ ```
587
+
588
+ In the cases where you want to bypass the account check, there is StackMaster flag `--skip-account-check` that can be used.
589
+
540
590
  ## Commands
541
591
 
542
592
  ```bash
@@ -34,6 +34,9 @@ module StackMaster
34
34
  global_option '-q', '--quiet', 'Do not output the resulting Stack Events, just return immediately' do
35
35
  StackMaster.quiet!
36
36
  end
37
+ global_option '--skip-account-check', 'Do not check if command is allowed to execute in account' do
38
+ StackMaster.skip_account_check!
39
+ end
37
40
 
38
41
  command :apply do |c|
39
42
  c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
@@ -178,18 +181,22 @@ module StackMaster
178
181
  return
179
182
  end
180
183
 
184
+ stack_name = Utils.underscore_to_hyphen(args[1])
185
+ allowed_accounts = []
186
+
181
187
  # Because delete can work without a stack_master.yml
182
188
  if options.config and File.file?(options.config)
183
189
  config = load_config(options.config)
184
190
  region = Utils.underscore_to_hyphen(config.unalias_region(args[0]))
191
+ allowed_accounts = config.find_stack(region, stack_name)&.allowed_accounts
185
192
  else
186
193
  region = args[0]
187
194
  end
188
195
 
189
- stack_name = Utils.underscore_to_hyphen(args[1])
190
-
191
- StackMaster.cloud_formation_driver.set_region(region)
192
- StackMaster::Commands::Delete.perform(region, stack_name)
196
+ execute_if_allowed_account(allowed_accounts) do
197
+ StackMaster.cloud_formation_driver.set_region(region)
198
+ StackMaster::Commands::Delete.perform(region, stack_name)
199
+ end
193
200
  end
194
201
  end
195
202
 
@@ -223,15 +230,35 @@ module StackMaster
223
230
  success = false
224
231
  end
225
232
  stack_definitions = stack_definitions.select do |stack_definition|
226
- StackStatus.new(config, stack_definition).changed?
233
+ running_in_allowed_account?(stack_definition.allowed_accounts) && StackStatus.new(config, stack_definition).changed?
227
234
  end if options.changed
228
235
  stack_definitions.each do |stack_definition|
229
236
  StackMaster.cloud_formation_driver.set_region(stack_definition.region)
230
237
  StackMaster.stdout.puts "Executing #{command.command_name} on #{stack_definition.stack_name} in #{stack_definition.region}"
231
- success = false unless command.perform(config, stack_definition, options).success?
238
+ success = execute_if_allowed_account(stack_definition.allowed_accounts) do
239
+ command.perform(config, stack_definition, options).success?
240
+ end
232
241
  end
233
242
  end
234
243
  success
235
244
  end
245
+
246
+ def execute_if_allowed_account(allowed_accounts, &block)
247
+ raise ArgumentError, "Block required to execute this method" unless block_given?
248
+ if running_in_allowed_account?(allowed_accounts)
249
+ block.call
250
+ else
251
+ StackMaster.stdout.puts "Account '#{identity.account}' is not an allowed account. Allowed accounts are #{allowed_accounts}."
252
+ false
253
+ end
254
+ end
255
+
256
+ def running_in_allowed_account?(allowed_accounts)
257
+ StackMaster.skip_account_check? || identity.running_in_allowed_account?(allowed_accounts)
258
+ end
259
+
260
+ def identity
261
+ @identity ||= StackMaster::Identity.new
262
+ end
236
263
  end
237
264
  end
@@ -13,7 +13,11 @@ module StackMaster
13
13
 
14
14
  def perform
15
15
  unless cfn_lint_available
16
- failed! "Failed to run cfn-lint, do you have it installed and available in $PATH?"
16
+ failed! 'Failed to run cfn-lint. You may need to install it using'\
17
+ '`pip install cfn-lint`, or add it to $PATH.'\
18
+ "\n"\
19
+ '(See https://github.com/aws-cloudformation/cfn-python-lint'\
20
+ ' for package information)'
17
21
  end
18
22
 
19
23
  Tempfile.open(['stack', ".#{proposed_stack.template_format}"]) do |f|
@@ -16,12 +16,13 @@ module StackMaster
16
16
  progress if @show_progress
17
17
  status = @config.stacks.map do |stack_definition|
18
18
  stack_status = StackStatus.new(@config, stack_definition)
19
+ allowed_accounts = stack_definition.allowed_accounts
19
20
  progress.increment if @show_progress
20
21
  {
21
22
  region: stack_definition.region,
22
23
  stack_name: stack_definition.stack_name,
23
- stack_status: stack_status.status,
24
- different: stack_status.changed_message,
24
+ stack_status: running_in_allowed_account?(allowed_accounts) ? stack_status.status : "Disallowed account",
25
+ different: running_in_allowed_account?(allowed_accounts) ? stack_status.changed_message : "N/A",
25
26
  }
26
27
  end
27
28
  tp.set :max_width, self.window_size
@@ -41,6 +42,14 @@ module StackMaster
41
42
  def sort_params(hash)
42
43
  hash.sort.to_h
43
44
  end
45
+
46
+ def running_in_allowed_account?(allowed_accounts)
47
+ StackMaster.skip_account_check? || identity.running_in_allowed_account?(allowed_accounts)
48
+ end
49
+
50
+ def identity
51
+ @identity ||= StackMaster::Identity.new
52
+ end
44
53
  end
45
54
  end
46
55
  end
@@ -116,6 +116,7 @@ module StackMaster
116
116
  'base_dir' => @base_dir,
117
117
  'template_dir' => @template_dir,
118
118
  'additional_parameter_lookup_dirs' => @region_to_aliases[region])
119
+ stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts']
119
120
  @stacks << StackDefinition.new(stack_attributes)
120
121
  end
121
122
  end
@@ -0,0 +1,23 @@
1
+ module StackMaster
2
+ class Identity
3
+ def running_in_allowed_account?(allowed_accounts)
4
+ allowed_accounts.nil? || allowed_accounts.empty? || allowed_accounts.include?(account)
5
+ end
6
+
7
+ def account
8
+ @account ||= sts.get_caller_identity.account
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :sts
14
+
15
+ def region
16
+ @region ||= ENV['AWS_REGION'] || Aws.config[:region] || Aws.shared_config.region || 'us-east-1'
17
+ end
18
+
19
+ def sts
20
+ @sts ||= Aws::STS::Client.new(region: region)
21
+ end
22
+ end
23
+ end
@@ -5,6 +5,7 @@ module StackMaster
5
5
  :template,
6
6
  :tags,
7
7
  :role_arn,
8
+ :allowed_accounts,
8
9
  :notification_arns,
9
10
  :base_dir,
10
11
  :template_dir,
@@ -23,8 +24,10 @@ module StackMaster
23
24
  @notification_arns = []
24
25
  @s3 = {}
25
26
  @files = []
27
+ @allowed_accounts = nil
26
28
  super
27
29
  @template_dir ||= File.join(@base_dir, 'templates')
30
+ @allowed_accounts = Array(@allowed_accounts)
28
31
  end
29
32
 
30
33
  def ==(other)
@@ -34,6 +37,7 @@ module StackMaster
34
37
  @template == other.template &&
35
38
  @tags == other.tags &&
36
39
  @role_arn == other.role_arn &&
40
+ @allowed_accounts == other.allowed_accounts &&
37
41
  @notification_arns == other.notification_arns &&
38
42
  @base_dir == other.base_dir &&
39
43
  @secret_file == other.secret_file &&
@@ -1,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "1.13.1"
2
+ VERSION = "1.14.0"
3
3
  end
data/lib/stack_master.rb CHANGED
@@ -38,6 +38,7 @@ module StackMaster
38
38
  autoload :PagedResponseAccumulator, 'stack_master/paged_response_accumulator'
39
39
  autoload :StackDefinition, 'stack_master/stack_definition'
40
40
  autoload :TemplateCompiler, 'stack_master/template_compiler'
41
+ autoload :Identity, 'stack_master/identity'
41
42
 
42
43
  autoload :StackDiffer, 'stack_master/stack_differ'
43
44
  autoload :Validator, 'stack_master/validator'
@@ -97,6 +98,7 @@ module StackMaster
97
98
  NON_INTERACTIVE_DEFAULT = false
98
99
  DEBUG_DEFAULT = false
99
100
  QUIET_DEFAULT = false
101
+ SKIP_ACCOUNT_CHECK_DEFAULT = false
100
102
 
101
103
  def interactive?
102
104
  !non_interactive?
@@ -136,6 +138,16 @@ module StackMaster
136
138
 
137
139
  def reset_flags
138
140
  @quiet = QUIET_DEFAULT
141
+ @skip_account_check = SKIP_ACCOUNT_CHECK_DEFAULT
142
+ end
143
+
144
+ def skip_account_check!
145
+ @skip_account_check = true
146
+ end
147
+ @skip_account_check = SKIP_ACCOUNT_CHECK_DEFAULT
148
+
149
+ def skip_account_check?
150
+ @skip_account_check
139
151
  end
140
152
 
141
153
  attr_accessor :non_interactive_answer
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: 1.13.1
4
+ version: 1.14.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: 2019-03-20 00:00:00.000000000 Z
12
+ date: 2019-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -417,7 +417,6 @@ files:
417
417
  - lib/stack_master.rb
418
418
  - lib/stack_master/aws_driver/cloud_formation.rb
419
419
  - lib/stack_master/aws_driver/s3.rb
420
- - lib/stack_master/aws_driver/sts.rb
421
420
  - lib/stack_master/change_set.rb
422
421
  - lib/stack_master/cli.rb
423
422
  - lib/stack_master/command.rb
@@ -436,6 +435,7 @@ files:
436
435
  - lib/stack_master/commands/validate.rb
437
436
  - lib/stack_master/config.rb
438
437
  - lib/stack_master/ctrl_c.rb
438
+ - lib/stack_master/identity.rb
439
439
  - lib/stack_master/paged_response_accumulator.rb
440
440
  - lib/stack_master/parameter_loader.rb
441
441
  - lib/stack_master/parameter_resolver.rb
@@ -479,7 +479,6 @@ files:
479
479
  - lib/stack_master/stack_events/streamer.rb
480
480
  - lib/stack_master/stack_states.rb
481
481
  - lib/stack_master/stack_status.rb
482
- - lib/stack_master/target_definition.rb
483
482
  - lib/stack_master/template_compiler.rb
484
483
  - lib/stack_master/template_compilers/cfndsl.rb
485
484
  - lib/stack_master/template_compilers/json.rb
@@ -488,7 +487,6 @@ files:
488
487
  - lib/stack_master/template_utils.rb
489
488
  - lib/stack_master/test_driver/cloud_formation.rb
490
489
  - lib/stack_master/test_driver/s3.rb
491
- - lib/stack_master/test_driver/sts.rb
492
490
  - lib/stack_master/testing.rb
493
491
  - lib/stack_master/utils.rb
494
492
  - lib/stack_master/validator.rb
@@ -516,7 +514,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
516
514
  - !ruby/object:Gem::Version
517
515
  version: '0'
518
516
  requirements: []
519
- rubygems_version: 3.0.3
517
+ rubyforge_project:
518
+ rubygems_version: 2.7.6
520
519
  signing_key:
521
520
  specification_version: 4
522
521
  summary: StackMaster is a sure-footed way of creating, updating and keeping track
@@ -1,22 +0,0 @@
1
- module StackMaster
2
- module AwsDriver
3
- class STS
4
- def set_region(region)
5
- @region = region
6
- end
7
-
8
- def get_account_id
9
- sts = new_sts_client
10
- # This should always work as you cannot allow/deny access to this API
11
- identity = sts.get_caller_identity
12
- return identity['account']
13
- end
14
-
15
- private
16
-
17
- def new_sts_client
18
- Aws::STS::Client.new(region: @region)
19
- end
20
- end
21
- end
22
- end
@@ -1,13 +0,0 @@
1
- module StackMaster
2
- class TargetDefinition
3
- attr_reader :region,
4
- :account_id,
5
- :stack_definition
6
-
7
- def initialize(region:, account_id:, stack_definition:)
8
- @region = region
9
- @account_id = account_id
10
- @stack_definition = stack_definition
11
- end
12
- end
13
- end
@@ -1,22 +0,0 @@
1
- module StackMaster
2
- module TestDriver
3
- class STS
4
- attr_accessor :account_id
5
- def initialize
6
- reset
7
- end
8
-
9
- def reset
10
- @account_id = "12345678910"
11
- end
12
-
13
- def set_region(region)
14
- @region = region
15
- end
16
-
17
- def get_account_id
18
- @account_id
19
- end
20
- end
21
- end
22
- end