knife-google 1.3.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +5 -13
  2. data/.gitignore +1 -0
  3. data/.travis.yml +15 -3
  4. data/CHANGELOG.md +4 -6
  5. data/Gemfile +3 -9
  6. data/README.md +208 -355
  7. data/RELEASE_NOTES.md +8 -17
  8. data/Rakefile +8 -49
  9. data/knife-google.gemspec +20 -17
  10. data/lib/chef/knife/cloud/google_service.rb +491 -0
  11. data/lib/chef/knife/cloud/google_service_helpers.rb +62 -0
  12. data/lib/chef/knife/cloud/google_service_options.rb +58 -0
  13. data/lib/chef/knife/google_disk_create.rb +40 -44
  14. data/lib/chef/knife/google_disk_delete.rb +22 -40
  15. data/lib/chef/knife/google_disk_list.rb +57 -51
  16. data/lib/chef/knife/google_project_quotas.rb +59 -0
  17. data/lib/chef/knife/google_region_list.rb +43 -102
  18. data/lib/chef/knife/google_region_quotas.rb +77 -0
  19. data/lib/chef/knife/google_server_create.rb +224 -505
  20. data/lib/chef/knife/google_server_delete.rb +20 -78
  21. data/lib/chef/knife/google_server_list.rb +42 -53
  22. data/lib/chef/knife/google_server_show.rb +44 -0
  23. data/lib/chef/knife/google_zone_list.rb +39 -50
  24. data/lib/knife-google/version.rb +3 -2
  25. data/spec/cloud/google_service_helpers_spec.rb +120 -0
  26. data/spec/cloud/google_service_spec.rb +832 -0
  27. data/spec/google_disk_create_spec.rb +72 -0
  28. data/spec/google_disk_delete_spec.rb +64 -0
  29. data/spec/google_disk_list_spec.rb +93 -0
  30. data/spec/google_project_quotas_spec.rb +63 -0
  31. data/spec/google_region_list_spec.rb +65 -0
  32. data/spec/google_region_quotas_spec.rb +108 -0
  33. data/spec/google_server_create_spec.rb +177 -0
  34. data/spec/google_server_delete_spec.rb +39 -0
  35. data/spec/google_server_list_spec.rb +77 -0
  36. data/spec/google_server_show_spec.rb +60 -0
  37. data/spec/google_zone_list_spec.rb +59 -0
  38. metadata +91 -114
  39. data/CONTRIB.md +0 -64
  40. data/lib/chef/knife/google_base.rb +0 -76
  41. data/lib/chef/knife/google_project_list.rb +0 -178
  42. data/lib/chef/knife/google_setup.rb +0 -31
  43. data/lib/google/compute.rb +0 -47
  44. data/lib/google/compute/client.rb +0 -216
  45. data/lib/google/compute/config.rb +0 -23
  46. data/lib/google/compute/creatable_resource_collection.rb +0 -55
  47. data/lib/google/compute/deletable_resource_collection.rb +0 -51
  48. data/lib/google/compute/disk.rb +0 -38
  49. data/lib/google/compute/exception.rb +0 -30
  50. data/lib/google/compute/firewall.rb +0 -65
  51. data/lib/google/compute/global_operation.rb +0 -60
  52. data/lib/google/compute/image.rb +0 -29
  53. data/lib/google/compute/listable_resource_collection.rb +0 -33
  54. data/lib/google/compute/machine_type.rb +0 -36
  55. data/lib/google/compute/mixins/utils.rb +0 -58
  56. data/lib/google/compute/network.rb +0 -29
  57. data/lib/google/compute/project.rb +0 -76
  58. data/lib/google/compute/region.rb +0 -31
  59. data/lib/google/compute/region_operation.rb +0 -62
  60. data/lib/google/compute/resource.rb +0 -81
  61. data/lib/google/compute/resource_collection.rb +0 -78
  62. data/lib/google/compute/server.rb +0 -88
  63. data/lib/google/compute/server/attached_disk.rb +0 -39
  64. data/lib/google/compute/server/network_interface.rb +0 -38
  65. data/lib/google/compute/server/network_interface/access_config.rb +0 -35
  66. data/lib/google/compute/server/serial_port_output.rb +0 -31
  67. data/lib/google/compute/snapshot.rb +0 -30
  68. data/lib/google/compute/version.rb +0 -19
  69. data/lib/google/compute/zone.rb +0 -34
  70. data/lib/google/compute/zone_operation.rb +0 -62
  71. data/spec/chef/knife/google_base_spec.rb +0 -46
  72. data/spec/chef/knife/google_disk_create_spec.rb +0 -37
  73. data/spec/chef/knife/google_disk_delete_spec.rb +0 -64
  74. data/spec/chef/knife/google_disk_list_spec.rb +0 -36
  75. data/spec/chef/knife/google_region_list_spec.rb +0 -32
  76. data/spec/chef/knife/google_server_create_spec.rb +0 -138
  77. data/spec/chef/knife/google_server_delete_spec.rb +0 -127
  78. data/spec/chef/knife/google_server_list_spec.rb +0 -39
  79. data/spec/chef/knife/google_setup_spec.rb +0 -24
  80. data/spec/chef/knife/google_zone_list_spec.rb +0 -32
  81. data/spec/data/client.json +0 -14
  82. data/spec/data/compute-v1.json +0 -6734
  83. data/spec/data/disk.json +0 -14
  84. data/spec/data/firewall.json +0 -13
  85. data/spec/data/global_operation.json +0 -36
  86. data/spec/data/image.json +0 -12
  87. data/spec/data/machine_type.json +0 -24
  88. data/spec/data/network.json +0 -10
  89. data/spec/data/project.json +0 -21
  90. data/spec/data/region.json +0 -23
  91. data/spec/data/serial_port_output.json +0 -5
  92. data/spec/data/server.json +0 -46
  93. data/spec/data/snapshot.json +0 -12
  94. data/spec/data/zone.json +0 -22
  95. data/spec/data/zone_operation.json +0 -36
  96. data/spec/google/compute/disk_spec.rb +0 -115
  97. data/spec/google/compute/firewall_spec.rb +0 -129
  98. data/spec/google/compute/global_operation_spec.rb +0 -62
  99. data/spec/google/compute/image_spec.rb +0 -75
  100. data/spec/google/compute/machine_type_spec.rb +0 -53
  101. data/spec/google/compute/network_spec.rb +0 -68
  102. data/spec/google/compute/project_spec.rb +0 -71
  103. data/spec/google/compute/region_spec.rb +0 -51
  104. data/spec/google/compute/server_spec.rb +0 -118
  105. data/spec/google/compute/snapshot_spec.rb +0 -57
  106. data/spec/google/compute/zone_operation_spec.rb +0 -62
  107. data/spec/google/compute/zone_spec.rb +0 -51
  108. data/spec/spec_helper.rb +0 -45
  109. data/spec/support/mocks.rb +0 -62
  110. data/spec/support/resource_examples.rb +0 -70
  111. data/spec/support/spec_google_base.rb +0 -60
@@ -0,0 +1,832 @@
1
+ #
2
+ # Author:: Chef Partner Engineering (<partnereng@chef.io>)
3
+ # Copyright:: Copyright (c) 2016 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
+ require "chef/knife"
20
+ require "chef/knife/cloud/exceptions"
21
+ require "chef/knife/cloud/google_service"
22
+ require "support/shared_examples_for_service"
23
+
24
+ shared_examples_for "a paginated list fetcher" do |fetch_method, items_method, *args|
25
+ it "retrieves paginated results from the API" do
26
+ expect(service).to receive(:paginated_results).with(fetch_method, items_method, *args)
27
+ subject
28
+ end
29
+
30
+ it "returns the results if they exist" do
31
+ expect(service).to receive(:paginated_results).with(fetch_method, items_method, *args).and_return("results")
32
+ expect(subject).to eq("results")
33
+ end
34
+
35
+ it "returns an empty array if there are no results" do
36
+ expect(service).to receive(:paginated_results).with(fetch_method, items_method, *args).and_return(nil)
37
+ expect(subject).to eq([])
38
+ end
39
+ end
40
+
41
+ describe Chef::Knife::Cloud::GoogleService do
42
+ let(:project) { "test_project" }
43
+ let(:zone) { "test_zone" }
44
+ let(:wait_time) { 123 }
45
+ let(:refresh_rate) { 321 }
46
+ let(:max_pages) { 456 }
47
+ let(:max_pages_size) { 654 }
48
+ let(:connection) { double("connection") }
49
+
50
+ let(:service) do
51
+ Chef::Knife::Cloud::GoogleService.new(
52
+ project: project,
53
+ zone: zone,
54
+ wait_time: wait_time,
55
+ refresh_rate: refresh_rate,
56
+ max_pages: max_pages,
57
+ max_pages_size: max_pages_size
58
+ )
59
+ end
60
+
61
+ before do
62
+ service.ui = Chef::Knife::UI.new($stdout, $stderr, $stdin, {})
63
+ allow(service.ui).to receive(:msg)
64
+ allow(service).to receive(:connection).and_return(connection)
65
+ end
66
+
67
+ describe '#connection' do
68
+ it "returns a properly configured ComputeService" do
69
+ compute_service = double("compute_service")
70
+ allow(service).to receive(:connection).and_call_original
71
+ expect(Google::Apis::ComputeV1::ComputeService).to receive(:new).and_return(compute_service)
72
+ expect(service).to receive(:authorization).and_return("authorization_object")
73
+ expect(compute_service).to receive(:authorization=).with("authorization_object")
74
+ expect(service.connection).to eq(compute_service)
75
+ end
76
+ end
77
+
78
+ describe '#authorization' do
79
+ it "returns a Google::Auth authorization object" do
80
+ auth_object = double("auth_object")
81
+ expect(Google::Auth).to receive(:get_application_default).and_return(auth_object)
82
+ expect(service.authorization).to eq(auth_object)
83
+ end
84
+ end
85
+
86
+ describe '#create_server' do
87
+ it "creates and returns the created instance" do
88
+ create_instance_obj = double("instance_obj")
89
+ create_options = { name: "test_instance" }
90
+ instance = double("instance")
91
+
92
+ expect(service).to receive(:validate_server_create_options!).with(create_options)
93
+ expect(service).to receive(:instance_object_for).with(create_options).and_return(create_instance_obj)
94
+ expect(connection).to receive(:insert_instance).with(project, zone, create_instance_obj).and_return("operation_id")
95
+ expect(service).to receive(:wait_for_operation).with("operation_id")
96
+ expect(service).to receive(:wait_for_status).with("RUNNING")
97
+ expect(service).to receive(:get_server).with("test_instance").and_return(instance)
98
+
99
+ expect(service.create_server(create_options)).to eq(instance)
100
+ end
101
+ end
102
+
103
+ describe '#delete_server' do
104
+ context "when the instance does not exist" do
105
+ before do
106
+ allow(service.ui).to receive(:warn)
107
+ expect(service).to receive(:get_server).and_raise(Google::Apis::ClientError.new("not found"))
108
+ end
109
+
110
+ it "prints a warning to the user" do
111
+ expect(service.ui).to receive(:warn).with("Unable to locate instance test_instance in project #{project}, zone #{zone}")
112
+
113
+ service.delete_server("test_instance")
114
+ end
115
+
116
+ it "does not attempt to delete the instance" do
117
+ expect(connection).not_to receive(:delete_instance)
118
+
119
+ service.delete_server("test_instance")
120
+ end
121
+ end
122
+
123
+ context "when the instance exists" do
124
+ it "confirms the deletion and deletes the instance" do
125
+ instance = double("instance")
126
+ expect(service).to receive(:get_server).with("test_instance").and_return(instance)
127
+ expect(service).to receive(:server_summary).with(instance)
128
+ expect(service.ui).to receive(:confirm)
129
+ expect(connection).to receive(:delete_instance).with(project, zone, "test_instance").and_return("operation-123")
130
+ expect(service).to receive(:wait_for_operation).with("operation-123")
131
+
132
+ service.delete_server("test_instance")
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#get_server' do
138
+ it "returns an instance" do
139
+ expect(connection).to receive(:get_instance).with(project, zone, "test_instance").and_return("instance")
140
+ expect(service.get_server("test_instance")).to eq("instance")
141
+ end
142
+ end
143
+
144
+ describe '#list_zones' do
145
+ subject { service.list_zones }
146
+ it_behaves_like "a paginated list fetcher", :list_zones, :items, "test_project"
147
+ end
148
+
149
+ describe '#list_disks' do
150
+ subject { service.list_disks }
151
+ it_behaves_like "a paginated list fetcher", :list_disks, :items, "test_project", "test_zone"
152
+ end
153
+
154
+ describe '#list_regions' do
155
+ subject { service.list_regions }
156
+ it_behaves_like "a paginated list fetcher", :list_regions, :items, "test_project"
157
+ end
158
+
159
+ describe '#list_project_quotas' do
160
+ let(:response) { double("response") }
161
+
162
+ before do
163
+ expect(service).to receive(:project).and_return(project)
164
+ expect(connection).to receive(:get_project).with(project).and_return(response)
165
+ end
166
+
167
+ it "returns the results if they exist" do
168
+ expect(response).to receive(:quotas).and_return("results")
169
+ expect(service.list_project_quotas).to eq("results")
170
+ end
171
+
172
+ it "returns an empty array if there are no results" do
173
+ expect(response).to receive(:quotas).and_return(nil)
174
+ expect(service.list_project_quotas).to eq([])
175
+ end
176
+ end
177
+
178
+ describe '#validate_server_create_options!' do
179
+ let(:options) do
180
+ {
181
+ machine_type: "test_type",
182
+ network: "test_network",
183
+ public_ip: "public_ip",
184
+ image: "test_image",
185
+ image_project: "test_image_project",
186
+ }
187
+ end
188
+
189
+ before do
190
+ allow(service).to receive(:valid_machine_type?).and_return(true)
191
+ allow(service).to receive(:valid_network?).and_return(true)
192
+ allow(service).to receive(:valid_public_ip_setting?).and_return(true)
193
+ allow(service).to receive(:image_search_for).and_return(true)
194
+ end
195
+
196
+ it "does not raise an exception when all parameters are supplied and accurate" do
197
+ expect { service.validate_server_create_options!(options) }.not_to raise_error
198
+ end
199
+
200
+ it "raises an exception if the machine type is not valid" do
201
+ expect(service).to receive(:valid_machine_type?).with("test_type").and_return(false)
202
+ expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
203
+ end
204
+
205
+ it "raises an exception if the network is not valid" do
206
+ expect(service).to receive(:valid_network?).with("test_network").and_return(false)
207
+ expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
208
+ end
209
+
210
+ it "raises an exception if the public ip setting is not valid" do
211
+ expect(service).to receive(:valid_public_ip_setting?).with("public_ip").and_return(false)
212
+ expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
213
+ end
214
+
215
+ it "raises an exception if the image parameters are not valid" do
216
+ expect(service).to receive(:image_search_for).with("test_image", "test_image_project").and_return(nil)
217
+ expect { service.validate_server_create_options!(options) }.to raise_error(RuntimeError)
218
+ end
219
+ end
220
+
221
+ describe '#check_api_call' do
222
+ it "returns false if the block raises a ClientError" do
223
+ expect(service.check_api_call { raise Google::Apis::ClientError.new("whoops") }).to eq(false)
224
+ end
225
+
226
+ it "raises an exception if the block raises something other than a ClientError" do
227
+ expect { service.check_api_call { raise RuntimeError.new("whoops") } }.to raise_error(RuntimeError)
228
+ end
229
+
230
+ it "returns true if the block does not raise an exception" do
231
+ expect(service.check_api_call { true }).to eq(true)
232
+ end
233
+ end
234
+
235
+ describe '#valid_machine_type?' do
236
+ it "returns false if no matchine type was specified" do
237
+ expect(service.valid_machine_type?(nil)).to eq(false)
238
+ end
239
+
240
+ it "checks the machine type using check_api_call" do
241
+ expect(connection).to receive(:get_machine_type).with(project, zone, "test_type")
242
+ expect(service).to receive(:check_api_call).and_call_original
243
+
244
+ service.valid_machine_type?("test_type")
245
+ end
246
+ end
247
+
248
+ describe '#valid_network?' do
249
+ it "returns false if no network was specified" do
250
+ expect(service.valid_network?(nil)).to eq(false)
251
+ end
252
+
253
+ it "checks the network using check_api_call" do
254
+ expect(connection).to receive(:get_network).with(project, "test_network")
255
+ expect(service).to receive(:check_api_call).and_call_original
256
+
257
+ service.valid_network?("test_network")
258
+ end
259
+ end
260
+
261
+ describe '#image_exist?' do
262
+ it "checks the image using check_api_call" do
263
+ expect(connection).to receive(:get_image).with("image_project", "image_name")
264
+ expect(service).to receive(:check_api_call).and_call_original
265
+
266
+ service.image_exist?("image_project", "image_name")
267
+ end
268
+ end
269
+
270
+ describe '#valid_public_ip_setting?' do
271
+ it "returns true if the public_ip is nil" do
272
+ expect(service.valid_public_ip_setting?(nil)).to eq(true)
273
+ end
274
+
275
+ it "returns true if the public_ip is ephemeral" do
276
+ expect(service.valid_public_ip_setting?("EPHEMERAL")).to eq(true)
277
+ end
278
+
279
+ it "returns true if the public_ip is none" do
280
+ expect(service.valid_public_ip_setting?("NONE")).to eq(true)
281
+ end
282
+
283
+ it "returns true if the public_ip is a valid IP address" do
284
+ expect(service).to receive(:valid_ip_address?).with("1.2.3.4").and_return(true)
285
+ expect(service.valid_public_ip_setting?("1.2.3.4")).to eq(true)
286
+ end
287
+
288
+ it "returns false if it is not nil, ephemeral, none, or a valid IP address" do
289
+ expect(service).to receive(:valid_ip_address?).with("not_an_ip").and_return(false)
290
+ expect(service.valid_public_ip_setting?("not_an_ip")).to eq(false)
291
+ end
292
+ end
293
+
294
+ describe '#valid_ip_address' do
295
+ it "returns false if IPAddr is unable to parse the address" do
296
+ expect(IPAddr).to receive(:new).with("not_an_ip").and_raise(IPAddr::InvalidAddressError)
297
+ expect(service.valid_ip_address?("not_an_ip")).to eq(false)
298
+ end
299
+
300
+ it "returns true if IPAddr can parse the address" do
301
+ expect(IPAddr).to receive(:new).with("1.2.3.4")
302
+ expect(service.valid_ip_address?("1.2.3.4")).to eq(true)
303
+ end
304
+ end
305
+
306
+ describe '#instance_object_for' do
307
+ let(:instance_object) { double("instance_object") }
308
+ let(:options) do
309
+ {
310
+ name: "test_instance",
311
+ can_ip_forward: "ip_forwarding",
312
+ machine_type: "test_machine_type",
313
+ metadata: "test_metadata",
314
+ tags: "test_tags",
315
+ }
316
+ end
317
+
318
+ before do
319
+ expect(service).to receive(:instance_disks_for).with(options).and_return("test_disks")
320
+ expect(service).to receive(:machine_type_url_for).with("test_machine_type").and_return("test_machine_type_url")
321
+ expect(service).to receive(:instance_metadata_for).with("test_metadata").and_return("test_metadata_obj")
322
+ expect(service).to receive(:instance_network_interfaces_for).with(options).and_return("test_network_interfaces")
323
+ expect(service).to receive(:instance_scheduling_for).with(options).and_return("test_scheduling")
324
+ allow(service).to receive(:instance_service_accounts_for).with(options).at_least(:once).and_return("test_service_accounts")
325
+ expect(service).to receive(:instance_tags_for).with("test_tags").and_return("test_tags_obj")
326
+ end
327
+
328
+ it "builds and returns a valid object for creating an instance" do
329
+ expect(Google::Apis::ComputeV1::Instance).to receive(:new).and_return(instance_object)
330
+ expect(instance_object).to receive(:name=).with("test_instance")
331
+ expect(instance_object).to receive(:can_ip_forward=).with("ip_forwarding")
332
+ expect(instance_object).to receive(:disks=).with("test_disks")
333
+ expect(instance_object).to receive(:machine_type=).with("test_machine_type_url")
334
+ expect(instance_object).to receive(:metadata=).with("test_metadata_obj")
335
+ expect(instance_object).to receive(:network_interfaces=).with("test_network_interfaces")
336
+ expect(instance_object).to receive(:scheduling=).with("test_scheduling")
337
+ expect(instance_object).to receive(:service_accounts=).with("test_service_accounts")
338
+ expect(instance_object).to receive(:tags=).with("test_tags_obj")
339
+
340
+ expect(service.instance_object_for(options)).to eq(instance_object)
341
+ end
342
+
343
+ it "does not include service accounts if none exist" do
344
+ expect(service).to receive(:instance_service_accounts_for).with(options).and_return(nil)
345
+ expect(instance_object).not_to receive(:service_accounts=)
346
+
347
+ service.instance_object_for(options)
348
+ end
349
+ end
350
+
351
+ describe '#instance_disks_for' do
352
+
353
+ before do
354
+ expect(service).to receive(:instance_boot_disk_for).with(options).and_return("boot_disk")
355
+ end
356
+
357
+ context "when no additional disks are to be attached" do
358
+ let(:options) { { additional_disks: [] } }
359
+
360
+ it "returns an array containing only the boot disk" do
361
+ expect(service.instance_disks_for(options)).to eq(%w{boot_disk})
362
+ end
363
+ end
364
+
365
+ context "when additional disks are to be attached and they exist" do
366
+ let(:options) { { additional_disks: %w{disk1 disk2} } }
367
+
368
+ it "returns an array containing all three disks" do
369
+ disk1 = double("disk1")
370
+ disk2 = double("disk2")
371
+ attached_disk1 = double("attached_disk1")
372
+ attached_disk2 = double("attached_disk2")
373
+
374
+ expect(connection).to receive(:get_disk).with(project, zone, "disk1").and_return(disk1)
375
+ expect(connection).to receive(:get_disk).with(project, zone, "disk2").and_return(disk2)
376
+ expect(disk1).to receive(:self_link).and_return("disk1_url")
377
+ expect(disk2).to receive(:self_link).and_return("disk2_url")
378
+ expect(Google::Apis::ComputeV1::AttachedDisk).to receive(:new).and_return(attached_disk1)
379
+ expect(Google::Apis::ComputeV1::AttachedDisk).to receive(:new).and_return(attached_disk2)
380
+ expect(attached_disk1).to receive(:source=).and_return("disk1_url")
381
+ expect(attached_disk2).to receive(:source=).and_return("disk2_url")
382
+ expect(service.instance_disks_for(options)).to eq(["boot_disk", attached_disk1, attached_disk2])
383
+ end
384
+ end
385
+
386
+ context "when an additional disk is to be attached but does not exist" do
387
+ let(:options) { { additional_disks: %w{bad_disk} } }
388
+
389
+ it "raises an exception" do
390
+ expect(connection).to receive(:get_disk).with(project, zone, "bad_disk").and_raise(Google::Apis::ClientError.new("disk not found"))
391
+ expect(service.ui).to receive(:error).with("Unable to attach disk bad_disk to the instance: disk not found")
392
+ expect { service.instance_disks_for(options) }.to raise_error(Google::Apis::ClientError)
393
+ end
394
+ end
395
+ end
396
+
397
+ describe '#instance_boot_disk_for' do
398
+ it "sets up a disk object and returns it" do
399
+ disk = double("disk")
400
+ params = double("params")
401
+ options = {
402
+ boot_disk_autodelete: "autodelete_param",
403
+ boot_disk_size: "disk_size",
404
+ boot_disk_ssd: "disk_ssd",
405
+ image: "disk_image",
406
+ image_project: "disk_image_project",
407
+ }
408
+
409
+ expect(service).to receive(:boot_disk_name_for).with(options).and_return("disk_name")
410
+ expect(service).to receive(:boot_disk_type_for).with(options).and_return("disk_type")
411
+ expect(service).to receive(:disk_type_url_for).with("disk_type").and_return("disk_type_url")
412
+ expect(service).to receive(:image_search_for).with("disk_image", "disk_image_project").and_return("source_image")
413
+
414
+ expect(Google::Apis::ComputeV1::AttachedDisk).to receive(:new).and_return(disk)
415
+ expect(Google::Apis::ComputeV1::AttachedDiskInitializeParams).to receive(:new).and_return(params)
416
+ expect(disk).to receive(:boot=).with(true)
417
+ expect(disk).to receive(:auto_delete=).with("autodelete_param")
418
+ expect(disk).to receive(:initialize_params=).with(params)
419
+ expect(params).to receive(:disk_name=).with("disk_name")
420
+ expect(params).to receive(:disk_size_gb=).with("disk_size")
421
+ expect(params).to receive(:disk_type=).with("disk_type_url")
422
+ expect(params).to receive(:source_image=).with("source_image")
423
+
424
+ expect(service.instance_boot_disk_for(options)).to eq(disk)
425
+ end
426
+ end
427
+
428
+ describe '#boot_disk_type_for' do
429
+ it "returns pd-ssd if boot_disk_ssd is true" do
430
+ expect(service.boot_disk_type_for(boot_disk_ssd: true)).to eq("pd-ssd")
431
+ end
432
+
433
+ it "returns pd-standard if boot_disk_ssd is false" do
434
+ expect(service.boot_disk_type_for(boot_disk_ssd: false)).to eq("pd-standard")
435
+ end
436
+ end
437
+
438
+ describe '#image_search_for' do
439
+ context "when the user supplies an image project" do
440
+ it "returns the image URL based on the image project" do
441
+ expect(service).to receive(:image_url_for).with("test_project", "test_image").and_return("image_url")
442
+ expect(service.image_search_for("test_image", "test_project")).to eq("image_url")
443
+ end
444
+ end
445
+
446
+ context "when the user does not supply an image project" do
447
+ context "when the image exists in the user's project" do
448
+ it "returns the image URL" do
449
+ expect(service).to receive(:image_url_for).with(project, "test_image").and_return("image_url")
450
+ expect(service.image_search_for("test_image", nil)).to eq("image_url")
451
+ end
452
+ end
453
+
454
+ context "when the image does not exist in the user's project" do
455
+ before do
456
+ expect(service).to receive(:image_url_for).with(project, "test_image").and_return(nil)
457
+ end
458
+
459
+ context "when the image matches a known public project" do
460
+ it "returns the image URL from the public project" do
461
+ expect(service).to receive(:public_project_for_image).with("test_image").and_return("public_project")
462
+ expect(service).to receive(:image_url_for).with("public_project", "test_image").and_return("image_url")
463
+ expect(service.image_search_for("test_image", nil)).to eq("image_url")
464
+ end
465
+ end
466
+
467
+ context "when the image does not match a known project" do
468
+ it "returns nil" do
469
+ expect(service).to receive(:public_project_for_image).with("test_image").and_return(nil)
470
+ expect(service).not_to receive(:image_url_for)
471
+ expect(service.image_search_for("test_image", nil)).to eq(nil)
472
+ end
473
+ end
474
+ end
475
+ end
476
+ end
477
+
478
+ describe '#image_url_for' do
479
+ it "returns nil if the image does not exist" do
480
+ expect(service).to receive(:image_exist?).with("image_project", "image_name").and_return(false)
481
+ expect(service.image_url_for("image_project", "image_name")).to eq(nil)
482
+ end
483
+
484
+ it "returns a properly formatted image URL if the image exists" do
485
+ expect(service).to receive(:image_exist?).with("image_project", "image_name").and_return(true)
486
+ expect(service.image_url_for("image_project", "image_name")).to eq("projects/image_project/global/images/image_name")
487
+ end
488
+ end
489
+
490
+ describe '#boot_disk_name_for' do
491
+ it "returns the boot disk name if supplied by the user" do
492
+ options = { name: "instance_name", boot_disk_name: "disk_name" }
493
+ expect(service.boot_disk_name_for(options)).to eq("disk_name")
494
+ end
495
+
496
+ it "returns the instance name if the boot disk name is not supplied" do
497
+ options = { name: "instance_name" }
498
+ expect(service.boot_disk_name_for(options)).to eq("instance_name")
499
+ end
500
+ end
501
+
502
+ describe '#machine_type_url_for' do
503
+ it "returns a properly-formatted machine type URL" do
504
+ expect(service.machine_type_url_for("test_type")).to eq("zones/test_zone/machineTypes/test_type")
505
+ end
506
+ end
507
+
508
+ describe '#instance_metadata_for' do
509
+ it "returns nil if the passed-in metadata is nil" do
510
+ expect(service.instance_metadata_for(nil)).to eq(nil)
511
+ end
512
+
513
+ it "returns nil if the passed-in metadata is empty" do
514
+ expect(service.instance_metadata_for([])).to eq(nil)
515
+ end
516
+
517
+ it "returns a properly-formatted metadata object if metadata is passed in" do
518
+ metadata = { "key1" => "value1", "key2" => "value2" }
519
+ metadata_obj = double("metadata_obj")
520
+ item_1 = double("item_1")
521
+ item_2 = double("item_2")
522
+
523
+ expect(Google::Apis::ComputeV1::Metadata).to receive(:new).and_return(metadata_obj)
524
+ expect(Google::Apis::ComputeV1::Metadata::Item).to receive(:new).and_return(item_1)
525
+ expect(Google::Apis::ComputeV1::Metadata::Item).to receive(:new).and_return(item_2)
526
+ expect(item_1).to receive(:key=).with("key1")
527
+ expect(item_1).to receive(:value=).with("value1")
528
+ expect(item_2).to receive(:key=).with("key2")
529
+ expect(item_2).to receive(:value=).with("value2")
530
+ expect(metadata_obj).to receive(:items=).with([item_1, item_2])
531
+
532
+ expect(service.instance_metadata_for(metadata)).to eq(metadata_obj)
533
+ end
534
+ end
535
+
536
+ describe '#instance_network_interfaces_for' do
537
+ it "returns an array containing a properly-formatted interface" do
538
+ interface = double("interface")
539
+ options = { network: "test_network", public_ip: "public_ip" }
540
+
541
+ expect(service).to receive(:network_url_for).with("test_network").and_return("network_url")
542
+ expect(service).to receive(:instance_access_configs_for).with("public_ip").and_return("access_configs")
543
+
544
+ expect(Google::Apis::ComputeV1::NetworkInterface).to receive(:new).and_return(interface)
545
+ expect(interface).to receive(:network=).with("network_url")
546
+ expect(interface).to receive(:access_configs=).with("access_configs")
547
+
548
+ expect(service.instance_network_interfaces_for(options)).to eq([interface])
549
+ end
550
+ end
551
+
552
+ describe '#network_url_for' do
553
+ it "returns a properly-formatted network URL" do
554
+ expect(service.network_url_for("test_network")).to eq("projects/test_project/global/networks/test_network")
555
+ end
556
+ end
557
+
558
+ describe '#instance_scheduling_for' do
559
+ it "returns a properly-formatted scheduling object" do
560
+ scheduling = double("scheduling")
561
+ options = { auto_restart: "auto_restart", auto_migrate: "auto_migrate" }
562
+
563
+ expect(service).to receive(:migrate_setting_for).with("auto_migrate").and_return("host_maintenance")
564
+ expect(Google::Apis::ComputeV1::Scheduling).to receive(:new).and_return(scheduling)
565
+ expect(scheduling).to receive(:automatic_restart=).with("auto_restart")
566
+ expect(scheduling).to receive(:on_host_maintenance=).with("host_maintenance")
567
+
568
+ expect(service.instance_scheduling_for(options)).to eq(scheduling)
569
+ end
570
+ end
571
+
572
+ describe '#migrate_setting_for' do
573
+ it "returns MIGRATE when auto_migrate is true" do
574
+ expect(service.migrate_setting_for(true)).to eq("MIGRATE")
575
+ end
576
+
577
+ it "returns TERMINATE when auto_migrate is false" do
578
+ expect(service.migrate_setting_for(false)).to eq("TERMINATE")
579
+ end
580
+ end
581
+
582
+ describe '#instance_service_accounts_for' do
583
+ it "returns nil if service_account_scopes is nil" do
584
+ expect(service.instance_service_accounts_for({})).to eq(nil)
585
+ end
586
+
587
+ it "returns nil if service_account_scopes is empty" do
588
+ expect(service.instance_service_accounts_for({ service_account_scopes: [] })).to eq(nil)
589
+ end
590
+
591
+ it "returns an array containing a properly-formatted service account" do
592
+ service_account = double("service_account")
593
+ options = { service_account_name: "account_name", service_account_scopes: %w{scope1 scope2} }
594
+
595
+ expect(Google::Apis::ComputeV1::ServiceAccount).to receive(:new).and_return(service_account)
596
+ expect(service_account).to receive(:email=).with("account_name")
597
+ expect(service_account).to receive(:scopes=).with([
598
+ "https://www.googleapis.com/auth/scope1",
599
+ "https://www.googleapis.com/auth/scope2",
600
+ ])
601
+
602
+ expect(service.instance_service_accounts_for(options)).to eq([service_account])
603
+ end
604
+ end
605
+
606
+ describe '#instance_tags_for' do
607
+ it "returns nil if tags is nil" do
608
+ expect(service.instance_tags_for(nil)).to eq(nil)
609
+ end
610
+
611
+ it "returns nil if tags is empty" do
612
+ expect(service.instance_tags_for([])).to eq(nil)
613
+ end
614
+
615
+ it "returns a properly-formatted tags object" do
616
+ tags_obj = double("tags_obj")
617
+
618
+ expect(Google::Apis::ComputeV1::Tags).to receive(:new).and_return(tags_obj)
619
+ expect(tags_obj).to receive(:items=).with("test_tags")
620
+
621
+ expect(service.instance_tags_for("test_tags")).to eq(tags_obj)
622
+ end
623
+ end
624
+
625
+ describe '#network_for' do
626
+ it "returns the network name if it exists" do
627
+ interface = double("interface", network: "/some/path/to/default_network")
628
+ instance = double("instance", network_interfaces: [interface])
629
+
630
+ expect(service.network_for(instance)).to eq("default_network")
631
+ end
632
+
633
+ it "returns 'unknown' if the network cannot be found" do
634
+ instance = double("instance")
635
+ expect(instance).to receive(:network_interfaces).and_raise(NoMethodError)
636
+
637
+ expect(service.network_for(instance)).to eq("unknown")
638
+ end
639
+ end
640
+
641
+ describe '#machine_type_for' do
642
+ it "returns the machine type name" do
643
+ instance = double("instance", machine_type: "/some/path/to/test_type")
644
+ expect(service.machine_type_for(instance)).to eq("test_type")
645
+ end
646
+ end
647
+
648
+ describe '#public_project_for_image' do
649
+ {
650
+ "centos" => "centos-cloud",
651
+ "container-vm" => "google-containers",
652
+ "coreos" => "coreos-cloud",
653
+ "debian" => "debian-cloud",
654
+ "opensuse-cloud" => "opensuse-cloud",
655
+ "rhel" => "rhel-cloud",
656
+ "sles" => "suse-cloud",
657
+ "ubuntu" => "ubuntu-os-cloud",
658
+ "windows" => "windows-cloud",
659
+ }.each do |image_name, project_name|
660
+ it "returns project #{project_name} for an image named #{image_name}" do
661
+ expect(service.public_project_for_image(image_name)).to eq(project_name)
662
+ end
663
+ end
664
+ end
665
+
666
+ describe '#disk_type_url_for' do
667
+ it "returns a properly-formatted disk type URL" do
668
+ expect(service.disk_type_url_for("disk_type")).to eq("zones/test_zone/diskTypes/disk_type")
669
+ end
670
+ end
671
+
672
+ describe '#paginated_results' do
673
+ let(:response) { double("response") }
674
+ let(:api_method) { :list_stuff }
675
+ let(:items_method) { :items }
676
+ let(:args) { %w{arg1 arg2} }
677
+ let(:max_pages) { 5 }
678
+ let(:max_page_size) { 100 }
679
+
680
+ subject { service.paginated_results(api_method, items_method, *args) }
681
+
682
+ before do
683
+ allow(response).to receive(:next_page_token)
684
+ allow(service).to receive(:max_pages).and_return(max_pages)
685
+ allow(service).to receive(:max_page_size).and_return(max_page_size)
686
+ end
687
+
688
+ context "when the response has no items" do
689
+ it "returns an empty array" do
690
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: nil).and_return(response)
691
+ expect(response).to receive(:items).and_return(nil)
692
+ expect(subject).to eq([])
693
+ end
694
+ end
695
+
696
+ context "when the response has items with no additional pages" do
697
+ it "calls the API once and returns the fetched results" do
698
+ expect(response).to receive(:items).and_return(%w{item1 item2})
699
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: nil).and_return(response)
700
+ expect(subject).to eq(%w{item1 item2})
701
+ end
702
+ end
703
+
704
+ context "when the response has items spanning 3 pages" do
705
+
706
+ it "calls the API 3 times and returns the results" do
707
+ response1 = double("response1", items: %w{item1 item2}, next_page_token: "page2")
708
+ response2 = double("response2", items: %w{item3 item4}, next_page_token: "page3")
709
+ response3 = double("response3", items: %w{item5 item6}, next_page_token: nil)
710
+
711
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: nil).and_return(response1)
712
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page2").and_return(response2)
713
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page3").and_return(response3)
714
+ expect(subject).to eq(%w{item1 item2 item3 item4 item5 item6})
715
+ end
716
+ end
717
+
718
+ context "when the response has items spanning more than max allowed pages" do
719
+ it "only calls the API the maximum allow number of times and returns results" do
720
+ response1 = double("response1", items: %w{item1}, next_page_token: "page2")
721
+ response2 = double("response2", items: %w{item2}, next_page_token: "page3")
722
+ response3 = double("response3", items: %w{item3}, next_page_token: "page4")
723
+ response4 = double("response4", items: %w{item4}, next_page_token: "page5")
724
+ response5 = double("response5", items: %w{item5}, next_page_token: "page6")
725
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: nil).and_return(response1)
726
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page2").and_return(response2)
727
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page3").and_return(response3)
728
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page4").and_return(response4)
729
+ expect(connection).to receive(:list_stuff).with(*args, max_results: max_page_size, page_token: "page5").and_return(response5)
730
+ expect(service.ui).to receive(:warn).with("Max pages (5) reached, but more results exist - truncating results...")
731
+ expect(subject).to eq(%w{item1 item2 item3 item4 item5})
732
+ end
733
+ end
734
+ end
735
+
736
+ describe '#wait_for_status' do
737
+ let(:item) { double("item") }
738
+
739
+ before do
740
+ allow(service).to receive(:wait_time).and_return(600)
741
+ allow(service).to receive(:refresh_rate).and_return(2)
742
+
743
+ # muffle any stdout output from this method
744
+ allow(service).to receive(:print)
745
+
746
+ # don"t actually sleep
747
+ allow(service).to receive(:sleep)
748
+ end
749
+
750
+ context "when the items completes normally, 3 loops" do
751
+ it "only refreshes the item 3 times" do
752
+ allow(item).to receive(:status).exactly(3).times.and_return("PENDING", "RUNNING", "DONE")
753
+
754
+ service.wait_for_status("DONE") { item }
755
+ end
756
+ end
757
+
758
+ context "when the item is completed on the first loop" do
759
+ it "only refreshes the item 1 time" do
760
+ allow(item).to receive(:status).once.and_return("DONE")
761
+
762
+ service.wait_for_status("DONE") { item }
763
+ end
764
+ end
765
+
766
+ context "when the timeout is exceeded" do
767
+ it "prints a warning and exits" do
768
+ allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
769
+ expect(service.ui).to receive(:error)
770
+ .with("Request did not complete in 600 seconds. Check the Google Cloud Console for more info.")
771
+ expect { service.wait_for_status("DONE") { item } }.to raise_error(SystemExit)
772
+ end
773
+ end
774
+
775
+ context "when a non-timeout exception is raised" do
776
+ it "raises the original exception" do
777
+ allow(item).to receive(:status).and_raise(RuntimeError)
778
+ expect { service.wait_for_status("DONE") { item } }.to raise_error(RuntimeError)
779
+ end
780
+ end
781
+ end
782
+
783
+ describe '#wait_for_operation' do
784
+ let(:operation) { double("operation", name: "operation-123") }
785
+
786
+ it "raises a properly-formatted exception when errors exist" do
787
+ error1 = double("error1", code: "ERROR1", message: "error 1")
788
+ error2 = double("error2", code: "ERROR2", message: "error 2")
789
+ expect(service).to receive(:wait_for_status).with("DONE")
790
+ expect(service).to receive(:operation_errors).with("operation-123").and_return([error1, error2])
791
+ expect(service.ui).to receive(:error).with("#{service.ui.color("ERROR1", :bold)}: error 1")
792
+ expect(service.ui).to receive(:error).with("#{service.ui.color("ERROR2", :bold)}: error 2")
793
+
794
+ expect { service.wait_for_operation(operation) }.to raise_error(RuntimeError, "Operation operation-123 failed.")
795
+ end
796
+
797
+ it "does not raise an exception if no errors are encountered" do
798
+ expect(service).to receive(:wait_for_status).with("DONE")
799
+ expect(service).to receive(:operation_errors).with("operation-123").and_return([])
800
+ expect(service.ui).not_to receive(:error)
801
+
802
+ expect { service.wait_for_operation(operation) }.not_to raise_error
803
+ end
804
+ end
805
+
806
+ describe '#zone_operation' do
807
+ it "fetches the operation from the API and returns it" do
808
+ expect(connection).to receive(:get_zone_operation).with(project, zone, "operation-123").and_return("operation")
809
+ expect(service.zone_operation("operation-123")).to eq("operation")
810
+ end
811
+ end
812
+
813
+ describe '#operation_errors' do
814
+ let(:operation) { double("operation") }
815
+ let(:error_obj) { double("error_obj") }
816
+
817
+ before do
818
+ expect(service).to receive(:zone_operation).with("operation-123").and_return(operation)
819
+ end
820
+
821
+ it "returns an empty array if there are no errors" do
822
+ expect(operation).to receive(:error).and_return(nil)
823
+ expect(service.operation_errors("operation-123")).to eq([])
824
+ end
825
+
826
+ it "returns the errors from the operation if they exist" do
827
+ expect(operation).to receive(:error).twice.and_return(error_obj)
828
+ expect(error_obj).to receive(:errors).and_return("some errors")
829
+ expect(service.operation_errors("operation-123")).to eq("some errors")
830
+ end
831
+ end
832
+ end