stemcell 0.5.0 → 0.6.0

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 (39) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +18 -0
  4. data/lib/stemcell/command_line.rb +2 -2
  5. data/lib/stemcell/errors.rb +3 -2
  6. data/lib/stemcell/metadata_source/chef_repository.rb +55 -0
  7. data/lib/stemcell/metadata_source/configuration.rb +77 -0
  8. data/lib/stemcell/metadata_source.rb +51 -98
  9. data/lib/stemcell/version.rb +1 -1
  10. data/lib/stemcell.rb +0 -2
  11. data/spec/fixtures/chef_repo/roles/unit-inherit-base.rb +20 -0
  12. data/spec/fixtures/chef_repo/roles/unit-inherit-both.rb +36 -0
  13. data/spec/fixtures/chef_repo/roles/unit-inherit-default.rb +21 -0
  14. data/spec/fixtures/chef_repo/roles/unit-inherit-none.rb +6 -0
  15. data/spec/fixtures/chef_repo/roles/unit-inherit-override.rb +21 -0
  16. data/spec/fixtures/chef_repo/roles/unit-simple-both.rb +29 -0
  17. data/spec/fixtures/chef_repo/roles/unit-simple-default.rb +16 -0
  18. data/spec/fixtures/chef_repo/roles/unit-simple-none.rb +2 -0
  19. data/spec/fixtures/chef_repo/roles/unit-simple-override.rb +16 -0
  20. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-inherit-both.json +16 -0
  21. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-inherit-default.json +14 -0
  22. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-inherit-none.json +11 -0
  23. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-inherit-override.json +14 -0
  24. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-simple-both.json +13 -0
  25. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-simple-default.json +11 -0
  26. data/spec/fixtures/chef_repo/roles-expected-metadata/unit-simple-override.json +11 -0
  27. data/spec/fixtures/chef_repo/stemcell-azs-missing.json +10 -0
  28. data/spec/fixtures/chef_repo/stemcell-backing-store-empty.json +9 -0
  29. data/spec/fixtures/chef_repo/stemcell-backing-store-missing.json +8 -0
  30. data/spec/fixtures/chef_repo/stemcell-defaults-missing.json +10 -0
  31. data/spec/fixtures/chef_repo/stemcell.json +13 -0
  32. data/spec/lib/stemcell/metadata_source/chef_repository_spec.rb +107 -0
  33. data/spec/lib/stemcell/metadata_source/configuration_spec.rb +117 -0
  34. data/spec/lib/stemcell/metadata_source_spec.rb +250 -0
  35. data/spec/spec_helper.rb +13 -0
  36. data/spec/support/fixture_helper.rb +14 -0
  37. metadata +60 -24
  38. data/lib/stemcell/utility/deep_merge.rb +0 -13
  39. data/lib/stemcell/utility.rb +0 -1
@@ -0,0 +1,13 @@
1
+ {
2
+ "instance_type": "m3.xlarge",
3
+ "security_groups": [
4
+ "all",
5
+ "default",
6
+ "override"
7
+ ],
8
+ "tags": {
9
+ "tag1": "tag1_value_override",
10
+ "tag2": "tag2_value",
11
+ "tag3": "tag3_value"
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "instance_type": "c1.xlarge",
3
+ "security_groups": [
4
+ "all",
5
+ "default"
6
+ ],
7
+ "tags": {
8
+ "tag1": "tag1_value_default",
9
+ "tag2": "tag2_value"
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "instance_type": "m3.xlarge",
3
+ "security_groups": [
4
+ "all",
5
+ "override"
6
+ ],
7
+ "tags": {
8
+ "tag1": "tag1_value_override",
9
+ "tag3": "tag3_value"
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "defaults": {
3
+ "instance_type": "m1.small"
4
+ },
5
+ "backing_store": {
6
+ "instance_store": {
7
+ "image_id": "ami-d9d6a6b0"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "defaults": {
3
+ "instance_type": "m1.small"
4
+ },
5
+ "backing_store": {},
6
+ "availability_zones": {
7
+ "us-east-1": ["us-east-1a"]
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "defaults": {
3
+ "instance_type": "m1.small"
4
+ },
5
+ "availability_zones": {
6
+ "us-east-1": ["us-east-1a"]
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "backing_store": {
3
+ "instance_store": {
4
+ "baz": "woo"
5
+ }
6
+ },
7
+ "availability_zones": {
8
+ "us-east-1": ["us-east-1a"]
9
+ }
10
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "defaults": {
3
+ "instance_type": "m1.small"
4
+ },
5
+ "backing_store": {
6
+ "instance_store": {
7
+ "image_id": "ami-d9d6a6b0"
8
+ }
9
+ },
10
+ "availability_zones": {
11
+ "us-east-1": ["us-east-1a"]
12
+ }
13
+ }
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stemcell::MetadataSource::ChefRepository do
4
+
5
+ let(:chef_root) { FixtureHelper.chef_repo_fixture_path }
6
+ let(:chef_repo) { Stemcell::MetadataSource::ChefRepository.new(chef_root) }
7
+
8
+ describe '#initialize' do
9
+
10
+ it "sets chef_root" do
11
+ expect(chef_repo.chef_root).to eql(chef_root)
12
+ end
13
+
14
+ context "with a mocked chef root" do
15
+ let(:chef_root) { '/path/to/chef' }
16
+
17
+ it "configures the cookbooks path" do
18
+ chef_repo
19
+ expect(Chef::Config[:cookbook_path]).to eql('/path/to/chef/cookbooks')
20
+ end
21
+ it "configures the data_bags path" do
22
+ chef_repo
23
+ expect(Chef::Config[:data_bag_path]).to eql('/path/to/chef/data_bags')
24
+ end
25
+ it "configures the roles path" do
26
+ chef_repo
27
+ expect(Chef::Config[:role_path]).to eql('/path/to/chef/roles')
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ describe '#metadata_for_role' do
34
+
35
+ let(:expected_metadata) { FixtureHelper.expected_metadata_for_role(role) }
36
+ let(:result_metadata) { chef_repo.metadata_for_role(role, environment) }
37
+
38
+ let(:environment) { 'production' }
39
+ let(:role) { nil }
40
+
41
+ context "for a role with no inheritence" do
42
+
43
+ context "and no attributes" do
44
+ let(:role) { 'unit-simple-none' }
45
+ it "returns nil" do
46
+ expect(result_metadata).to eql(nil)
47
+ end
48
+ end
49
+
50
+ context "and default attributes" do
51
+ let(:role) { 'unit-simple-default' }
52
+ it "returns the expected metdata" do
53
+ expect(result_metadata).to eql(expected_metadata)
54
+ end
55
+ end
56
+
57
+ context "and override attributes" do
58
+ let(:role) { 'unit-simple-override' }
59
+ it "returns the expected metdata" do
60
+ expect(result_metadata).to eql(expected_metadata)
61
+ end
62
+ end
63
+
64
+ context "and both default and override attributes" do
65
+ let(:role) { 'unit-simple-both' }
66
+ it "returns the expected metdata" do
67
+ expect(result_metadata).to eql(expected_metadata)
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ context "for a role with inheritence" do
74
+
75
+ context "and no attributes" do
76
+ let(:role) { 'unit-inherit-none' }
77
+ it "returns the expected metdata" do
78
+ expect(result_metadata).to eql(expected_metadata)
79
+ end
80
+ end
81
+
82
+ context "and default attributes" do
83
+ let(:role) { 'unit-inherit-default' }
84
+ it "returns the expected metdata" do
85
+ expect(result_metadata).to eql(expected_metadata)
86
+ end
87
+ end
88
+
89
+ context "and override attributes" do
90
+ let(:role) { 'unit-inherit-override' }
91
+ it "returns the expected metdata" do
92
+ expect(result_metadata).to eql(expected_metadata)
93
+ end
94
+ end
95
+
96
+ context "and both default and override attributes" do
97
+ let(:role) { 'unit-inherit-both' }
98
+ it "returns the expected metdata" do
99
+ expect(result_metadata).to eql(expected_metadata)
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stemcell::MetadataSource::Configuration do
4
+
5
+ let(:config_filename) { 'stemcell.json' }
6
+
7
+ let(:chef_root) { FixtureHelper.chef_repo_fixture_path }
8
+ let(:path) { File.join(chef_root, config_filename) }
9
+ let(:config) { Stemcell::MetadataSource::Configuration.new(path) }
10
+
11
+ describe '#initialize' do
12
+
13
+ it "sets config_path" do
14
+ expect(config.config_path).to eql path
15
+ end
16
+
17
+ it "sets all_options" do
18
+ expect(config.all_options.keys).to eql([
19
+ 'defaults',
20
+ 'backing_store',
21
+ 'availability_zones'
22
+ ])
23
+ end
24
+
25
+ context "when the required options are present" do
26
+ it "sets default_options" do
27
+ expect(config.default_options).to eql({
28
+ 'instance_type' => 'm1.small'
29
+ })
30
+ end
31
+
32
+ it "sets backing_store_options" do
33
+ expect(config.backing_store_options).to eql({
34
+ 'instance_store' => {
35
+ 'image_id' => 'ami-d9d6a6b0'
36
+ }
37
+ })
38
+ end
39
+
40
+ it "sets availability_zones" do
41
+ expect(config.availability_zones).to eql({
42
+ 'us-east-1' => ['us-east-1a']
43
+ })
44
+ end
45
+ end
46
+
47
+ context "when defaults are not specified" do
48
+ let(:config_filename) { 'stemcell-defaults-missing.json' }
49
+ it "raises" do
50
+ expect { config }.to raise_error(Stemcell::MetadataConfigParseError)
51
+ end
52
+ end
53
+
54
+ context "when backing store options are not specified" do
55
+ let(:config_filename) { 'stemcell-backing-store-missing.json' }
56
+ it "raises" do
57
+ expect { config }.to raise_error(Stemcell::MetadataConfigParseError)
58
+ end
59
+ end
60
+
61
+ context "when availability zones are empty" do
62
+ let(:config_filename) { 'stemcell-backing-store-empty.json' }
63
+ it "raises" do
64
+ expect { config }.to raise_error(Stemcell::MetadataConfigParseError)
65
+ end
66
+ end
67
+
68
+ context "when availability zones are not specified" do
69
+ let(:config_filename) { 'stemcell-azs-missing.json' }
70
+ it "raises" do
71
+ expect { config }.to raise_error(Stemcell::MetadataConfigParseError)
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ describe '#options_for_backing_store' do
78
+ let(:backing_store) { 'instance_store' }
79
+
80
+ context "when the backing store definition exists" do
81
+ it "returns the options" do
82
+ expect(config.options_for_backing_store(backing_store)).to eql({
83
+ 'image_id' => 'ami-d9d6a6b0'
84
+ })
85
+ end
86
+ end
87
+
88
+ context "when the backing store isn't defined" do
89
+ let(:backing_store) { 'nyanstore' }
90
+ it "raises" do
91
+ expect { config.options_for_backing_store(backing_store) }.to raise_error(
92
+ Stemcell::UnknownBackingStoreError
93
+ )
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ describe '#random_az_in_region' do
100
+ let(:region) { 'us-east-1' }
101
+
102
+ context "when availability zones are defined for the region" do
103
+ it "returns an az" do
104
+ expect(config.random_az_for_region(region)).to eql('us-east-1a')
105
+ end
106
+ end
107
+
108
+ context "when availability zone aren't defined for the region" do
109
+ let(:region) { 'nyancat' }
110
+ it "returns nil" do
111
+ expect(config.random_az_for_region(region)).to be_nil
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ end
@@ -0,0 +1,250 @@
1
+ require 'spec_helper'
2
+
3
+ describe Stemcell::MetadataSource do
4
+
5
+ let(:chef_root) { FixtureHelper.chef_repo_fixture_path }
6
+ let(:config_filename) { 'stemcell.json' }
7
+
8
+ let(:metadata_source) do
9
+ Stemcell::MetadataSource.new(chef_root, config_filename)
10
+ end
11
+
12
+ let(:config) { metadata_source.config }
13
+ let(:chef_repo) { metadata_source.chef_repo }
14
+
15
+ describe '#initialize' do
16
+
17
+ context "when the arguments are valid" do
18
+ it "sets chef_root" do
19
+ expect(metadata_source.chef_root).to eql chef_root
20
+ end
21
+ it "sets config_filename" do
22
+ expect(metadata_source.config_filename).to eql config_filename
23
+ end
24
+
25
+ it "constructs a configuration object" do
26
+ expect(metadata_source.config).to be_an_instance_of(
27
+ Stemcell::MetadataSource::Configuration)
28
+ end
29
+
30
+ it "uses the correct path for the configuration object" do
31
+ expect(metadata_source.config.config_path).to eql(
32
+ File.join(chef_root, config_filename))
33
+ end
34
+
35
+ it "constructs a chef repository object" do
36
+ expect(metadata_source.chef_repo).to be_an_instance_of(
37
+ Stemcell::MetadataSource::ChefRepository)
38
+ end
39
+
40
+ it "uses the correct path for the chef repository object" do
41
+ expect(metadata_source.chef_repo.chef_root).to eql(chef_root)
42
+ end
43
+ end
44
+
45
+ context "when the chef root is nil" do
46
+ let(:chef_root) { nil }
47
+ it "raises an ArgumentError" do
48
+ expect { metadata_source }.to raise_error(ArgumentError)
49
+ end
50
+ end
51
+
52
+ context "when the configuration file name is nil" do
53
+ let(:config_filename) { nil }
54
+ it "raise an ArgumentError" do
55
+ expect { metadata_source }.to raise_error(ArgumentError)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ describe '#expand_role' do
62
+
63
+ let(:default_options) { Hash.new }
64
+ let(:backing_options) { Hash.new }
65
+ let(:availability_zones) { Hash.new }
66
+ let(:role_metadata) { Hash.new }
67
+ let(:override_options) { Hash.new }
68
+ let(:expand_options) { Hash.new }
69
+
70
+ before do
71
+ config.stub(:default_options) { default_options }
72
+ config.stub(:availability_zones) { availability_zones }
73
+ config.stub(:options_for_backing_store) { backing_options }
74
+ chef_repo.stub(:metadata_for_role) { role_metadata }
75
+ end
76
+
77
+ let(:role) { 'role' }
78
+ let(:environment) { 'production' }
79
+
80
+ let(:expansion) do
81
+ metadata_source.expand_role(
82
+ role,
83
+ environment,
84
+ override_options,
85
+ expand_options)
86
+ end
87
+
88
+ context "when arguments are valid" do
89
+
90
+ before { role_metadata.merge!('instance_type' => 'c1.xlarge') }
91
+
92
+ describe "backing store" do
93
+
94
+ context "when backing store is not explicitly set" do
95
+ it "uses instance_store" do
96
+ expect(expansion['backing_store']).to eql 'instance_store'
97
+ end
98
+ end
99
+
100
+ context "when the override options specify a backing store" do
101
+ before { override_options.merge!('backing_store' => 'from_override') }
102
+
103
+ it "is the value in the override options" do
104
+ expect(expansion['backing_store']).to eql 'from_override'
105
+ end
106
+
107
+ it "overrides the backing store set in the role" do
108
+ role_metadata.merge!('backing_store' => 'from_role')
109
+ expect(expansion['backing_store']).to eql 'from_override'
110
+ end
111
+ end
112
+
113
+ context "when the role metadata specifies a backing store" do
114
+ before { role_metadata.merge!('backing_store' => 'from_role') }
115
+
116
+ it "is the value in the role metadata" do
117
+ expect(expansion['backing_store']).to eql 'from_role'
118
+ end
119
+
120
+ it "overrides the value given in the configuration" do
121
+ default_options.merge!('backing_store' => 'from_defaults')
122
+ expect(expansion['backing_store']).to eql 'from_role'
123
+ end
124
+ end
125
+
126
+ context "when the default options specify a backing store" do
127
+ before { default_options.merge!('backing_store' => 'from_defaults') }
128
+
129
+ it "is the value in the default options" do
130
+ expect(expansion['backing_store']).to eql 'from_defaults'
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+ describe 'expansion' do
137
+
138
+ context "when no options are explicitly set" do
139
+ it "contains the defaults" do
140
+ # This assumes that the environment is set to the default
141
+ Stemcell::MetadataSource::DEFAULT_OPTIONS.each_pair do |key, value|
142
+ expect(expansion[key]).to eql value
143
+ end
144
+ end
145
+ end
146
+
147
+ context "when the role and environment are not the default" do
148
+ let(:role) { 'not_default_role' }
149
+ let(:environment) { 'not_default_environmnet' }
150
+
151
+ it "contains the role" do
152
+ expect(expansion['chef_role']).to eql role
153
+ end
154
+ it "contains the environment" do
155
+ expect(expansion['chef_environment']).to eql environment
156
+ end
157
+ end
158
+
159
+ it "calls the config object to retrieve the backing store options" do
160
+ backing_options.merge!('image_id' => 'ami-nyancat')
161
+ override_options.merge!('backing_store' => 'ebs')
162
+ config.should_receive(:options_for_backing_store).with('ebs') { backing_options }
163
+ expect(expansion['image_id']).to eql 'ami-nyancat'
164
+ end
165
+
166
+ it "calls the repository object to determine the role metadata" do
167
+ role_metadata.merge!('image_id' => 'ami-nyancat')
168
+ chef_repo.should_receive(:metadata_for_role).with(role, environment) { role_metadata }
169
+ expect(expansion['image_id']).to eql 'ami-nyancat'
170
+ end
171
+
172
+ context "when a config default overrides a built-in default" do
173
+ before { default_options.merge!('git_branch' => 'from_default') }
174
+
175
+ it "returns the value from the config defaults" do
176
+ expect(expansion['git_branch']).to eql 'from_default'
177
+ end
178
+ end
179
+
180
+ context "when role metadata overrides a config default" do
181
+ before { default_options.merge!('option' => 'from_default') }
182
+ before { role_metadata.merge!('option' => 'from_role') }
183
+
184
+ it "returns the value from the role metadata" do
185
+ expect(expansion['option']).to eql 'from_role'
186
+ end
187
+ end
188
+
189
+ context "when an override option overrides the role metadata" do
190
+ before { role_metadata.merge!('option' => 'from_role') }
191
+ before { override_options.merge!('option' => 'from_override') }
192
+
193
+ it "returns the value from the override options" do
194
+ expect(expansion['option']).to eql 'from_override'
195
+ end
196
+ end
197
+
198
+ context "when a region was specified but no availability zone" do
199
+
200
+ let(:availability_zones) { { 'us-east-1' => ['us-east-1a'] } }
201
+
202
+ before do
203
+ override_options.merge!('region' => 'us-east-1')
204
+ override_options.merge!('availability_zone' => nil)
205
+ end
206
+
207
+ it "substitutes an availability zone from the config" do
208
+ expect(expansion['availability_zone']).to eql 'us-east-1a'
209
+ end
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+
216
+ context "when the role metadata isn't present" do
217
+ let(:role_metadata) { nil }
218
+
219
+ context "when allowing empty roles" do
220
+ before { expand_options[:allow_empty_roles] = true }
221
+ it "returns successfully" do
222
+ expect(expansion).to_not be_nil
223
+ end
224
+ end
225
+
226
+ context "when not allowing empty roles" do
227
+ before { expand_options[:allow_empty_roles] = false }
228
+ it "raises" do
229
+ expect { expansion }.to raise_error(Stemcell::EmptyRoleError)
230
+ end
231
+ end
232
+ end
233
+
234
+ context "when role is nil" do
235
+ let(:role) { nil }
236
+ it "raises" do
237
+ expect { expansion }.to raise_error(ArgumentError)
238
+ end
239
+ end
240
+
241
+ context "when environment is nil" do
242
+ let(:environment) { nil }
243
+ it "raises" do
244
+ expect { expansion }.to raise_error(ArgumentError)
245
+ end
246
+ end
247
+
248
+ end
249
+
250
+ end
@@ -0,0 +1,13 @@
1
+ require 'rspec/instafail'
2
+ require 'simplecov'
3
+
4
+ require 'stemcell'
5
+ require 'support/fixture_helper'
6
+
7
+ SimpleCov.start do
8
+ add_filter 'spec'
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.color_enabled = true
13
+ end
@@ -0,0 +1,14 @@
1
+ module FixtureHelper
2
+ def self.chef_repo_fixture_path
3
+ File.expand_path("../../fixtures/chef_repo", __FILE__)
4
+ end
5
+
6
+ def self.expected_metadata_path
7
+ File.join(chef_repo_fixture_path, 'roles-expected-metadata')
8
+ end
9
+
10
+ def self.expected_metadata_for_role(role)
11
+ fixture_path = File.join(expected_metadata_path, "#{role}.json")
12
+ JSON.parse(File.read(fixture_path))
13
+ end
14
+ end