cloudfinder-ec2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,265 @@
1
+ describe Cloudfinder::EC2::Clusterfinder do
2
+
3
+ let (:client) { Aws::EC2::Client.new(stub_responses: true) }
4
+ let (:reservations) { [] }
5
+ let (:cluster_name) { 'production' }
6
+ let (:region) { 'some-region' }
7
+
8
+ before (:each) do
9
+ allow(Aws::EC2::Client).to receive(:new).and_return(client)
10
+ end
11
+
12
+ shared_examples_for 'cluster object response' do
13
+ it 'provides cluster name in returned cluster' do
14
+ expect(cluster.cluster_name).to eq cluster_name
15
+ end
16
+ end
17
+
18
+ shared_examples_for 'empty cluster' do
19
+ include_examples 'cluster object response'
20
+
21
+ it 'returns empty cluster' do
22
+ expect(cluster).to be_empty
23
+ end
24
+ end
25
+
26
+ shared_examples_for 'running cluster with roles' do |roles|
27
+ include_examples 'cluster object response'
28
+
29
+ it 'returns running cluster' do
30
+ expect(cluster).to be_running
31
+ end
32
+
33
+ it 'has only the expected cluster roles' do
34
+ expect(cluster.list_roles).to eq roles
35
+ end
36
+ end
37
+
38
+ shared_examples_for 'cluster with instance in role' do |role, instance_index, instance_id|
39
+ it "has #{role} role in cluster" do
40
+ expect(cluster).to have_role(role)
41
+ end
42
+
43
+ it "has instance #{instance_id} as #{role}:##{instance_index}" do
44
+ expect(cluster.list_role_instances(role)[instance_index].instance_id).to eq instance_id
45
+ end
46
+
47
+ it "populates #{instance_id} public IP" do
48
+ expect(cluster.list_role_instances(role)[instance_index].public_ip).to eq public_ip(instance_id)
49
+ end
50
+
51
+ it "populates #{instance_id} private IP" do
52
+ expect(cluster.list_role_instances(role)[instance_index].private_ip).to eq private_ip(instance_id)
53
+ end
54
+
55
+ it "populates #{instance_id} public DNS" do
56
+ expect(cluster.list_role_instances(role)[instance_index].public_dns).to eq public_dns(instance_id)
57
+ end
58
+
59
+ it "populates #{instance_id} private DNS" do
60
+ expect(cluster.list_role_instances(role)[instance_index].private_dns).to eq private_dns(instance_id)
61
+ end
62
+ end
63
+
64
+ describe '#find' do
65
+ let (:cluster) { subject.find(region: region, cluster_name: cluster_name) }
66
+
67
+ context 'with invalid arguments' do
68
+ it 'throws without region' do
69
+ expect { subject.find(cluster_name: cluster_name) }.to raise_error(ArgumentError)
70
+ end
71
+
72
+ it 'throws without cluster name' do
73
+ expect { subject.find(region: region) }.to raise_error(ArgumentError)
74
+ end
75
+ end
76
+
77
+ context 'when searching for EC2 instances' do
78
+ let (:client) { spy(Aws::EC2::Client) }
79
+
80
+ it 'creates AWS API client for the specified region' do
81
+ expect(Aws::EC2::Client).to receive(:new).with(region: 'any-region').once.and_return(client)
82
+ subject.find(region: 'any-region', cluster_name: cluster_name)
83
+ end
84
+
85
+ it 'requests details of running EC2 instances' do
86
+ subject.find(region: 'us-east-1', cluster_name: 'i12345678')
87
+ expect(client).to have_received(:describe_instances).once.with(filters: [{ name: 'instance-state-name', values: ['running'] }])
88
+ end
89
+ end
90
+
91
+ context 'when AWS API throws exceptions' do
92
+ it 'bubbles exceptions to caller' do
93
+ client.stub_responses(:describe_instances, Aws::Errors::MissingCredentialsError)
94
+ expect { subject.find(region: 'us-east-1', cluster_name: 'any') }.to raise_error(Aws::Errors::MissingCredentialsError)
95
+ end
96
+ end
97
+
98
+ context 'when no instances are running' do
99
+ before (:each) do
100
+ client.stub_responses(:describe_instances, reservations: [])
101
+ end
102
+
103
+ include_examples 'empty cluster'
104
+ end
105
+
106
+ context 'when one instance is running' do
107
+ before (:each) do
108
+ stub_describe_instances(stub_reservation(running_instance))
109
+ end
110
+
111
+ context 'with no tags' do
112
+ let(:running_instance) { stub_instance }
113
+ include_examples 'empty cluster'
114
+ end
115
+
116
+ context 'with irrelevant tags' do
117
+ let(:running_instance) { stub_instance(tags: { 'name' => 'qa-server' }) }
118
+ include_examples 'empty cluster'
119
+ end
120
+
121
+ context 'with cloudfinder-cluster tag for another cluster' do
122
+ let(:running_instance) { stub_instance(cluster_tag: 'another-cluster', role_tag: 'db') }
123
+ include_examples 'empty cluster'
124
+ end
125
+
126
+ context 'tagged for correct cloudfinder-cluster without cloudfinder-role' do
127
+ let(:running_instance) { stub_instance(cluster_tag: cluster_name, role_tag: nil) }
128
+ include_examples 'empty cluster'
129
+ end
130
+
131
+ context 'tagged for correct cloudfinder-cluster with cloudfinder-role=db' do
132
+ let(:running_instance) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
133
+
134
+ include_examples('running cluster with roles', [:db])
135
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
136
+ end
137
+ end
138
+
139
+ context 'when two instances in single reservation' do
140
+ before (:each) do
141
+ stub_describe_instances(stub_reservation(instance_1, instance_2))
142
+ end
143
+
144
+ context 'with only one in this cluster' do
145
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
146
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: 'another-cluster', role_tag: 'app') }
147
+
148
+ include_examples('running cluster with roles', [:db])
149
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
150
+ end
151
+
152
+ context 'both in this cluster with same role' do
153
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
154
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: cluster_name, role_tag: 'db') }
155
+
156
+ include_examples('running cluster with roles', [:db])
157
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
158
+ include_examples('cluster with instance in role', :db, 1, 'i-00000002')
159
+ end
160
+
161
+ context 'both in this cluster with different roles' do
162
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
163
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: cluster_name, role_tag: 'app') }
164
+
165
+ include_examples('running cluster with roles', [:db, :app])
166
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
167
+ include_examples('cluster with instance in role', :app, 0, 'i-00000002')
168
+ end
169
+ end
170
+
171
+ context 'when three instances in two reservations' do
172
+ before (:each) do
173
+ stub_describe_instances(
174
+ stub_reservation(instance_1, instance_2),
175
+ stub_reservation(instance_3)
176
+ )
177
+ end
178
+
179
+ context 'with only two in this cluster' do
180
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: 'another-cluster', role_tag: 'db') }
181
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: 'another-cluster', role_tag: 'app') }
182
+ let (:instance_3) { stub_instance(id: 'i-00000003', cluster_tag: cluster_name, role_tag: 'app') }
183
+
184
+ include_examples('running cluster with roles', [:app])
185
+ include_examples('cluster with instance in role', :app, 0, 'i-00000003')
186
+ end
187
+
188
+ context 'all in this cluster with same role' do
189
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
190
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: cluster_name, role_tag: 'db') }
191
+ let (:instance_3) { stub_instance(id: 'i-00000003', cluster_tag: cluster_name, role_tag: 'db') }
192
+
193
+ include_examples('running cluster with roles', [:db])
194
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
195
+ include_examples('cluster with instance in role', :db, 1, 'i-00000002')
196
+ include_examples('cluster with instance in role', :db, 2, 'i-00000003')
197
+ end
198
+
199
+ context 'all in this cluster with different roles' do
200
+ let (:instance_1) { stub_instance(id: 'i-00000001', cluster_tag: cluster_name, role_tag: 'db') }
201
+ let (:instance_2) { stub_instance(id: 'i-00000002', cluster_tag: cluster_name, role_tag: 'app') }
202
+ let (:instance_3) { stub_instance(id: 'i-00000003', cluster_tag: cluster_name, role_tag: 'cache') }
203
+
204
+ include_examples('running cluster with roles', [:db, :app, :cache])
205
+ include_examples('cluster with instance in role', :db, 0, 'i-00000001')
206
+ include_examples('cluster with instance in role', :app, 0, 'i-00000002')
207
+ include_examples('cluster with instance in role', :cache, 0, 'i-00000003')
208
+ end
209
+ end
210
+ end
211
+
212
+ def stub_describe_instances(*reservations)
213
+ client.stub_responses(:describe_instances, reservations: reservations)
214
+ end
215
+
216
+ def stub_reservation(*instances)
217
+ { instances: instances }
218
+ end
219
+
220
+ def stub_instance(args = {})
221
+ id = args[:id] || 'i-00000001'
222
+ instance = {
223
+ instance_id: id,
224
+ public_ip_address: args[:public_ip] || public_ip(id), #46.137.0.1',
225
+ private_ip_address: args[:private_ip] || private_ip(id), #'10.248.0.1',
226
+ public_dns_name: args[:public_dns] || public_dns(id), #'ec2-46-137-0-1.eu-west-1.compute.amazonaws.com',
227
+ private_dns_name: args[:public_dns] || private_dns(id), #'ip-10-248-0-1.eu-west-1.compute.internal',
228
+ tags: []
229
+ }
230
+ (args[:tags] || {}).each do |key, value|
231
+ instance[:tags] << { key: key, value: value }
232
+ end
233
+
234
+ if args[:cluster_tag]
235
+ instance[:tags] << { key: Cloudfinder::EC2::CLUSTER_TAG_NAME, value: args[:cluster_tag] }
236
+ end
237
+
238
+ if args[:role_tag]
239
+ instance[:tags] << { key: Cloudfinder::EC2::ROLE_TAG_NAME, value: args[:role_tag] }
240
+ end
241
+
242
+ instance
243
+ end
244
+
245
+ def numeric_instance_id(instance_id)
246
+ instance_id[-2, 2].to_i
247
+ end
248
+
249
+ def private_ip(instance_id)
250
+ "10.248.0.#{numeric_instance_id(instance_id)}"
251
+ end
252
+
253
+ def public_ip(instance_id)
254
+ "46.137.0.#{numeric_instance_id(instance_id)}"
255
+ end
256
+
257
+ def private_dns(instance_id)
258
+ "ec2-46-137-0-#{numeric_instance_id(instance_id)}.eu-west-1.compute.amazonaws.com"
259
+ end
260
+
261
+ def public_dns(instance_id)
262
+ "ip-10-248-0-#{numeric_instance_id(instance_id)}.eu-west-1.compute.internal"
263
+ end
264
+
265
+ end
@@ -0,0 +1,131 @@
1
+ describe Cloudfinder::EC2::Command::List, focus: true do
2
+ let (:detector) { spy(Cloudfinder::EC2::Detector) }
3
+ let (:detector_result) { { cluster_name: detected_cluster, cluster_role: 'db', region: detected_region } }
4
+ let (:cluster_finder) { spy(Cloudfinder::EC2::Clusterfinder) }
5
+ let (:found_cluster) { double(Cloudfinder::EC2::Cluster) }
6
+ let (:cluster_hash) { { cluster_name: detected_cluster, roles: {} } }
7
+ let (:standard_out) { StringIO.new }
8
+ let (:error_out) { StringIO.new }
9
+ let (:detected_cluster) { 'qa' }
10
+ let (:detected_region) { 'eu-west-1' }
11
+
12
+ subject do
13
+ Cloudfinder::EC2::Command::List.new(
14
+ cluster_finder,
15
+ detector,
16
+ standard_out,
17
+ error_out
18
+ )
19
+ end
20
+
21
+ before :each do
22
+ allow(cluster_finder).to receive(:find).and_return found_cluster
23
+ allow(found_cluster).to receive(:to_hash).and_return cluster_hash
24
+ end
25
+
26
+ shared_examples_for 'render cluster as JSON' do
27
+ it 'prints cluster hash to standard out as JSON' do
28
+ expect(JSON.parse(standard_out.string)).to eq stringify_keys(cluster_hash)
29
+ end
30
+
31
+ it 'prints nothing to standard error' do
32
+ expect(error_out.string).to eq ''
33
+ end
34
+ end
35
+
36
+ shared_examples_for 'find cluster by specified name' do |name|
37
+ it 'finds cluster with specified cluster name' do
38
+ expect(cluster_finder).to have_received(:find).with(hash_including(cluster_name: name))
39
+ end
40
+ end
41
+
42
+ shared_examples_for 'find cluster by detected name' do
43
+ it 'finds cluster with detected cluster name' do
44
+ expect(cluster_finder).to have_received(:find).with(hash_including(cluster_name: detected_cluster))
45
+ end
46
+ end
47
+
48
+ shared_examples_for 'find cluster in specified region' do |region|
49
+ it 'finds cluster in specified region' do
50
+ expect(cluster_finder).to have_received(:find).with(hash_including(region: region))
51
+ end
52
+ end
53
+
54
+ shared_examples_for 'find cluster in detected region' do
55
+ it 'finds cluster in detected region' do
56
+ expect(cluster_finder).to have_received(:find).with(hash_including(region: detected_region))
57
+ end
58
+ end
59
+
60
+
61
+ describe '#execute' do
62
+ context 'when cluster name and region are specified' do
63
+ before :each do
64
+ subject.execute(cluster_name: 'other-cluster', region: 'custom-region')
65
+ end
66
+
67
+ it 'does not attempt automatic cluster detection' do
68
+ expect(detector).not_to have_received(:detect_cluster)
69
+ end
70
+
71
+ include_examples('find cluster by specified name', 'other-cluster')
72
+ include_examples('find cluster in specified region', 'custom-region')
73
+ include_examples 'render cluster as JSON'
74
+ end
75
+
76
+ context 'when only cluster name is specified' do
77
+ before :each do
78
+ expect(detector).to receive(:detect_cluster).and_return detector_result
79
+ subject.execute(cluster_name: 'other-cluster')
80
+ end
81
+
82
+ include_examples('find cluster by specified name', 'other-cluster')
83
+ include_examples('find cluster in detected region')
84
+ include_examples 'render cluster as JSON'
85
+ end
86
+
87
+ context 'when region is specified and cluster name can be detected' do
88
+ before :each do
89
+ expect(detector).to receive(:detect_cluster).and_return detector_result
90
+ subject.execute(region: 'custom-region')
91
+ end
92
+
93
+ include_examples('find cluster by detected name')
94
+ include_examples('find cluster in specified region', 'custom-region')
95
+ include_examples 'render cluster as JSON'
96
+ end
97
+
98
+ context 'when cluster detector throws exception' do
99
+ before :each do
100
+ expect(detector).to receive(:detect_cluster).and_raise Errno::ENETUNREACH
101
+ end
102
+
103
+ it 'rethrows the exception' do
104
+ expect { subject.execute }.to raise_error Errno::ENETUNREACH
105
+ end
106
+
107
+ it 'prints nothing to standard out' do
108
+ begin
109
+ subject.execute rescue StandardError
110
+ end
111
+ expect(standard_out.string).to eq ''
112
+ end
113
+
114
+ it 'prints exception header to stderr' do
115
+ begin
116
+ subject.execute rescue StandardError
117
+ end
118
+ expect(error_out.string).to match /This instance may not be running on EC2/
119
+ end
120
+ end
121
+ end
122
+
123
+ def stringify_keys(hash)
124
+ string_hash = {}
125
+ hash.each do |key, value|
126
+ string_hash[key.to_s] = value
127
+ end
128
+ string_hash
129
+ end
130
+
131
+ end
@@ -0,0 +1,152 @@
1
+ describe Cloudfinder::EC2::Detector do
2
+ describe '#detect' do
3
+ let (:ec2_client) { Aws::EC2::Client.new(stub_responses: true) }
4
+ let (:tags) { [] }
5
+
6
+ before (:each) do
7
+ allow(Aws::EC2::Client).to receive(:new).and_return(ec2_client)
8
+ ec2_client.stub_responses(:describe_tags, tags: tags)
9
+ end
10
+
11
+ context 'when instance metadata not available' do
12
+ it 'throws exception on timeout' do
13
+ allow(subject).to receive(:open).and_raise TimeoutError
14
+ expect { subject.detect_cluster }.to raise_error(TimeoutError)
15
+ end
16
+
17
+ it 'throws exception on connection refused' do
18
+ allow(subject).to receive(:open).and_raise Errno::ECONNREFUSED
19
+ expect { subject.detect_cluster }.to raise_error(Errno::ECONNREFUSED)
20
+ end
21
+
22
+ it 'throws exception on 404' do
23
+ allow(subject).to receive(:open).and_raise OpenURI::HTTPError.new('404', double('io'))
24
+ expect { subject.detect_cluster }.to raise_error(OpenURI::HTTPError)
25
+ end
26
+
27
+ it 'throws exception on network unreachable' do
28
+ allow(subject).to receive(:open).and_raise Errno::ENETUNREACH
29
+ expect { subject.detect_cluster }.to raise_error(Errno::ENETUNREACH)
30
+ end
31
+
32
+ end
33
+
34
+ shared_examples_for 'instance outside cluster' do
35
+ include_examples 'instance with metadata'
36
+
37
+ it 'returns a nil cluster name' do
38
+ expect(subject.detect_cluster[:cluster_name]).to be_nil
39
+ end
40
+
41
+ it 'returns a nil cluster role' do
42
+ expect(subject.detect_cluster[:cluster_name]).to be_nil
43
+ end
44
+ end
45
+
46
+ shared_examples_for 'instance with metadata' do
47
+ it 'returns the instance id' do
48
+ expect(subject.detect_cluster[:instance_id]).to eq instance_id
49
+ end
50
+
51
+ it 'returns the AWS region' do
52
+ expect(subject.detect_cluster[:region]).to eq region
53
+ end
54
+ end
55
+
56
+ context 'when instance metadata available' do
57
+ let (:instance_id) { 'i-00000001' }
58
+ let (:availability_zone) { 'eu-west-1c' }
59
+ let (:region) { 'eu-west-1' }
60
+
61
+ before :each do
62
+ stub_metadata('/placement/availability-zone', availability_zone)
63
+ stub_metadata('/instance-id', instance_id)
64
+ end
65
+
66
+ context 'when searching for EC2 instances' do
67
+ let (:ec2_client) { spy(Aws::EC2::Client) }
68
+
69
+ it 'creates AWS API client for the specified region' do
70
+ expect(Aws::EC2::Client).to receive(:new).with(region: region).once
71
+ subject.detect_cluster
72
+ end
73
+
74
+ it 'requests the tags for this instance' do
75
+ subject.detect_cluster
76
+ expect(ec2_client).to have_received(:describe_tags).once.with(filters: [{ name: 'resource-id', values: [instance_id] }])
77
+ end
78
+
79
+ end
80
+
81
+ context 'when AWS API throws exceptions' do
82
+ it 'bubbles exceptions to caller' do
83
+ ec2_client.stub_responses(:describe_tags, Aws::Errors::MissingCredentialsError)
84
+ expect { subject.detect_cluster }.to raise_error(Aws::Errors::MissingCredentialsError)
85
+ end
86
+ end
87
+
88
+ context 'when not part of cluster' do
89
+ context 'when instance has no tags' do
90
+ let (:tags) { [] }
91
+ include_examples 'instance outside cluster'
92
+ end
93
+
94
+ context 'when instance only has irrelevant tags' do
95
+ let (:tags) { [stub_tag('Name', 'anything')] }
96
+ include_examples 'instance outside cluster'
97
+ end
98
+
99
+ context 'when instance has no cloudfinder-cluster tag' do
100
+ let (:tags) { [stub_tag(Cloudfinder::EC2::ROLE_TAG_NAME, 'anything')] }
101
+ include_examples 'instance outside cluster'
102
+ end
103
+
104
+ context 'when instance has no cloudfinder-role tag' do
105
+ let (:tags) { [stub_tag(Cloudfinder::EC2::CLUSTER_TAG_NAME, 'anything')] }
106
+ include_examples 'instance outside cluster'
107
+ end
108
+ end
109
+
110
+ context 'when part of cluster' do
111
+ let (:cluster_name) { 'qa' }
112
+ let (:cluster_role) { 'app' }
113
+ let (:tags) { [
114
+ stub_tag(Cloudfinder::EC2::CLUSTER_TAG_NAME, cluster_name),
115
+ stub_tag(Cloudfinder::EC2::ROLE_TAG_NAME, cluster_role)
116
+ ] }
117
+
118
+ include_examples 'instance with metadata'
119
+
120
+ it 'returns cluster name' do
121
+ expect(subject.detect_cluster[:cluster_name]).to eq cluster_name
122
+ end
123
+ it 'returns cluster role' do
124
+ expect(subject.detect_cluster[:cluster_role]).to eq cluster_role.to_sym
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def stub_metadata(path, response)
131
+ url = "http://169.254.169.254/latest/meta-data#{path}"
132
+ timeout = Cloudfinder::EC2::Detector::EC2_METADATA_TIMEOUT
133
+ allow(subject).to receive(:open).with(url, { read_timeout: timeout }).and_return FakeResponse.new(response)
134
+ end
135
+
136
+ def stub_tag(key, value)
137
+ {
138
+ key: key,
139
+ value: value
140
+ }
141
+ end
142
+
143
+ class FakeResponse
144
+ def initialize(body)
145
+ @body = body
146
+ end
147
+
148
+ def read
149
+ @body
150
+ end
151
+ end
152
+ end