stack_master 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +208 -0
  7. data/Rakefile +11 -0
  8. data/apply_demo.gif +0 -0
  9. data/bin/stack_master +16 -0
  10. data/example/simple/Gemfile +3 -0
  11. data/example/simple/parameters/myapp_vpc.yml +1 -0
  12. data/example/simple/parameters/myapp_web.yml +2 -0
  13. data/example/simple/stack_master.yml +13 -0
  14. data/example/simple/templates/myapp_vpc.rb +39 -0
  15. data/example/simple/templates/myapp_web.rb +16 -0
  16. data/features/apply.feature +241 -0
  17. data/features/delete.feature +43 -0
  18. data/features/diff.feature +191 -0
  19. data/features/events.feature +38 -0
  20. data/features/init.feature +6 -0
  21. data/features/outputs.feature +49 -0
  22. data/features/region_aliases.feature +66 -0
  23. data/features/resources.feature +45 -0
  24. data/features/stack_defaults.feature +88 -0
  25. data/features/status.feature +124 -0
  26. data/features/step_definitions/stack_steps.rb +50 -0
  27. data/features/support/env.rb +14 -0
  28. data/lib/stack_master.rb +81 -0
  29. data/lib/stack_master/aws_driver/cloud_formation.rb +56 -0
  30. data/lib/stack_master/cli.rb +164 -0
  31. data/lib/stack_master/command.rb +13 -0
  32. data/lib/stack_master/commands/apply.rb +104 -0
  33. data/lib/stack_master/commands/delete.rb +53 -0
  34. data/lib/stack_master/commands/diff.rb +31 -0
  35. data/lib/stack_master/commands/events.rb +39 -0
  36. data/lib/stack_master/commands/init.rb +109 -0
  37. data/lib/stack_master/commands/list_stacks.rb +16 -0
  38. data/lib/stack_master/commands/outputs.rb +27 -0
  39. data/lib/stack_master/commands/resources.rb +33 -0
  40. data/lib/stack_master/commands/status.rb +47 -0
  41. data/lib/stack_master/commands/validate.rb +17 -0
  42. data/lib/stack_master/config.rb +86 -0
  43. data/lib/stack_master/ctrl_c.rb +4 -0
  44. data/lib/stack_master/parameter_loader.rb +17 -0
  45. data/lib/stack_master/parameter_resolver.rb +45 -0
  46. data/lib/stack_master/parameter_resolvers/secret.rb +42 -0
  47. data/lib/stack_master/parameter_resolvers/security_group.rb +20 -0
  48. data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +29 -0
  49. data/lib/stack_master/parameter_resolvers/stack_output.rb +53 -0
  50. data/lib/stack_master/prompter.rb +14 -0
  51. data/lib/stack_master/security_group_finder.rb +29 -0
  52. data/lib/stack_master/sns_topic_finder.rb +27 -0
  53. data/lib/stack_master/stack.rb +96 -0
  54. data/lib/stack_master/stack_definition.rb +49 -0
  55. data/lib/stack_master/stack_differ.rb +80 -0
  56. data/lib/stack_master/stack_events/fetcher.rb +45 -0
  57. data/lib/stack_master/stack_events/presenter.rb +27 -0
  58. data/lib/stack_master/stack_events/streamer.rb +55 -0
  59. data/lib/stack_master/stack_states.rb +34 -0
  60. data/lib/stack_master/template_compiler.rb +21 -0
  61. data/lib/stack_master/test_driver/cloud_formation.rb +139 -0
  62. data/lib/stack_master/testing.rb +7 -0
  63. data/lib/stack_master/utils.rb +31 -0
  64. data/lib/stack_master/validator.rb +25 -0
  65. data/lib/stack_master/version.rb +3 -0
  66. data/logo.png +0 -0
  67. data/script/buildkite/bundle.sh +5 -0
  68. data/script/buildkite/clean.sh +3 -0
  69. data/script/buildkite_rspec.sh +27 -0
  70. data/spec/fixtures/parameters/myapp_vpc.yml +1 -0
  71. data/spec/fixtures/stack_master.yml +35 -0
  72. data/spec/fixtures/templates/myapp_vpc.json +1 -0
  73. data/spec/spec_helper.rb +99 -0
  74. data/spec/stack_master/commands/apply_spec.rb +92 -0
  75. data/spec/stack_master/commands/delete_spec.rb +40 -0
  76. data/spec/stack_master/commands/init_spec.rb +17 -0
  77. data/spec/stack_master/commands/status_spec.rb +38 -0
  78. data/spec/stack_master/commands/validate_spec.rb +26 -0
  79. data/spec/stack_master/config_spec.rb +81 -0
  80. data/spec/stack_master/parameter_loader_spec.rb +81 -0
  81. data/spec/stack_master/parameter_resolver_spec.rb +58 -0
  82. data/spec/stack_master/parameter_resolvers/secret_spec.rb +66 -0
  83. data/spec/stack_master/parameter_resolvers/security_group_spec.rb +17 -0
  84. data/spec/stack_master/parameter_resolvers/sns_topic_name_spec.rb +43 -0
  85. data/spec/stack_master/parameter_resolvers/stack_output_spec.rb +77 -0
  86. data/spec/stack_master/security_group_finder_spec.rb +49 -0
  87. data/spec/stack_master/sns_topic_finder_spec.rb +25 -0
  88. data/spec/stack_master/stack_definition_spec.rb +37 -0
  89. data/spec/stack_master/stack_differ_spec.rb +34 -0
  90. data/spec/stack_master/stack_events/fetcher_spec.rb +65 -0
  91. data/spec/stack_master/stack_events/presenter_spec.rb +18 -0
  92. data/spec/stack_master/stack_events/streamer_spec.rb +33 -0
  93. data/spec/stack_master/stack_spec.rb +157 -0
  94. data/spec/stack_master/template_compiler_spec.rb +48 -0
  95. data/spec/stack_master/test_driver/cloud_formation_spec.rb +24 -0
  96. data/spec/stack_master/utils_spec.rb +30 -0
  97. data/spec/stack_master/validator_spec.rb +38 -0
  98. data/stack_master.gemspec +38 -0
  99. data/stacktemplates/parameter_region.yml +3 -0
  100. data/stacktemplates/parameter_stack_name.yml +3 -0
  101. data/stacktemplates/stack.json.erb +20 -0
  102. data/stacktemplates/stack_master.yml.erb +6 -0
  103. metadata +427 -0
@@ -0,0 +1,26 @@
1
+ RSpec.describe StackMaster::Commands::Validate do
2
+
3
+ subject(:validate) { described_class.new(config, stack_definition) }
4
+ let(:config) { instance_double(StackMaster::Config) }
5
+ let(:region) { "us-east-1" }
6
+ let(:stack_name) { "mystack" }
7
+ let(:stack_definition) do
8
+ StackMaster::StackDefinition.new(
9
+ region: region,
10
+ stack_name: stack_name,
11
+ template: 'myapp_vpc.json',
12
+ tags: { 'environment' => 'production' },
13
+ base_dir: File.expand_path('spec/fixtures')
14
+ )
15
+ end
16
+
17
+ describe "#perform" do
18
+ context "can find stack" do
19
+ it "calls the validator to validate the stack definition" do
20
+ expect(StackMaster::Validator).to receive(:perform).with(stack_definition)
21
+ validate.perform
22
+ end
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,81 @@
1
+ RSpec.describe StackMaster::Config do
2
+ subject(:loaded_config) { StackMaster::Config.load!('spec/fixtures/stack_master.yml') }
3
+ let(:base_dir) { File.expand_path('spec/fixtures') }
4
+ let(:myapp_vpc_definition) {
5
+ StackMaster::StackDefinition.new(
6
+ region: 'us-east-1',
7
+ stack_name: 'myapp-vpc',
8
+ template: 'myapp_vpc.json',
9
+ tags: { 'application' => 'my-awesome-blog', 'environment' => 'production' },
10
+ notification_arns: ['test_arn', 'test_arn_2'],
11
+ base_dir: base_dir,
12
+ secret_file: 'production.yml.gpg',
13
+ stack_policy_file: 'my_policy.json',
14
+ additional_parameter_lookup_dirs: ['production']
15
+ )
16
+ }
17
+
18
+ it 'returns an object that can find stack definitions' do
19
+ stack = loaded_config.find_stack('us-east-1', 'myapp-vpc')
20
+ expect(stack).to eq(myapp_vpc_definition)
21
+ end
22
+
23
+ it 'can find things with underscores instead of hyphens' do
24
+ stack = loaded_config.find_stack('us_east_1', 'myapp_vpc')
25
+ expect(stack).to eq(myapp_vpc_definition)
26
+ end
27
+
28
+ it 'exposes the base_dir' do
29
+ expect(loaded_config.base_dir).to eq base_dir
30
+ end
31
+
32
+ it 'loads stack defaults' do
33
+ expect(loaded_config.stack_defaults).to eq({
34
+ 'tags' => { 'application' => 'my-awesome-blog' }
35
+ })
36
+ end
37
+
38
+ it 'loads region defaults' do
39
+ expect(loaded_config.region_defaults).to eq({
40
+ 'us-east-1' => {
41
+ 'tags' => { 'environment' => 'production' },
42
+ 'notification_arns' => ['test_arn'],
43
+ 'secret_file' => 'production.yml.gpg',
44
+ 'stack_policy_file' => 'my_policy.json'
45
+ },
46
+ 'ap-southeast-2' => {
47
+ 'tags' => {'environment' => 'staging'},
48
+ 'notification_arns' => ['test_arn_3'],
49
+ 'secret_file' => 'staging.yml.gpg'
50
+ }
51
+ })
52
+ end
53
+
54
+ it 'loads region_aliases' do
55
+ expect(loaded_config.region_aliases).to eq(
56
+ 'production' => 'us-east-1',
57
+ 'staging' => 'ap-southeast-2'
58
+ )
59
+ end
60
+
61
+ it 'deep merges stack attributes' do
62
+ expect(loaded_config.find_stack('ap-southeast-2', 'myapp-vpc')).to eq(StackMaster::StackDefinition.new(
63
+ stack_name: 'myapp-vpc',
64
+ region: 'ap-southeast-2',
65
+ tags: {
66
+ 'application' => 'my-awesome-blog',
67
+ 'environment' => 'staging'
68
+ },
69
+ notification_arns: ['test_arn_3', 'test_arn_4'],
70
+ template: 'myapp_vpc.rb',
71
+ base_dir: base_dir,
72
+ secret_file: 'staging.yml.gpg',
73
+ additional_parameter_lookup_dirs: ['staging']
74
+ ))
75
+ end
76
+
77
+ it 'allows region aliases in region defaults' do
78
+ config = StackMaster::Config.new({'region_aliases' => { 'production' => 'us-east-1' }, 'region_defaults' => { 'production' => { 'secret_file' => 'production.yml.gpg' }}, 'stacks' => {}}, '/base')
79
+ expect(config.region_defaults).to eq('us-east-1' => { 'secret_file' => 'production.yml.gpg' })
80
+ end
81
+ end
@@ -0,0 +1,81 @@
1
+ RSpec.describe StackMaster::ParameterLoader do
2
+ subject(:parameters) { StackMaster::ParameterLoader.load(parameter_files) }
3
+ let(:parameter_files) { [
4
+ '/base_dir/parameters/stack_name.yml',
5
+ '/base_dir/parameters/us-east-1/stack_name.yml'
6
+ ] }
7
+
8
+ context "no parameter file" do
9
+ before do
10
+ allow(File).to receive(:exists?).and_return(false)
11
+ end
12
+
13
+ it "returns empty parameters" do
14
+ expect(parameters).to eq({})
15
+ end
16
+ end
17
+
18
+ context "an empty stack parameter file" do
19
+ before do
20
+ allow(File).to receive(:exists?).with('/base_dir/parameters/stack_name.yml').and_return(true)
21
+ allow(File).to receive(:exists?).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return(false)
22
+ allow(File).to receive(:read).with('/base_dir/parameters/stack_name.yml').and_return("")
23
+ end
24
+
25
+ it "returns an empty hash" do
26
+ expect(parameters).to eq({})
27
+ end
28
+ end
29
+
30
+ context "stack parameter file" do
31
+ before do
32
+ allow(File).to receive(:exists?).with('/base_dir/parameters/stack_name.yml').and_return(true)
33
+ allow(File).to receive(:exists?).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return(false)
34
+ allow(File).to receive(:read).with('/base_dir/parameters/stack_name.yml').and_return("Param1: value1")
35
+ end
36
+
37
+ it "returns params from stack_name.yml" do
38
+ expect(parameters).to eq({ 'Param1' => 'value1' })
39
+ end
40
+ end
41
+
42
+ context "region parameter file" do
43
+ before do
44
+ allow(File).to receive(:exists?).with('/base_dir/parameters/stack_name.yml').and_return(false)
45
+ allow(File).to receive(:exists?).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return(true)
46
+ allow(File).to receive(:read).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return("Param2: value2")
47
+ end
48
+
49
+ it "returns params from the region base stack_name.yml" do
50
+ expect(parameters).to eq({ 'Param2' => 'value2' })
51
+ end
52
+ end
53
+
54
+ context "stack and region parameter file" do
55
+ before do
56
+ allow(File).to receive(:exists?).with('/base_dir/parameters/stack_name.yml').and_return(true)
57
+ allow(File).to receive(:exists?).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return(true)
58
+ allow(File).to receive(:read).with('/base_dir/parameters/stack_name.yml').and_return("Param1: value1\nParam2: valueX")
59
+ allow(File).to receive(:read).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return("Param2: value2")
60
+ end
61
+
62
+ it "returns params from the region base stack_name.yml" do
63
+ expect(parameters).to eq({
64
+ 'Param1' => 'value1',
65
+ 'Param2' => 'value2'
66
+ })
67
+ end
68
+ end
69
+
70
+ context 'underscored parameter names' do
71
+ before do
72
+ allow(File).to receive(:exists?).with('/base_dir/parameters/stack_name.yml').and_return(true)
73
+ allow(File).to receive(:exists?).with('/base_dir/parameters/us-east-1/stack_name.yml').and_return(false)
74
+ allow(File).to receive(:read).with('/base_dir/parameters/stack_name.yml').and_return("vpc_id: vpc-xxxxxx")
75
+ end
76
+
77
+ it "camelcases them" do
78
+ expect(parameters).to eq({'VpcId' => 'vpc-xxxxxx'})
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,58 @@
1
+ RSpec.describe StackMaster::ParameterResolver do
2
+ let(:config) { double }
3
+ let(:my_resolver) {
4
+ Class.new do
5
+ def initialize(config, region)
6
+ end
7
+
8
+ def resolve(value)
9
+ value.to_i * 5
10
+ end
11
+ end
12
+ }
13
+ before do
14
+ stub_const('StackMaster::ParameterResolvers::MyResolver', my_resolver)
15
+ end
16
+
17
+
18
+ def resolve(params)
19
+ StackMaster::ParameterResolver.resolve(config, 'us-east-1', params)
20
+ end
21
+
22
+ it 'returns the same value for strings' do
23
+ expect(resolve(param1: 'value1')).to eq(param1: 'value1')
24
+ end
25
+
26
+ it 'it throws an error when the hash contains more than one key' do
27
+ expect {
28
+ resolve(param: { nested1: 'value1', nested2: 'value2' })
29
+ }.to raise_error(StackMaster::ParameterResolver::InvalidParameter)
30
+ end
31
+
32
+ it 'throws an error when given an array' do
33
+ expect {
34
+ resolve(param: [1, 2])
35
+ }.to raise_error(StackMaster::ParameterResolver::InvalidParameter)
36
+ end
37
+
38
+ context 'when given a proper resolve hash' do
39
+ it 'returns the value returned by the resolver as the parameter value' do
40
+ expect(resolve(param: { my_resolver: 2 })).to eq(param: 10)
41
+ end
42
+ end
43
+
44
+ context 'when the resolver is unknown' do
45
+ it 'throws an error' do
46
+ expect {
47
+ resolve(param: { my_unknown_resolver: 2 })
48
+ }.to raise_error StackMaster::ParameterResolver::ResolverNotFound
49
+ end
50
+ end
51
+
52
+ context 'resolver class caching' do
53
+ it "uses the same instance of the resolver for the duration of the resolve run" do
54
+ expect(my_resolver).to receive(:new).once.and_call_original
55
+ expect(resolve(param: { my_resolver: 2 }, param2: { my_resolver: 2 })).to eq(param: 10, param2: 10)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,66 @@
1
+ RSpec.describe StackMaster::ParameterResolvers::Secret do
2
+ let(:base_dir) { '/base_dir' }
3
+ let(:config) { double(base_dir: base_dir) }
4
+ let(:stack_definition) { double(secret_file: secrets_file_name, stack_name: 'mystack', region: 'us-east-1') }
5
+ subject(:resolve_secret) { StackMaster::ParameterResolvers::Secret.new(config, stack_definition).resolve(value) }
6
+ let(:value) { 'my_file/my_secret_key' }
7
+ let(:secrets_file_name) { "my_file.yml.gpg" }
8
+ let(:file_path) { "#{base_dir}/secrets/#{secrets_file_name}" }
9
+
10
+ context 'the secret file does not exist' do
11
+ before do
12
+ allow(File).to receive(:exist?).with(file_path).and_return(false)
13
+ end
14
+
15
+ it 'raises an ArgumentError with the location of the expected secret file' do
16
+ expect {
17
+ resolve_secret
18
+ }.to raise_error(ArgumentError, /#{file_path}/)
19
+ end
20
+ end
21
+
22
+ context 'no secret file is specified for the stack definition' do
23
+ before do
24
+ allow(stack_definition).to receive(:secret_file).and_return(nil)
25
+ end
26
+
27
+ it 'raises an ArgumentError with the location of the expected secret file' do
28
+ expect {
29
+ resolve_secret
30
+ }.to raise_error(ArgumentError, /No secret_file defined/)
31
+ end
32
+ end
33
+
34
+ context 'the secret file exists' do
35
+ let(:dir) { double(Dotgpg::Dir) }
36
+ let(:decrypted_file) { <<EOF }
37
+ secret_key_1: secret_value_1
38
+ secret_key_2: secret_value_2
39
+ EOF
40
+
41
+ before do
42
+ allow(File).to receive(:exist?).with(file_path).and_return(true)
43
+ allow(Dotgpg::Dir).to receive(:closest).with(file_path).and_return(dir)
44
+ allow(dir).to receive(:decrypt).with("secrets/#{secrets_file_name}", anything)
45
+ allow(StringIO).to receive(:new).and_return(double(string: decrypted_file))
46
+ end
47
+
48
+ context 'the secret key does not exist' do
49
+ let(:value) { 'unknown_secret' }
50
+
51
+ it 'raises a secret not found error' do
52
+ expect {
53
+ resolve_secret
54
+ }.to raise_error(StackMaster::ParameterResolvers::Secret::SecretNotFound)
55
+ end
56
+ end
57
+
58
+ context 'the secret key exists' do
59
+ let(:value) { 'secret_key_2' }
60
+
61
+ it 'returns the secret' do
62
+ expect(resolve_secret).to eq('secret_value_2')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ RSpec.describe StackMaster::ParameterResolvers::SecurityGroup do
2
+ describe "#resolve" do
3
+ subject(:resolver) { described_class.new(nil, double(region: 'us-east-1')) }
4
+ let(:finder) { instance_double(StackMaster::SecurityGroupFinder) }
5
+ let(:sg_id) { 'sg-id' }
6
+ let(:sg_name) { 'sg-name' }
7
+
8
+ before do
9
+ allow(StackMaster::SecurityGroupFinder).to receive(:new).with('us-east-1').and_return finder
10
+ allow(finder).to receive(:find).with(sg_name).and_return sg_id
11
+ end
12
+
13
+ it "resolves the security group" do
14
+ expect(resolver.resolve(sg_name)).to eq sg_id
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ RSpec.describe StackMaster::ParameterResolvers::SnsTopicName do
2
+ let(:region) { 'us-east-1' }
3
+ let(:stack_name) { 'my-stack' }
4
+ let(:config) { double }
5
+
6
+ def resolve(value)
7
+ described_class.new(config, double(region: 'us-east-1')).resolve(value)
8
+ end
9
+
10
+ subject(:resolved_value) { resolve(value) }
11
+
12
+ context 'when given a hash' do
13
+ let(:value) { { not_expected: 1} }
14
+
15
+ it 'raises an error' do
16
+ expect {
17
+ resolved_value
18
+ }.to raise_error(ArgumentError)
19
+ end
20
+ end
21
+
22
+ context 'when given a string value' do
23
+ let(:value) { 'my-topic-name' }
24
+
25
+ context 'the stack and sns topic name exist' do
26
+ before do
27
+ allow_any_instance_of(StackMaster::SnsTopicFinder).to receive(:find).with(value).and_return('myresolvedvalue')
28
+ end
29
+
30
+ it 'resolves the value' do
31
+ expect(resolved_value).to eq 'myresolvedvalue'
32
+ end
33
+ end
34
+
35
+ context "the topic doesn't exist" do
36
+ it 'raises topic not found' do
37
+ expect {
38
+ resolved_value
39
+ }.to raise_error(StackMaster::ParameterResolvers::SnsTopicName::TopicNotFound)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ RSpec.describe StackMaster::ParameterResolvers::StackOutput do
2
+ let(:region) { 'us-east-1' }
3
+ let(:stack_name) { 'my-stack' }
4
+ let(:config) { double }
5
+ let(:resolver) { described_class.new(config, double(region: 'us-east-1')) }
6
+ let(:cf) { Aws::CloudFormation::Client.new }
7
+
8
+ def resolve(value)
9
+ resolver.resolve(value)
10
+ end
11
+
12
+ subject(:resolved_value) { resolve(value) }
13
+
14
+ context 'when given an invalid string value' do
15
+ let(:value) { 'stack-name-without-output' }
16
+
17
+ it 'raises an error' do
18
+ expect {
19
+ resolved_value
20
+ }.to raise_error(ArgumentError)
21
+ end
22
+ end
23
+
24
+ context 'when given a hash' do
25
+ let(:value) { { not_expected: 1} }
26
+
27
+ it 'raises an error' do
28
+ expect {
29
+ resolved_value
30
+ }.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+
34
+ context 'when given a valid string value' do
35
+ let(:value) { 'my-stack/MyOutput' }
36
+ let(:stacks) { [{ stack_name: 'blah', creation_time: Time.now, stack_status: 'CREATE_COMPLETE', outputs: outputs}] }
37
+ let(:outputs) { [] }
38
+
39
+ before do
40
+ allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf)
41
+ cf.stub_responses(:describe_stacks, stacks: stacks)
42
+ end
43
+
44
+ context 'the stack and output exist' do
45
+ let(:outputs) { [{output_key: 'MyOutput', output_value: 'myresolvedvalue'}] }
46
+
47
+ it 'resolves the value' do
48
+ expect(resolved_value).to eq 'myresolvedvalue'
49
+ end
50
+
51
+ it 'caches stacks for the lifetime of the instance' do
52
+ resolver.resolve(value)
53
+ resolver.resolve(value)
54
+ end
55
+ end
56
+
57
+ context "the stack doesn't exist" do
58
+ let(:stacks) { nil }
59
+
60
+ it 'resolves the value' do
61
+ expect {
62
+ resolved_value
63
+ }.to raise_error(StackMaster::ParameterResolvers::StackOutput::StackNotFound)
64
+ end
65
+ end
66
+
67
+ context "the output doesn't exist" do
68
+ let(:outputs) { [] }
69
+
70
+ it 'resolves the value' do
71
+ expect {
72
+ resolved_value
73
+ }.to raise_error(StackMaster::ParameterResolvers::StackOutput::StackOutputNotFound)
74
+ end
75
+ end
76
+ end
77
+ end