stack_master 0.13.0 → 0.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
  SHA1:
3
- metadata.gz: decf1cea6cb689ff5f5df562e7393715a97c9a59
4
- data.tar.gz: 6b2b11e73212a5e154f50a7350618b1e5f3a31a0
3
+ metadata.gz: 8693e4d791f64b4704d3e5b068d0e3a77fa8c6a6
4
+ data.tar.gz: 69c0fcb456e40b8dc933ea73697bc194b65e20ea
5
5
  SHA512:
6
- metadata.gz: 663b7860af6120f09ebed47d50374a51b09d4b4f9f3981b7753f1876fbcefa082b62d5dc99a7ebdeee78a2b85c0f23bcb088a5619e4a69284b08a2574547d537
7
- data.tar.gz: 65f77fc48be1584005cfc7292f07815a7f575b5b63cda9c6334572668bedfa9097f50ec5386c4aa25b642acd3e244b56a793bfc3e45ec675a1cf6a749457ed61
6
+ metadata.gz: 6805b7c93f992d0d83f6728732bf58001e5ebdda5bc2cb59d4438ede7e8ef725feb29f77512b5e3d673210405f7e869d197443b77545c602564f0704efe515ed
7
+ data.tar.gz: 42bc841676942028c0f8cc59206e5d7ca3dfb8c38301aa52e543c921eff25bd3afd97f0ba2e1a02bbf386e4d81d34684aa3c71e9e32390e69fa8cc35abb00bbd
data/README.md CHANGED
@@ -168,11 +168,20 @@ region/account, even though the resolved values will be different.
168
168
  ### Stack Output
169
169
 
170
170
  The stack output parameter resolver looks up outputs from other stacks in the
171
- same region. The expected format is `[stack-name]/[OutputName]`.
171
+ same or different region. The expected format is `[(region|region-alias):]stack-name/(OutputName|output_name)`.
172
172
 
173
173
  ```yaml
174
174
  vpc_id:
175
+ # Output from a stack in the same region
175
176
  stack_output: my-vpc-stack/VpcId
177
+
178
+ bucket_name:
179
+ # Output from a stack in a different region
180
+ stack_output: us-east-1:init-bucket/bucket_name
181
+
182
+ zone_name:
183
+ # Output from a stack in a different region using its alias
184
+ stack_output: global:hosted-zone/ZoneName
176
185
  ```
177
186
 
178
187
  This is the most used parameter resolver because it enables stacks to be split
@@ -307,7 +316,7 @@ vpc_id:
307
316
 
308
317
  Most resolvers support taking an array of values that will each be resolved.
309
318
  Unless stated otherwise in the documentation, the array version of the
310
- resolver will be named with the [pluralized](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-pluralize)
319
+ resolver will be named with the [pluralized](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-pluralize)
311
320
  name of the original resolver.
312
321
 
313
322
  When creating a new resolver, one can automatically create the array resolver by adding a `array_resolver` statement
@@ -3,9 +3,15 @@ module StackMaster
3
3
  class CloudFormation
4
4
  extend Forwardable
5
5
 
6
- def set_region(region)
7
- @region = region
8
- @cf = nil
6
+ def region
7
+ @region ||= ENV['AWS_REGION'] || Aws.config[:region] || Aws.shared_config.region
8
+ end
9
+
10
+ def set_region(value)
11
+ if region != value
12
+ @region = value
13
+ @cf = nil
14
+ end
9
15
  end
10
16
 
11
17
  def_delegators :cf, :create_change_set,
@@ -19,42 +25,16 @@ module StackMaster
19
25
  :get_stack_policy,
20
26
  :describe_stack_events,
21
27
  :update_stack,
22
- :create_stack
23
-
24
- def describe_stacks(options)
25
- retry_with_backoff do
26
- cf.describe_stacks(options)
27
- end
28
- end
29
-
30
- def validate_template(options)
31
- retry_with_backoff do
32
- cf.validate_template(options)
33
- end
34
- end
28
+ :create_stack,
29
+ :validate_template,
30
+ :describe_stacks
35
31
 
36
32
  private
37
33
 
38
34
  def cf
39
- @cf ||= Aws::CloudFormation::Client.new(region: @region)
35
+ @cf ||= Aws::CloudFormation::Client.new(region: region, retry_limit: 10)
40
36
  end
41
37
 
42
- def retry_with_backoff
43
- delay = 1
44
- max_delay = 30
45
- begin
46
- yield
47
- rescue Aws::CloudFormation::Errors::Throttling => e
48
- if e.message =~ /Rate exceeded/
49
- sleep delay
50
- delay *= 2
51
- if delay > max_delay
52
- delay = max_delay
53
- end
54
- retry
55
- end
56
- end
57
- end
58
38
  end
59
39
  end
60
40
  end
@@ -10,14 +10,15 @@ module StackMaster
10
10
  @config = config
11
11
  @stack_definition = stack_definition
12
12
  @stacks = {}
13
+ @cf_drivers = {}
14
+ @output_regex = %r{(?:(?<region>[^:]+):)?(?<stack_name>[^:/]+)/(?<output_name>.+)}
13
15
  end
14
16
 
15
17
  def resolve(value)
16
- validate_value!(value)
17
- stack_name, output_name = value.split('/')
18
- stack = find_stack(stack_name)
18
+ region, stack_name, output_name = parse!(value)
19
+ stack = find_stack(stack_name, region)
19
20
  if stack
20
- output = stack.outputs.find { |output| output.output_key == output_name.camelize }
21
+ output = stack.outputs.find { |stack_output| stack_output.output_key == output_name.camelize }
21
22
  if output
22
23
  output.output_value
23
24
  else
@@ -34,21 +35,41 @@ module StackMaster
34
35
  @cf ||= StackMaster.cloud_formation_driver
35
36
  end
36
37
 
37
- def validate_value!(value)
38
- if !value.is_a?(String) || !value.include?('/')
39
- raise ArgumentError, 'Stack output values must be in the form of stack-name/output-name'
38
+ def parse!(value)
39
+ if !value.is_a?(String) || !(match = @output_regex.match(value))
40
+ raise ArgumentError, 'Stack output values must be in the form of [region:]stack-name/output_name'
40
41
  end
42
+
43
+ [
44
+ match[:region] || cf.region,
45
+ match[:stack_name],
46
+ match[:output_name]
47
+ ]
41
48
  end
42
49
 
43
- def find_stack(stack_name)
44
- @stacks.fetch(stack_name) do
45
- cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
46
- @stacks[stack_name] = cf_stack
50
+ def find_stack(stack_name, region)
51
+ unaliased_region = @config.unalias_region(region)
52
+ stack_key = stack_key(stack_name, unaliased_region)
53
+
54
+ @stacks.fetch(stack_key) do
55
+ regional_cf = cf_for_region(unaliased_region)
56
+ cf_stack = regional_cf.describe_stacks(stack_name: stack_name).stacks.first
57
+ @stacks[stack_key] = cf_stack
47
58
  end
48
59
  end
49
60
 
50
- def cf
51
- @cf ||= StackMaster.cloud_formation_driver
61
+ def stack_key(stack_name, region)
62
+ "#{region}:#{stack_name}"
63
+ end
64
+
65
+ def cf_for_region(region)
66
+ return cf if cf.region == region
67
+
68
+ @cf_drivers.fetch(region) do
69
+ cloud_formation_driver = cf.class.new
70
+ cloud_formation_driver.set_region(region)
71
+ @cf_drivers[region] = cloud_formation_driver
72
+ end
52
73
  end
53
74
  end
54
75
  end
@@ -65,6 +65,10 @@ module StackMaster
65
65
  reset
66
66
  end
67
67
 
68
+ def region
69
+ @region ||= ENV['AWS_REGION'] || Aws.config[:region] || Aws.shared_config.region
70
+ end
71
+
68
72
  def set_region(region)
69
73
  @region = region
70
74
  end
@@ -1,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "0.13.0"
2
+ VERSION = "0.14.0"
3
3
  end
@@ -7,7 +7,7 @@ RSpec.describe StackMaster::Commands::Delete do
7
7
 
8
8
  before do
9
9
  StackMaster.cloud_formation_driver.set_region(region)
10
- allow(Aws::CloudFormation::Client).to receive(:new).with(region: region).and_return(cf)
10
+ allow(Aws::CloudFormation::Client).to receive(:new).with(region: region, retry_limit: 10).and_return(cf)
11
11
  allow(delete).to receive(:ask?).and_return('y')
12
12
  allow(StackMaster::StackEvents::Streamer).to receive(:stream)
13
13
  end
@@ -41,23 +41,4 @@ RSpec.describe StackMaster::Commands::Status do
41
41
  end
42
42
  end
43
43
 
44
- context "handles AWS throttling" do
45
- let(:throttle_exception) { Aws::CloudFormation::Errors::Throttling.new(double(), "Rate exceeded.") }
46
- let(:stack1) { double(:stack1, template_hash: {}, parameters_with_defaults: {a: 1}, stack_status: 'UPDATE_COMPLETE') }
47
- let(:stack2) { double(:stack2, template_hash: {}, parameters_with_defaults: {a: 2}, stack_status: 'CREATE_COMPLETE') }
48
- let(:proposed_stack1) { double(:proposed_stack1, template_body: "{}", parameters_with_defaults: {a: 1}) }
49
- let(:proposed_stack2) { double(:proposed_stack2, template_body: "{}", parameters_with_defaults: {a: 1}) }
50
-
51
- it "doubles the sleep time across calls" do
52
- call_count = 0
53
- expect(cf).to receive(:describe_stacks).at_least(1).times do
54
- call_count += 1
55
- call_count <= 3 ? raise(throttle_exception) : double(stacks: double(first: nil))
56
- end
57
- expect(StackMaster.cloud_formation_driver).to receive(:sleep).with(1).ordered
58
- expect(StackMaster.cloud_formation_driver).to receive(:sleep).with(2).ordered
59
- expect(StackMaster.cloud_formation_driver).to receive(:sleep).with(4).ordered
60
- expect { status.perform }.to_not raise_exception
61
- end
62
- end
63
44
  end
@@ -1,9 +1,9 @@
1
1
  RSpec.describe StackMaster::ParameterResolvers::StackOutput do
2
2
  let(:region) { 'us-east-1' }
3
3
  let(:stack_name) { 'my-stack' }
4
- let(:config) { double }
5
4
  let(:resolver) { described_class.new(config, double(region: 'us-east-1')) }
6
5
  let(:cf) { Aws::CloudFormation::Client.new }
6
+ let(:config) { double(:unalias_region => region) }
7
7
 
8
8
  def resolve(value)
9
9
  resolver.resolve(value)
@@ -74,4 +74,54 @@ RSpec.describe StackMaster::ParameterResolvers::StackOutput do
74
74
  end
75
75
  end
76
76
  end
77
+
78
+ context 'when given a valid string value including region' do
79
+ let(:value) { 'us-east-1:my-stack/MyOutput' }
80
+ let(:stacks) { [{ stack_name: 'my-stack', creation_time: Time.now, stack_status: 'CREATE_COMPLETE', outputs: outputs}] }
81
+ let(:outputs) { [] }
82
+
83
+ before do
84
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf)
85
+ cf.stub_responses(:describe_stacks, stacks: stacks)
86
+ end
87
+
88
+ context 'the stack and output exist' do
89
+ let(:outputs) { [{output_key: 'MyOutput', output_value: 'myresolvedvalue'}] }
90
+
91
+ it 'resolves the value' do
92
+ expect(resolved_value).to eq 'myresolvedvalue'
93
+ end
94
+
95
+ context 'the stack and output exist in a different region with the same name' do
96
+ let(:value_in_region_alias) { 'global:my-stack/MyOutput' }
97
+ let(:value_in_region_2) { 'ap-southeast-2:my-stack/MyOutput' }
98
+ let(:outputs_in_region_2) { [{output_key: 'MyOutput', output_value: 'myresolvedvalue2'}] }
99
+ let(:stacks_in_region_2) { [{ stack_name: 'my-stack', creation_time: Time.now, stack_status: 'CREATE_COMPLETE', outputs: outputs_in_region_2}] }
100
+
101
+ before do
102
+ cf.stub_responses(
103
+ :describe_stacks,
104
+ { stacks: stacks },
105
+ { stacks: stacks_in_region_2 }
106
+ )
107
+ allow(config).to receive(:unalias_region) do |aliased_region|
108
+ if aliased_region == 'global'
109
+ 'us-east-1'
110
+ else
111
+ aliased_region
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'resolves the value to the right region' do
117
+ resolver.resolve(value)
118
+ expect(resolver.resolve(value_in_region_2)).to eq 'myresolvedvalue2'
119
+ end
120
+
121
+ it 'resolves to the same region if it is an alias' do
122
+ expect(resolver.resolve(value_in_region_alias)).to eq 'myresolvedvalue'
123
+ end
124
+ end
125
+ end
126
+ end
77
127
  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: 0.13.0
4
+ version: 0.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: 2017-03-01 00:00:00.000000000 Z
12
+ date: 2017-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler