kitchen-google-as 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2015 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Kitchen
20
+ module Driver
21
+ GCE_VERSION = "1.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