kitchen-google 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Kitchen
20
+ module Driver
21
+ GCE_VERSION = "1.0.0".freeze
22
+ end
23
+ end
@@ -1,8 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
3
  # Author:: Andrew Leonard (<andy@hurricane-ridge.com>)
4
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
4
5
  #
5
- # Copyright (C) 2013-2014, Andrew Leonard
6
+ # Copyright (C) 2013-2016, Andrew Leonard and Chef Software, Inc.
6
7
  #
7
8
  # Licensed under the Apache License, Version 2.0 (the "License");
8
9
  # you may not use this file except in compliance with the License.
@@ -16,383 +17,945 @@
16
17
  # See the License for the specific language governing permissions and
17
18
  # limitations under the License.
18
19
 
19
- require_relative '../../spec_helper.rb'
20
+ require "spec_helper"
21
+ require "google/apis/compute_v1"
22
+ require "kitchen/driver/gce"
23
+ require "kitchen/provisioner/dummy"
24
+ require "kitchen/transport/dummy"
25
+ require "kitchen/verifier/dummy"
26
+
27
+ shared_examples_for "a validity checker" do |config_key, api_method, *args|
28
+ it "returns false if the config value is nil" do
29
+ expect(driver).to receive(:config).and_return({})
30
+ expect(subject).to eq(false)
31
+ end
20
32
 
21
- require 'resolv'
33
+ it "checks the outcome of the API call" do
34
+ connection = double("connection")
35
+ allow(driver).to receive(:config).and_return({ config_key => "test_value" })
36
+ expect(driver).to receive(:connection).and_return(connection)
37
+ expect(connection).to receive(api_method).with(*args, "test_value")
38
+ expect(driver).to receive(:check_api_call).and_call_original
39
+ expect(subject).to eq(true)
40
+ end
41
+ end
22
42
 
23
43
  describe Kitchen::Driver::Gce do
44
+ let(:logged_output) { StringIO.new }
45
+ let(:logger) { Logger.new(logged_output) }
46
+ let(:platform) { Kitchen::Platform.new(name: "fake_platform") }
47
+ let(:transport) { Kitchen::Transport::Dummy.new }
48
+ let(:driver) { Kitchen::Driver::Gce.new(config) }
49
+
50
+ let(:project) { "test_project" }
51
+ let(:zone) { "test_zone" }
24
52
 
25
53
  let(:config) do
26
- { google_client_email: '123456789012@developer.gserviceaccount.com',
27
- google_project: 'alpha-bravo-123'
54
+ {
55
+ project: project,
56
+ zone: zone,
57
+ image_name: "test_image",
28
58
  }
29
59
  end
30
60
 
31
- let(:state) { Hash.new }
61
+ let(:instance) do
62
+ instance_double(Kitchen::Instance,
63
+ logger: logger,
64
+ transport: transport,
65
+ platform: platform,
66
+ to_str: "instance_str"
67
+ )
68
+ end
32
69
 
33
- let(:logged_output) { StringIO.new }
34
- let(:logger) { Logger.new(logged_output) }
70
+ before do
71
+ allow(driver).to receive(:instance).and_return(instance)
72
+ allow(driver).to receive(:project).and_return("test_project")
73
+ allow(driver).to receive(:zone).and_return("test_zone")
74
+ end
35
75
 
36
- let(:instance) do
37
- double(
38
- logger: logger,
39
- name: 'default-distro-12'
40
- )
41
- end
42
-
43
- let(:driver) do
44
- d = Kitchen::Driver::Gce.new(config)
45
- allow(d).to receive(:instance) { instance }
46
- allow(d).to receive(:wait_for_sshd) { true }
47
- d
48
- end
49
-
50
- let(:fog) do
51
- Fog::Compute::Google::Mock.new({})
52
- end
53
-
54
- let(:disk) do
55
- fog.disks.create(
56
- name: 'rspec-test-disk',
57
- size_gb: 10,
58
- zone_name: 'us-central1-b',
59
- source_image: 'debian-7-wheezy-v20130816'
60
- )
61
- end
62
-
63
- let(:server) do
64
- fog.servers.create(
65
- name: 'rspec-test-instance',
66
- disks: [disk],
67
- machine_type: 'n1-standard-1',
68
- zone_name: 'us-central1-b'
69
- )
70
- end
71
-
72
- before(:each) do
73
- Fog.mock!
74
- Fog::Mock.reset
75
- Fog::Mock.delay = 0
76
- end
77
-
78
- describe '#initialize' do
79
- context 'with default options' do
80
-
81
- defaults = {
82
- area: 'us-central1',
83
- autodelete_disk: true,
84
- disk_size: 10,
85
- inst_name: nil,
86
- machine_type: 'n1-standard-1',
87
- network: 'default',
88
- region: nil,
89
- service_accounts: nil,
90
- tags: [],
91
- username: ENV['USER'],
92
- zone_name: nil,
93
- google_key_location: nil,
94
- google_json_key_location: nil,
95
- preemptible: false,
96
- auto_restart: false
97
- }
76
+ it "driver API version is 2" do
77
+ expect(driver.diagnose_plugin[:api_version]).to eq(2)
78
+ end
98
79
 
99
- defaults.each do |k, v|
100
- it "sets the correct default for #{k}" do
101
- expect(driver[k]).to eq(v)
102
- end
103
- end
80
+ describe '#name' do
81
+ it "has an overridden name" do
82
+ expect(driver.name).to eq("Google Compute (GCE)")
83
+ end
84
+ end
85
+
86
+ describe '#create' do
87
+ let(:connection) { double("connection") }
88
+ let(:operation) { double("operation", name: "test_operation") }
89
+ let(:state) { {} }
90
+
91
+ before do
92
+ allow(driver).to receive(:validate!)
93
+ allow(driver).to receive(:connection).and_return(connection)
94
+ allow(driver).to receive(:generate_server_name)
95
+ allow(driver).to receive(:wait_for_operation)
96
+ allow(driver).to receive(:server_instance)
97
+ allow(driver).to receive(:create_instance_object)
98
+ allow(driver).to receive(:ip_address_for)
99
+ allow(driver).to receive(:update_windows_password)
100
+ allow(driver).to receive(:wait_for_server)
101
+ allow(connection).to receive(:insert_instance).and_return(operation)
102
+ end
103
+
104
+ it "does not create the server if the hostname is in the state file" do
105
+ expect(connection).not_to receive(:insert_instance)
106
+ driver.create(server_name: "server_exists")
107
+ end
108
+
109
+ it "generates a unique server name and sets the state" do
110
+ expect(driver).to receive(:generate_server_name).and_return("server_1")
111
+ driver.create(state)
112
+ expect(state[:server_name]).to eq("server_1")
104
113
  end
105
114
 
106
- context 'with overriden options' do
107
- overrides = {
108
- area: 'europe-west',
109
- autodelete_disk: false,
110
- disk_size: 15,
111
- inst_name: 'ci-instance',
112
- machine_type: 'n1-highmem-8',
113
- network: 'dev-net',
114
- region: 'asia-east1',
115
- service_accounts: %w(userdata.email compute.readonly),
116
- tags: %w(qa integration),
117
- username: 'root',
118
- zone_name: 'europe-west1-a',
119
- google_key_location: '/path/to/foo.p12',
120
- google_json_key_location: '/path/to/bar.json',
121
- preemptible: true,
122
- auto_restart: false # because of preemptible, see kitchen-google/pull/22
115
+ it "creates the instance via the API and waits for it to complete" do
116
+ expect(driver).to receive(:generate_server_name).and_return("server_1")
117
+ expect(driver).to receive(:create_instance_object).with("server_1").and_return("create_obj")
118
+ expect(connection).to receive(:insert_instance).with("test_project", "test_zone", "create_obj").and_return(operation)
119
+ expect(driver).to receive(:wait_for_operation).with(operation)
120
+
121
+ driver.create(state)
122
+ end
123
+
124
+ it "sets the correct data in the state object" do
125
+ expect(driver).to receive(:generate_server_name).and_return("server_1")
126
+ expect(driver).to receive(:server_instance).with("server_1").and_return("server_obj")
127
+ expect(driver).to receive(:ip_address_for).with("server_obj").and_return("1.2.3.4")
128
+ driver.create(state)
129
+
130
+ expect(state[:server_name]).to eq("server_1")
131
+ expect(state[:hostname]).to eq("1.2.3.4")
132
+ expect(state[:zone]).to eq("test_zone")
133
+ end
134
+
135
+ it "updates the windows password" do
136
+ expect(driver).to receive(:generate_server_name).and_return("server_1")
137
+ expect(driver).to receive(:update_windows_password).with("server_1")
138
+ driver.create(state)
139
+ end
140
+
141
+ it "waits for the server to be ready" do
142
+ expect(driver).to receive(:wait_for_server)
143
+ driver.create(state)
144
+ end
145
+
146
+ it "destroys the server if any exceptions are raised" do
147
+ expect(connection).to receive(:insert_instance).and_raise(RuntimeError)
148
+ expect(driver).to receive(:destroy).with(state)
149
+ expect { driver.create(state) }.to raise_error(RuntimeError)
150
+ end
151
+ end
152
+
153
+ describe '#destroy' do
154
+ let(:connection) { double("connection") }
155
+ let(:state) { { server_name: "server_1", hostname: "test_host", zone: "test_zone" } }
156
+
157
+ before do
158
+ allow(driver).to receive(:connection).and_return(connection)
159
+ allow(driver).to receive(:server_exist?).and_return(true)
160
+ allow(driver).to receive(:wait_for_operation)
161
+ allow(connection).to receive(:delete_instance)
162
+ end
163
+
164
+ it "does not attempt to delete the instance if there is no server_name" do
165
+ expect(connection).not_to receive(:delete_instance)
166
+ driver.destroy({})
167
+ end
168
+
169
+ it "does not attempt to delete the instance if it does not exist" do
170
+ expect(driver).to receive(:server_exist?).with("server_1").and_return(false)
171
+ expect(connection).not_to receive(:delete_instance)
172
+ driver.destroy(state)
173
+ end
174
+
175
+ it "deletes the instance via the API and waits for it to complete" do
176
+ expect(connection).to receive(:delete_instance).with("test_project", "test_zone", "server_1").and_return("operation")
177
+ expect(driver).to receive(:wait_for_operation).with("operation")
178
+ driver.destroy(state)
179
+ end
180
+
181
+ it "deletes the state keys" do
182
+ driver.destroy(state)
183
+ expect(state.key?(:server_name)).to eq(false)
184
+ expect(state.key?(:hostname)).to eq(false)
185
+ expect(state.key?(:zone)).to eq(false)
186
+ end
187
+ end
188
+
189
+ describe '#validate!' do
190
+ let(:config) do
191
+ {
192
+ project: "test_project",
193
+ zone: "test_zone",
194
+ machine_type: "test_machine_type",
195
+ disk_type: "test_disk_type",
196
+ network: "test_network",
123
197
  }
198
+ end
124
199
 
125
- let(:config) { overrides }
200
+ before do
201
+ allow(driver).to receive(:valid_project?).and_return(true)
202
+ allow(driver).to receive(:valid_zone?).and_return(true)
203
+ allow(driver).to receive(:valid_region?).and_return(true)
204
+ allow(driver).to receive(:valid_machine_type?).and_return(true)
205
+ allow(driver).to receive(:valid_disk_type?).and_return(true)
206
+ allow(driver).to receive(:valid_network?).and_return(true)
207
+ allow(driver).to receive(:winrm_transport?).and_return(false)
208
+ allow(driver).to receive(:config).and_return(config)
209
+ end
126
210
 
127
- overrides.each do |k, v|
128
- it "overrides the default value for #{k}" do
129
- expect(driver[k]).to eq(v)
130
- end
211
+ it "does not raise an exception when all validations are successful" do
212
+ expect { driver.validate! }.not_to raise_error
213
+ end
214
+
215
+ context "when neither zone nor region are specified" do
216
+ let(:config) { {} }
217
+ it "raises an exception" do
218
+ expect { driver.validate! }.to raise_error(RuntimeError, "Either zone or region must be specified")
131
219
  end
132
220
  end
133
- end
134
221
 
135
- describe '#connection' do
136
- context 'with required variables set' do
137
- it 'returns a Fog Compute object' do
138
- expect(driver.send(:connection)).to be_a(Fog::Compute::Google::Mock)
222
+ context "when zone and region are both set" do
223
+ let(:config) { { zone: "test_zone", region: "test_region" } }
224
+
225
+ it "warns the user that the region will be ignored" do
226
+ expect(driver).to receive(:warn).with("Both zone and region specified - region will be ignored.")
227
+ driver.validate!
139
228
  end
229
+ end
140
230
 
141
- it 'uses the v1 api version' do
142
- conn = driver.send(:connection)
143
- expect(conn.api_version).to eq('v1')
231
+ context "when region is set to 'any'" do
232
+ let(:config) { { region: "any" } }
233
+ it "raises an exception" do
234
+ expect { driver.validate! }.to raise_error(RuntimeError, "'any' is no longer a valid region")
144
235
  end
145
236
  end
146
237
 
147
- context 'without required variables set' do
148
- let(:config) { Hash.new }
238
+ context "when zone is set" do
239
+ let(:config) { { zone: "test_zone" } }
149
240
 
150
- it 'raises an error' do
151
- expect { driver.send(:connection) }.to raise_error(ArgumentError)
241
+ it "raises an exception if the zone is not valid" do
242
+ expect(driver).to receive(:valid_zone?).and_return(false)
243
+ expect { driver.validate! }.to raise_error(RuntimeError, "Zone test_zone is not a valid zone")
152
244
  end
153
245
  end
154
- end
155
246
 
156
- describe '#create' do
157
- context 'with an existing server' do
158
- let(:state) do
159
- s = Hash.new
160
- s[:server_id] = 'default-distro-12345678'
161
- s
247
+ context "when region is set" do
248
+ let(:config) { { region: "test_region" } }
249
+
250
+ it "raises an exception if the region is not valid" do
251
+ expect(driver).to receive(:valid_region?).and_return(false)
252
+ expect { driver.validate! }.to raise_error(RuntimeError, "Region test_region is not a valid region")
162
253
  end
254
+ end
255
+
256
+ it "raises an exception if the project is invalid" do
257
+ expect(driver).to receive(:valid_project?).and_return(false)
258
+ expect { driver.validate! }.to raise_error(RuntimeError, "Project test_project is not a valid project")
259
+ end
260
+
261
+ it "raises an exception if the machine_type is invalid" do
262
+ expect(driver).to receive(:valid_machine_type?).and_return(false)
263
+ expect { driver.validate! }.to raise_error(RuntimeError, "Machine type test_machine_type is not valid")
264
+ end
163
265
 
164
- it 'returns if server_id already exists' do
165
- expect(driver.create(state)).to equal nil
266
+ it "raises an exception if the disk_type is invalid" do
267
+ expect(driver).to receive(:valid_disk_type?).and_return(false)
268
+ expect { driver.validate! }.to raise_error(RuntimeError, "Disk type test_disk_type is not valid")
269
+ end
270
+
271
+ it "raises an exception if the network is invalid" do
272
+ expect(driver).to receive(:valid_network?).and_return(false)
273
+ expect { driver.validate! }.to raise_error(RuntimeError, "Network test_network is not valid")
274
+ end
275
+
276
+ it "raises an exception if WinRM transport is used but no email is set" do
277
+ expect(driver).to receive(:winrm_transport?).and_return(true)
278
+ expect { driver.validate! }.to raise_error(RuntimeError, "Email address of GCE user is not set")
279
+ end
280
+ end
281
+
282
+ describe '#connection' do
283
+ it "returns a properly configured ComputeService" do
284
+ compute_service = double("compute_service")
285
+ client_options = double("client_options")
286
+
287
+ expect(Google::Apis::ClientOptions).to receive(:new).and_return(client_options)
288
+ expect(client_options).to receive(:application_name=).with("kitchen-google")
289
+ expect(client_options).to receive(:application_version=).with(Kitchen::Driver::GCE_VERSION)
290
+
291
+ expect(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
292
+ expect(driver).to receive(:authorization).and_return("authorization_object")
293
+ expect(compute_service).to receive(:authorization=).with("authorization_object")
294
+ expect(compute_service).to receive(:client_options=).with(client_options)
295
+
296
+ expect(driver.connection).to eq(compute_service)
297
+ end
298
+ end
299
+
300
+ describe '#authorization' do
301
+ it "returns a Google::Auth authorization object" do
302
+ auth_object = double("auth_object")
303
+ expect(Google::Auth).to receive(:get_application_default).and_return(auth_object)
304
+ expect(driver.authorization).to eq(auth_object)
305
+ end
306
+ end
307
+
308
+ describe '#winrm_transport?' do
309
+ it "returns true if the transport name is Winrm" do
310
+ expect(transport).to receive(:name).and_return("Winrm")
311
+ expect(driver.winrm_transport?).to eq(true)
312
+ end
313
+
314
+ it "returns false if the transport name is not Winrm" do
315
+ expect(transport).to receive(:name).and_return("Ssh")
316
+ expect(driver.winrm_transport?).to eq(false)
317
+ end
318
+ end
319
+
320
+ describe '#update_windows_password' do
321
+ it "does not attempt to reset the password if the transport is not WinRM" do
322
+ expect(driver).to receive(:winrm_transport?).and_return(false)
323
+ expect(GoogleComputeWindowsPassword).not_to receive(:new)
324
+
325
+ driver.update_windows_password("server_1")
326
+ end
327
+
328
+ it "resets the password and puts it in the state object if the transport is WinRM" do
329
+ state = {}
330
+ winpass = double("winpass")
331
+ winpass_config = {
332
+ project: "test_project",
333
+ zone: "test_zone",
334
+ instance_name: "server_1",
335
+ email: "test_email",
336
+ username: "test_username",
337
+ }
338
+
339
+ allow(driver).to receive(:state).and_return(state)
340
+ expect(transport).to receive(:config).and_return(username: "test_username")
341
+ expect(driver).to receive(:config).and_return(email: "test_email")
342
+ expect(driver).to receive(:winrm_transport?).and_return(true)
343
+ expect(GoogleComputeWindowsPassword).to receive(:new).with(winpass_config).and_return(winpass)
344
+ expect(winpass).to receive(:new_password).and_return("password123")
345
+ driver.update_windows_password("server_1")
346
+ expect(state[:password]).to eq("password123")
347
+ end
348
+ end
349
+
350
+ describe '#check_api_call' do
351
+ it "returns false and logs a debug message if the block raises a ClientError" do
352
+ expect(driver).to receive(:debug).with("API error: whoops")
353
+ expect(driver.check_api_call { raise Google::Apis::ClientError.new("whoops") }).to eq(false)
354
+ end
355
+
356
+ it "raises an exception if the block raises something other than a ClientError" do
357
+ expect { driver.check_api_call { raise RuntimeError.new("whoops") } }.to raise_error(RuntimeError)
358
+ end
359
+
360
+ it "returns true if the block does not raise an exception" do
361
+ expect(driver.check_api_call { true }).to eq(true)
362
+ end
363
+ end
364
+
365
+ describe '#valid_machine_type?' do
366
+ subject { driver.valid_machine_type? }
367
+ it_behaves_like "a validity checker", :machine_type, :get_machine_type, "test_project", "test_zone"
368
+ end
369
+
370
+ describe '#valid_network?' do
371
+ subject { driver.valid_network? }
372
+ it_behaves_like "a validity checker", :network, :get_network, "test_project"
373
+ end
374
+
375
+ describe '#valid_zone?' do
376
+ subject { driver.valid_zone? }
377
+ it_behaves_like "a validity checker", :zone, :get_zone, "test_project"
378
+ end
379
+
380
+ describe '#valid_region?' do
381
+ subject { driver.valid_region? }
382
+ it_behaves_like "a validity checker", :region, :get_region, "test_project"
383
+ end
384
+
385
+ describe '#valid_disk_type?' do
386
+ subject { driver.valid_disk_type? }
387
+ it_behaves_like "a validity checker", :disk_type, :get_disk_type, "test_project", "test_zone"
388
+ end
389
+
390
+ describe '#image_exist?' do
391
+ it "checks the outcome of the API call" do
392
+ connection = double("connection")
393
+ expect(driver).to receive(:connection).and_return(connection)
394
+ expect(connection).to receive(:get_image).with("image_project", "image_name")
395
+ expect(driver).to receive(:check_api_call).and_call_original
396
+ expect(driver.image_exist?("image_project", "image_name")).to eq(true)
397
+ end
398
+ end
399
+
400
+ describe '#server_exist?' do
401
+ it "checks the outcome of the API call" do
402
+ expect(driver).to receive(:server_instance).with("server_1")
403
+ expect(driver).to receive(:check_api_call).and_call_original
404
+ expect(driver.server_exist?("server_1")).to eq(true)
405
+ end
406
+ end
407
+
408
+ describe '#project' do
409
+ it "returns the project from the config" do
410
+ allow(driver).to receive(:project).and_call_original
411
+ expect(driver).to receive(:config).and_return(project: "my_project")
412
+ expect(driver.project).to eq("my_project")
413
+ end
414
+ end
415
+
416
+ describe '#region' do
417
+ it "returns the region from the config" do
418
+ expect(driver).to receive(:config).and_return(region: "my_region")
419
+ expect(driver.region).to eq("my_region")
420
+ end
421
+ end
422
+
423
+ describe '#zone' do
424
+ before do
425
+ allow(driver).to receive(:zone).and_call_original
426
+ end
427
+
428
+ context "when a zone exists in the state" do
429
+ let(:state) { { zone: "state_zone" } }
430
+
431
+ it "returns the zone from the state" do
432
+ expect(driver).to receive(:state).and_return(state)
433
+ expect(driver.zone).to eq("state_zone")
166
434
  end
167
435
  end
168
436
 
169
- context 'when an instance is successfully created' do
437
+ context "when a zone does not exist in the state" do
438
+ let(:state) { {} }
170
439
 
171
- let(:driver) do
172
- d = Kitchen::Driver::Gce.new(config)
173
- allow(d).to receive(:create_instance) { server }
174
- allow(d).to receive(:wait_for_up_instance) { nil }
175
- d
440
+ before do
441
+ allow(driver).to receive(:state).and_return(state)
176
442
  end
177
443
 
178
- it 'sets a value for server_id in the state hash' do
179
- driver.send(:create, state)
180
- expect(state[:server_id]).to eq('rspec-test-instance')
444
+ it "returns the zone from the config if it exists" do
445
+ expect(driver).to receive(:config).and_return(zone: "config_zone")
446
+ expect(driver.zone).to eq("config_zone")
181
447
  end
182
448
 
183
- it 'returns nil' do
184
- expect(driver.send(:create, state)).to equal(nil)
449
+ it "returns the zone from find_zone if it does not exist in the config" do
450
+ expect(driver).to receive(:config).and_return({})
451
+ expect(driver).to receive(:find_zone).and_return("found_zone")
452
+ expect(driver.zone).to eq("found_zone")
185
453
  end
186
454
  end
187
455
  end
188
456
 
189
- describe '#create_disk' do
190
- context 'with defaults and required options' do
191
- it 'returns a Google Disk object' do
192
- config[:image_name] = 'debian-7-wheezy-v20130816'
193
- config[:inst_name] = 'rspec-disk'
194
- config[:zone_name] = 'us-central1-a'
195
- expect(driver.send(:create_disk)).to be_a(Fog::Compute::Google::Disk)
196
- end
457
+ describe '#find_zone' do
458
+ let(:zones_in_region) { double("zones_in_region") }
459
+
460
+ before do
461
+ expect(driver).to receive(:zones_in_region).and_return(zones_in_region)
197
462
  end
198
463
 
199
- context 'without required options' do
200
- it 'returns a Fog NotFound Error' do
201
- expect { driver.send(:create_disk) }.to raise_error(
202
- Fog::Errors::NotFound)
203
- end
464
+ it "returns a random zone from the list of zones in the region" do
465
+ zone = double("zone", name: "random_zone")
466
+ expect(zones_in_region).to receive(:sample).and_return(zone)
467
+ expect(driver.find_zone).to eq("random_zone")
468
+ end
469
+
470
+ it "raises an exception if no zones are found" do
471
+ expect(zones_in_region).to receive(:sample).and_return(nil)
472
+ expect(driver).to receive(:region).and_return("test_region")
473
+ expect { driver.find_zone }.to raise_error(RuntimeError, "Unable to find a suitable zone in test_region")
204
474
  end
205
475
  end
206
476
 
207
- describe '#create_instance' do
208
- context 'with default options' do
209
- it 'returns a Fog Compute Server object' do
210
- expect(driver.send(:create_instance)).to be_a(
211
- Fog::Compute::Google::Server)
212
- end
477
+ describe '#zones_in_region' do
478
+ it "returns a correct list of available zones" do
479
+ zone1 = double("zone1", status: "UP", region: "a/b/c/test_region")
480
+ zone2 = double("zone2", status: "UP", region: "a/b/c/test_region")
481
+ zone3 = double("zone3", status: "DOWN", region: "a/b/c/test_region")
482
+ zone4 = double("zone4", status: "UP", region: "a/b/c/wrong_region")
483
+ zone5 = double("zone5", status: "UP", region: "a/b/c/test_region")
484
+ connection = double("connection")
485
+ response = double("response", items: [zone1, zone2, zone3, zone4, zone5])
486
+
487
+ allow(driver).to receive(:region).and_return("test_region")
488
+ expect(driver).to receive(:connection).and_return(connection)
489
+ expect(connection).to receive(:list_zones).and_return(response)
490
+ expect(driver.zones_in_region).to eq([zone1, zone2, zone5])
491
+ end
492
+ end
213
493
 
214
- it 'sets the region to the default "us-central1"' do
215
- driver.send(:create_instance)
216
- expect(config[:region]).to eq('us-central1')
217
- end
494
+ describe '#server_instance' do
495
+ it "returns the instance from the API" do
496
+ connection = double("connection")
497
+ expect(driver).to receive(:connection).and_return(connection)
498
+ expect(connection).to receive(:get_instance).with("test_project", "test_zone", "server_1").and_return("instance")
499
+ expect(driver.server_instance("server_1")).to eq("instance")
218
500
  end
501
+ end
219
502
 
220
- context 'area set, region unset' do
221
- let(:config) do
222
- { area: 'europe-west1',
223
- google_client_email: '123456789012@developer.gserviceaccount.com',
224
- google_key_location: '/home/user/gce/123456-privatekey.p12',
225
- google_project: 'alpha-bravo-123'
226
- }
227
- end
503
+ describe '#ip_address_for' do
504
+ it "returns the private IP if use_private_ip is true" do
505
+ expect(driver).to receive(:config).and_return(use_private_ip: true)
506
+ expect(driver).to receive(:private_ip_for).with("server").and_return("1.2.3.4")
507
+ expect(driver.ip_address_for("server")).to eq("1.2.3.4")
508
+ end
228
509
 
229
- it 'sets region to the area value' do
230
- driver.send(:create_instance)
231
- expect(config[:region]).to eq(config[:area])
232
- end
510
+ it "returns the public IP if use_private_ip is false" do
511
+ expect(driver).to receive(:config).and_return(use_private_ip: false)
512
+ expect(driver).to receive(:public_ip_for).with("server").and_return("4.3.2.1")
513
+ expect(driver.ip_address_for("server")).to eq("4.3.2.1")
233
514
  end
515
+ end
234
516
 
235
- context 'area set, region set' do
236
- let(:config) do
237
- { area: 'fugazi',
238
- google_client_email: '123456789012@developer.gserviceaccount.com',
239
- google_key_location: '/home/user/gce/123456-privatekey.p12',
240
- google_project: 'alpha-bravo-123',
241
- region: 'europe-west1'
242
- }
243
- end
517
+ describe '#private_ip_for' do
518
+ it "returns the IP address if it exists" do
519
+ network_interface = double("network_interface", network_ip: "1.2.3.4")
520
+ server = double("server", network_interfaces: [network_interface])
244
521
 
245
- it 'sets the region independent of the area value' do
246
- driver.send(:create_instance)
247
- expect(config[:region]).to eq('europe-west1')
248
- end
522
+ expect(driver.private_ip_for(server)).to eq("1.2.3.4")
523
+ end
249
524
 
525
+ it "raises an exception if the IP cannot be found" do
526
+ server = double("server")
527
+
528
+ expect(server).to receive(:network_interfaces).and_raise(NoMethodError)
529
+ expect { driver.private_ip_for(server) }.to raise_error(RuntimeError, "Unable to determine private IP for instance")
250
530
  end
251
531
  end
252
532
 
253
- describe '#create_server' do
254
- context 'with default options' do
255
- it 'returns a Fog Compute Server object' do
256
- expect(driver.send(:create_instance)).to be_a(
257
- Fog::Compute::Google::Server)
258
- end
533
+ describe '#public_ip_for' do
534
+ it "returns the IP address if it exists" do
535
+ access_config = double("access_config", nat_ip: "4.3.2.1")
536
+ network_interface = double("network_interface", access_configs: [access_config])
537
+ server = double("server", network_interfaces: [network_interface])
538
+
539
+ expect(driver.public_ip_for(server)).to eq("4.3.2.1")
540
+ end
541
+
542
+ it "raises an exception if the IP cannot be found" do
543
+ network_interface = double("network_interface")
544
+ server = double("server", network_interfaces: [network_interface])
545
+
546
+ expect(network_interface).to receive(:access_configs).and_raise(NoMethodError)
547
+ expect { driver.public_ip_for(server) }.to raise_error(RuntimeError, "Unable to determine public IP for instance")
259
548
  end
260
549
  end
261
550
 
262
- describe '#destroy' do
263
- let(:state) do
264
- s = Hash.new
265
- s[:server_id] = 'rspec-test-instance'
266
- s[:hostname] = '198.51.100.17'
267
- s
551
+ describe '#generate_server_name' do
552
+ it "generates and returns a server name" do
553
+ expect(instance).to receive(:name).and_return("ABC123")
554
+ expect(SecureRandom).to receive(:hex).with(3).and_return("abcdef")
555
+ expect(driver.generate_server_name).to eq("tk-abc123-abcdef")
268
556
  end
269
557
 
270
- it 'returns if server_id does not exist' do
271
- expect(driver.destroy({})).to equal nil
558
+ it "uses a UUID-based server name if the instance name is too long" do
559
+ expect(instance).to receive(:name).twice.and_return("123456789012345678901234567890123456789012345678901235467890")
560
+ expect(driver).to receive(:warn)
561
+ expect(SecureRandom).to receive(:hex).with(3).and_return("abcdef")
562
+ expect(SecureRandom).to receive(:uuid).and_return("lmnop")
563
+ expect(driver.generate_server_name).to eq("tk-lmnop")
272
564
  end
565
+ end
273
566
 
274
- it 'removes the server state information' do
275
- driver.destroy(state)
276
- expect(state[:hostname]).to equal(nil)
277
- expect(state[:server_id]).to equal(nil)
567
+ describe '#boot_disk' do
568
+ it "sets up a disk object and returns it" do
569
+ disk = double("disk")
570
+ params = double("params")
571
+
572
+ config = {
573
+ autodelete_disk: "auto_delete",
574
+ disk_size: "test_size",
575
+ disk_type: "test_type",
576
+ }
577
+
578
+ allow(driver).to receive(:config).and_return(config)
579
+ expect(driver).to receive(:disk_type_url_for).with("test_type").and_return("disk_url")
580
+ expect(driver).to receive(:disk_image_url).and_return("disk_image_url")
581
+
582
+ expect(Google::Apis::ComputeV1::AttachedDisk).to receive(:new).and_return(disk)
583
+ expect(Google::Apis::ComputeV1::AttachedDiskInitializeParams).to receive(:new).and_return(params)
584
+ expect(disk).to receive(:boot=).with(true)
585
+ expect(disk).to receive(:auto_delete=).with("auto_delete")
586
+ expect(disk).to receive(:initialize_params=).with(params)
587
+ expect(params).to receive(:disk_name=).with("server_1")
588
+ expect(params).to receive(:disk_size_gb=).with("test_size")
589
+ expect(params).to receive(:disk_type=).with("disk_url")
590
+ expect(params).to receive(:source_image=).with("disk_image_url")
591
+
592
+ expect(driver.boot_disk("server_1")).to eq(disk)
278
593
  end
279
594
  end
280
595
 
281
- describe '#generate_inst_name' do
282
- context 'with a name less than 28 characters' do
283
- it 'concatenates the name and a UUID' do
284
- expect(driver.send(:generate_inst_name)).to match(
285
- /^default-distro-12-[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/)
286
- end
596
+ describe '#disk_type_url_for' do
597
+ it "returns a disk URL" do
598
+ expect(driver.disk_type_url_for("my_type")).to eq("zones/test_zone/diskTypes/my_type")
287
599
  end
600
+ end
288
601
 
289
- context 'with a name 27 characters or longer' do
290
- let(:instance) do
291
- double(name: 'a23456789012345678901234567')
292
- end
602
+ describe '#disk_image_url' do
603
+ before do
604
+ allow(driver).to receive(:config).and_return(config)
605
+ end
293
606
 
294
- it 'shortens the base name and appends a UUID' do
295
- expect(driver.send(:generate_inst_name).length).to eq 63
296
- expect(driver.send(:generate_inst_name)).to match(
297
- /^a2345678901234567890123456
298
- -[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/x)
607
+ context "when the user supplies an image project" do
608
+ let(:config) { { image_project: "my_image_project", image_name: "my_image" } }
609
+
610
+ it "returns the image URL based on the image project" do
611
+ expect(driver).to receive(:image_url_for).with("my_image_project", "my_image").and_return("image_url")
612
+ expect(driver.disk_image_url).to eq("image_url")
299
613
  end
300
614
  end
301
615
 
302
- context 'with a "name" value containing an invalid leading character' do
303
- let(:instance) do
304
- double(name: '12345')
616
+ context "when the user does not supply an image project" do
617
+ let(:config) { { image_name: "my_image" } }
618
+
619
+ context "when the image exists in the user's project" do
620
+ it "returns the image URL" do
621
+ expect(driver).to receive(:image_url_for).with(project, "my_image").and_return("image_url")
622
+ expect(driver.disk_image_url).to eq("image_url")
623
+ end
305
624
  end
306
625
 
307
- it 'adds a leading "t"' do
308
- expect(driver.send(:generate_inst_name)).to match(
309
- /^t12345
310
- -[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/x)
626
+ context "when the image does not exist in the user's project" do
627
+ before do
628
+ expect(driver).to receive(:image_url_for).with(project, "my_image").and_return(nil)
629
+ end
630
+
631
+ context "when the image matches a known public project" do
632
+ it "returns the image URL from the public project" do
633
+ expect(driver).to receive(:public_project_for_image).with("my_image").and_return("public_project")
634
+ expect(driver).to receive(:image_url_for).with("public_project", "my_image").and_return("image_url")
635
+ expect(driver.disk_image_url).to eq("image_url")
636
+ end
637
+ end
638
+
639
+ context "when the image does not match a known project" do
640
+ it "returns nil" do
641
+ expect(driver).to receive(:public_project_for_image).with("my_image").and_return(nil)
642
+ expect(driver).not_to receive(:image_url_for)
643
+ expect(driver.disk_image_url).to eq(nil)
644
+ end
645
+ end
311
646
  end
312
647
  end
648
+ end
313
649
 
314
- context 'with a "name" value containing uppercase letters' do
315
- let(:instance) do
316
- double(name: 'AbCdEf')
317
- end
650
+ describe '#image_url_for' do
651
+ it "returns nil if the image does not exist" do
652
+ expect(driver).to receive(:image_exist?).with("image_project", "image_name").and_return(false)
653
+ expect(driver.image_url_for("image_project", "image_name")).to eq(nil)
654
+ end
318
655
 
319
- it 'downcases the "name" characters in the instance name' do
320
- expect(driver.send(:generate_inst_name)).to match(
321
- /^abcdef
322
- -[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/x)
323
- end
656
+ it "returns a properly formatted image URL if the image exists" do
657
+ expect(driver).to receive(:image_exist?).with("image_project", "image_name").and_return(true)
658
+ expect(driver.image_url_for("image_project", "image_name")).to eq("projects/image_project/global/images/image_name")
324
659
  end
660
+ end
325
661
 
326
- context 'with a name value containing invalid characters' do
327
- let(:instance) do
328
- double(name: 'a!b@c#d$e%f^g&h*i(j)')
662
+ describe '#public_project_for_image' do
663
+ {
664
+ "centos" => "centos-cloud",
665
+ "container-vm" => "google-containers",
666
+ "coreos" => "coreos-cloud",
667
+ "debian" => "debian-cloud",
668
+ "opensuse-cloud" => "opensuse-cloud",
669
+ "rhel" => "rhel-cloud",
670
+ "sles" => "suse-cloud",
671
+ "ubuntu" => "ubuntu-os-cloud",
672
+ "windows" => "windows-cloud",
673
+ }.each do |image_name, project_name|
674
+ it "returns project #{project_name} for an image named #{image_name}" do
675
+ expect(driver.public_project_for_image(image_name)).to eq(project_name)
329
676
  end
677
+ end
678
+ end
330
679
 
331
- it 'replaces the invalid characters with dashes' do
332
- expect(driver.send(:generate_inst_name)).to match(
333
- /^a-b-c-d-e-f-g-h-i-j-
334
- -[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/x)
335
- end
680
+ describe '#machine_type_url' do
681
+ it "returns a machine type URL" do
682
+ expect(driver).to receive(:config).and_return(machine_type: "machine_type")
683
+ expect(driver.machine_type_url).to eq("zones/test_zone/machineTypes/machine_type")
336
684
  end
337
685
  end
338
686
 
339
- describe '#select_zone' do
340
- context 'when choosing from any region' do
341
- let(:config) do
342
- { region: 'any',
343
- google_client_email: '123456789012@developer.gserviceaccount.com',
344
- google_key_location: '/home/user/gce/123456-privatekey.p12',
345
- google_project: 'alpha-bravo-123'
346
- }
347
- end
687
+ describe '#instance_metadata' do
688
+ it "returns a properly-configured metadata object" do
689
+ item1 = double("item1")
690
+ item2 = double("item2")
691
+ item3 = double("item3")
692
+ metadata = double("metadata")
693
+
694
+ expect(instance).to receive(:name).and_return("instance_name")
695
+ expect(driver).to receive(:env_user).and_return("env_user")
696
+ expect(Google::Apis::ComputeV1::Metadata).to receive(:new).and_return(metadata)
697
+ expect(Google::Apis::ComputeV1::Metadata::Item).to receive(:new).and_return(item1)
698
+ expect(Google::Apis::ComputeV1::Metadata::Item).to receive(:new).and_return(item2)
699
+ expect(Google::Apis::ComputeV1::Metadata::Item).to receive(:new).and_return(item3)
700
+ expect(item1).to receive(:key=).with("created-by")
701
+ expect(item1).to receive(:value=).with("test-kitchen")
702
+ expect(item2).to receive(:key=).with("test-kitchen-instance")
703
+ expect(item2).to receive(:value=).with("instance_name")
704
+ expect(item3).to receive(:key=).with("test-kitchen-user")
705
+ expect(item3).to receive(:value=).with("env_user")
706
+ expect(metadata).to receive(:items=).with([item1, item2, item3])
707
+
708
+ expect(driver.instance_metadata).to eq(metadata)
709
+ end
710
+ end
348
711
 
349
- it 'chooses from all zones' do
350
- expect(driver.send(:select_zone)).to satisfy do |zone|
351
- %w(europe-west1-a us-central1-a us-central1-b
352
- us-central2-a).include?(zone)
353
- end
712
+ describe '#env_user' do
713
+ it "returns the current user from the environment" do
714
+ expect(ENV).to receive(:[]).with("USER").and_return("test_user")
715
+ expect(driver.env_user).to eq("test_user")
716
+ end
717
+
718
+ it "returns 'unknown' if there is no USER present" do
719
+ expect(ENV).to receive(:[]).with("USER").and_return(nil)
720
+ expect(driver.env_user).to eq("unknown")
721
+ end
722
+ end
723
+
724
+ describe '#instance_network_interfaces' do
725
+ it "returns an array containing a properly-formatted interface" do
726
+ interface = double("interface")
727
+
728
+ expect(driver).to receive(:network_url).and_return("network_url")
729
+ expect(driver).to receive(:interface_access_configs).and_return("access_configs")
730
+
731
+ expect(Google::Apis::ComputeV1::NetworkInterface).to receive(:new).and_return(interface)
732
+ expect(interface).to receive(:network=).with("network_url")
733
+ expect(interface).to receive(:access_configs=).with("access_configs")
734
+
735
+ expect(driver.instance_network_interfaces).to eq([interface])
736
+ end
737
+ end
738
+
739
+ describe '#network_url' do
740
+ it "returns a network URL" do
741
+ expect(driver).to receive(:config).and_return(network: "test_network")
742
+ expect(driver.network_url).to eq("projects/test_project/global/networks/test_network")
743
+ end
744
+ end
745
+
746
+ describe '#interface_access_configs' do
747
+ it "returns a properly-configured access config object" do
748
+ access_config = double("access_config")
749
+
750
+ expect(driver).to receive(:config).and_return({})
751
+ expect(Google::Apis::ComputeV1::AccessConfig).to receive(:new).and_return(access_config)
752
+ expect(access_config).to receive(:name=).with("External NAT")
753
+ expect(access_config).to receive(:type=).with("ONE_TO_ONE_NAT")
754
+
755
+ expect(driver.interface_access_configs).to eq([access_config])
756
+ end
757
+
758
+ it "returns an empty array if use_private_ip is true" do
759
+ expect(driver).to receive(:config).and_return(use_private_ip: true)
760
+ expect(driver.interface_access_configs).to eq([])
761
+ end
762
+ end
763
+
764
+ describe '#instance_scheduling' do
765
+ it "returns a properly-configured scheduling object" do
766
+ scheduling = double("scheduling")
767
+
768
+ allow(driver).to receive(:config).and_return(auto_restart: true, preemptible: false)
769
+ expect(driver).to receive(:migrate_setting).and_return("host_maintenance")
770
+ expect(Google::Apis::ComputeV1::Scheduling).to receive(:new).and_return(scheduling)
771
+ expect(scheduling).to receive(:automatic_restart=).with("true")
772
+ expect(scheduling).to receive(:preemptible=).with("false")
773
+ expect(scheduling).to receive(:on_host_maintenance=).with("host_maintenance")
774
+ expect(driver.instance_scheduling).to eq(scheduling)
775
+ end
776
+ end
777
+
778
+ describe '#migrate_setting' do
779
+ it "returns MIGRATE if auto_migrate is true" do
780
+ expect(driver).to receive(:config).and_return(auto_migrate: true)
781
+ expect(driver.migrate_setting).to eq("MIGRATE")
782
+ end
783
+
784
+ it "returns TERMINATE if auto_migrate is false" do
785
+ expect(driver).to receive(:config).and_return(auto_migrate: false)
786
+ expect(driver.migrate_setting).to eq("TERMINATE")
787
+ end
788
+ end
789
+
790
+ describe '#instance_service_accounts' do
791
+ it "returns nil if service_account_scopes is nil" do
792
+ allow(driver).to receive(:config).and_return({})
793
+ expect(driver.instance_service_accounts).to eq(nil)
794
+ end
795
+
796
+ it "returns nil if service_account_scopes is empty" do
797
+ allow(driver).to receive(:config).and_return(service_account_scopes: [])
798
+ expect(driver.instance_service_accounts).to eq(nil)
799
+ end
800
+
801
+ it "returns an array containing a properly-formatted service account" do
802
+ service_account = double("service_account")
803
+
804
+ allow(driver).to receive(:config).and_return(service_account_name: "account_name", service_account_scopes: %w{scope1 scope2})
805
+ expect(Google::Apis::ComputeV1::ServiceAccount).to receive(:new).and_return(service_account)
806
+ expect(service_account).to receive(:email=).with("account_name")
807
+ expect(driver).to receive(:service_account_scope_url).with("scope1").and_return("https://www.googleapis.com/auth/scope1")
808
+ expect(driver).to receive(:service_account_scope_url).with("scope2").and_return("https://www.googleapis.com/auth/scope2")
809
+ expect(service_account).to receive(:scopes=).with([
810
+ "https://www.googleapis.com/auth/scope1",
811
+ "https://www.googleapis.com/auth/scope2",
812
+ ])
813
+
814
+ expect(driver.instance_service_accounts).to eq([service_account])
815
+ end
816
+ end
817
+
818
+ describe '#service_account_scope_url' do
819
+ it "returns the passed-in scope if it already looks like a scope URL" do
820
+ scope = "https://www.googleapis.com/auth/fake_scope"
821
+ expect(driver.service_account_scope_url(scope)).to eq(scope)
822
+ end
823
+
824
+ it "returns a properly-formatted scope URL if a short-name or alias is provided" do
825
+ expect(driver).to receive(:translate_scope_alias).with("scope_alias").and_return("real_scope")
826
+ expect(driver.service_account_scope_url("scope_alias")).to eq("https://www.googleapis.com/auth/real_scope")
827
+ end
828
+ end
829
+
830
+ describe '#translate_scope_alias' do
831
+ it "returns a scope for a given alias" do
832
+ expect(driver.translate_scope_alias("storage-rw")).to eq("devstorage.read_write")
833
+ end
834
+
835
+ it "returns the passed-in scope alias if nothing matches in the alias map" do
836
+ expect(driver.translate_scope_alias("fake_scope")).to eq("fake_scope")
837
+ end
838
+ end
839
+
840
+ describe '#instance_tags' do
841
+ it "returns a properly-formatted tags object" do
842
+ tags_obj = double("tags_obj")
843
+
844
+ expect(driver).to receive(:config).and_return(tags: "test_tags")
845
+ expect(Google::Apis::ComputeV1::Tags).to receive(:new).and_return(tags_obj)
846
+ expect(tags_obj).to receive(:items=).with("test_tags")
847
+
848
+ expect(driver.instance_tags).to eq(tags_obj)
849
+ end
850
+ end
851
+
852
+ describe '#wait_time' do
853
+ it "returns the configured wait time" do
854
+ expect(driver).to receive(:config).and_return(wait_time: 123)
855
+ expect(driver.wait_time).to eq(123)
856
+ end
857
+ end
858
+
859
+ describe '#refresh_rate' do
860
+ it "returns the configured refresh rate" do
861
+ expect(driver).to receive(:config).and_return(refresh_rate: 321)
862
+ expect(driver.refresh_rate).to eq(321)
863
+ end
864
+ end
865
+
866
+ describe '#wait_for_status' do
867
+ let(:item) { double("item") }
868
+
869
+ before do
870
+ allow(driver).to receive(:wait_time).and_return(600)
871
+ allow(driver).to receive(:refresh_rate).and_return(2)
872
+
873
+ # don"t actually sleep
874
+ allow(driver).to receive(:sleep)
875
+ end
876
+
877
+ context "when the items completes normally, 3 loops" do
878
+ it "only refreshes the item 3 times" do
879
+ allow(item).to receive(:status).exactly(3).times.and_return("PENDING", "RUNNING", "DONE")
880
+
881
+ driver.wait_for_status("DONE") { item }
354
882
  end
355
883
  end
356
884
 
357
- context 'when choosing from the "europe-west1" region' do
358
- let(:config) do
359
- { region: 'europe-west1',
360
- google_client_email: '123456789012@developer.gserviceaccount.com',
361
- google_key_location: '/home/user/gce/123456-privatekey.p12',
362
- google_project: 'alpha-bravo-123'
363
- }
885
+ context "when the item is completed on the first loop" do
886
+ it "only refreshes the item 1 time" do
887
+ allow(item).to receive(:status).once.and_return("DONE")
888
+
889
+ driver.wait_for_status("DONE") { item }
364
890
  end
891
+ end
365
892
 
366
- it 'chooses a zone in europe-west1' do
367
- expect(driver.send(:select_zone)).to satisfy do |zone|
368
- %w(europe-west1-a).include?(zone)
369
- end
893
+ context "when the timeout is exceeded" do
894
+ it "prints a warning and exits" do
895
+ allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
896
+ expect(driver).to receive(:error)
897
+ .with("Request did not complete in 600 seconds. Check the Google Cloud Console for more info.")
898
+ expect { driver.wait_for_status("DONE") { item } }.to raise_error(RuntimeError)
370
899
  end
371
900
  end
372
901
 
373
- context 'when choosing from the default "us-central1" region' do
374
- let(:config) do
375
- { region: 'us-central1',
376
- google_client_email: '123456789012@developer.gserviceaccount.com',
377
- google_key_location: '/home/user/gce/123456-privatekey.p12',
378
- google_project: 'alpha-bravo-123'
379
- }
902
+ context "when a non-timeout exception is raised" do
903
+ it "raises the original exception" do
904
+ allow(item).to receive(:status).and_raise(NoMethodError)
905
+ expect { driver.wait_for_status("DONE") { item } }.to raise_error(NoMethodError)
380
906
  end
907
+ end
908
+ end
381
909
 
382
- it 'chooses a zone in us-central1' do
383
- expect(driver.send(:select_zone)).to satisfy do |zone|
384
- %w(us-central1-a us-central1-b us-central2-a).include?(zone)
385
- end
910
+ describe '#wait_for_operation' do
911
+ let(:operation) { double("operation", name: "operation-123") }
386
912
 
387
- end
913
+ it "raises a properly-formatted exception when errors exist" do
914
+ error1 = double("error1", code: "ERROR1", message: "error 1")
915
+ error2 = double("error2", code: "ERROR2", message: "error 2")
916
+ expect(driver).to receive(:wait_for_status).with("DONE")
917
+ expect(driver).to receive(:operation_errors).with("operation-123").and_return([error1, error2])
918
+ expect(driver).to receive(:error).with("ERROR1: error 1")
919
+ expect(driver).to receive(:error).with("ERROR2: error 2")
920
+
921
+ expect { driver.wait_for_operation(operation) }.to raise_error(RuntimeError, "Operation operation-123 failed.")
922
+ end
923
+
924
+ it "does not raise an exception if no errors are encountered" do
925
+ expect(driver).to receive(:wait_for_status).with("DONE")
926
+ expect(driver).to receive(:operation_errors).with("operation-123").and_return([])
927
+ expect(driver).not_to receive(:error)
928
+
929
+ expect { driver.wait_for_operation(operation) }.not_to raise_error
388
930
  end
389
931
  end
390
932
 
391
- describe '#wait_for_up_instance' do
392
- it 'sets the hostname' do
393
- driver.send(:wait_for_up_instance, server, state)
394
- # Mock instance gives us a random IP each time:
395
- expect(state[:hostname]).to match(Resolv::IPv4::Regex)
933
+ describe '#zone_operation' do
934
+ it "fetches the operation from the API and returns it" do
935
+ connection = double("connection")
936
+ expect(driver).to receive(:connection).and_return(connection)
937
+ expect(connection).to receive(:get_zone_operation).with(project, zone, "operation-123").and_return("operation")
938
+ expect(driver.zone_operation("operation-123")).to eq("operation")
939
+ end
940
+ end
941
+
942
+ describe '#operation_errors' do
943
+ let(:operation) { double("operation") }
944
+ let(:error_obj) { double("error_obj") }
945
+
946
+ before do
947
+ expect(driver).to receive(:zone_operation).with("operation-123").and_return(operation)
948
+ end
949
+
950
+ it "returns an empty array if there are no errors" do
951
+ expect(operation).to receive(:error).and_return(nil)
952
+ expect(driver.operation_errors("operation-123")).to eq([])
953
+ end
954
+
955
+ it "returns the errors from the operation if they exist" do
956
+ expect(operation).to receive(:error).twice.and_return(error_obj)
957
+ expect(error_obj).to receive(:errors).and_return("some errors")
958
+ expect(driver.operation_errors("operation-123")).to eq("some errors")
396
959
  end
397
960
  end
398
961
  end