kitchen-google 0.3.0 → 1.0.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.
@@ -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