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