stack_master 0.0.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 (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