knife-google 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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