kitchen-google 3.0.2 → 3.1.1
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.
- checksums.yaml +4 -4
- data/lib/kitchen/driver/gce.rb +285 -43
- data/lib/kitchen/driver/gce_version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 915d1d94065a49854c4c040df37b17d4a1e6853bbffd4872bb6e94d255e070bf
|
|
4
|
+
data.tar.gz: ab9bbf5503260572daf07e281d7087db57110b27ef25fc47f1879cfcc8a1e75d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddba9a545b204a39596ed608a310e6714976bb1553af5ab18df6fb4bb135f323c18e5cba31097f7ecb603f1366bef4cfbd03c70b761904b26d16183db47d1b39
|
|
7
|
+
data.tar.gz: 66f9605d7a1c529939b3aa1fe0ea375bb1eac6a9689b5ea6cc6427d8f70bf8fdfed63f245f6daab76bca11ef228b52c2e62a989faf1c143ca8511e719f8b1b5b
|
data/lib/kitchen/driver/gce.rb
CHANGED
|
@@ -21,6 +21,7 @@ require "kitchen"
|
|
|
21
21
|
require_relative "gce_version"
|
|
22
22
|
require_relative "gce/windows_password"
|
|
23
23
|
require "securerandom" unless defined?(SecureRandom)
|
|
24
|
+
require "time" unless defined?(Time.iso8601)
|
|
24
25
|
require "timeout" unless defined?(Timeout)
|
|
25
26
|
|
|
26
27
|
module Kitchen
|
|
@@ -43,6 +44,12 @@ module Kitchen
|
|
|
43
44
|
# @return [Hash] the Test Kitchen state hash for the action in progress
|
|
44
45
|
attr_accessor :state
|
|
45
46
|
|
|
47
|
+
# @!method state=(value)
|
|
48
|
+
# Stores the Test Kitchen state hash for the action in progress.
|
|
49
|
+
#
|
|
50
|
+
# @param value [Hash] the state hash to expose to the helper methods
|
|
51
|
+
# @return [Hash] the assigned state hash
|
|
52
|
+
|
|
46
53
|
# Maps the short scope aliases accepted by `gcloud` onto the scope
|
|
47
54
|
# segment of their fully-qualified OAuth 2.0 URL.
|
|
48
55
|
#
|
|
@@ -105,10 +112,18 @@ module Kitchen
|
|
|
105
112
|
default_config :metadata, {}
|
|
106
113
|
default_config :labels, {}
|
|
107
114
|
|
|
115
|
+
# Pattern a GCE resource name must match in full. GCE applies the same
|
|
116
|
+
# pattern to instance names and disk names alike.
|
|
117
|
+
#
|
|
118
|
+
# @return [Regexp] the permitted name pattern
|
|
119
|
+
RESOURCE_NAME_REGEX = /(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)/
|
|
120
|
+
|
|
108
121
|
# Pattern a GCE disk name must match in full.
|
|
109
122
|
#
|
|
123
|
+
# @deprecated Use {RESOURCE_NAME_REGEX}, which is the same pattern and
|
|
124
|
+
# says what it actually covers.
|
|
110
125
|
# @return [Regexp] the permitted disk-name pattern
|
|
111
|
-
DISK_NAME_REGEX =
|
|
126
|
+
DISK_NAME_REGEX = RESOURCE_NAME_REGEX
|
|
112
127
|
|
|
113
128
|
# Longest instance name GCE accepts.
|
|
114
129
|
#
|
|
@@ -173,7 +188,8 @@ module Kitchen
|
|
|
173
188
|
# before the error is re-raised.
|
|
174
189
|
#
|
|
175
190
|
# @param state [Hash] the Test Kitchen state hash, mutated in place with
|
|
176
|
-
# `:server_name`, `:hostname` and `:
|
|
191
|
+
# `:server_name`, `:hostname`, `:zone` and `:created_disks`, plus
|
|
192
|
+
# `:password` for Windows guests
|
|
177
193
|
# @return [void]
|
|
178
194
|
# @raise [StandardError] if instance creation fails for any reason
|
|
179
195
|
def create(state)
|
|
@@ -182,20 +198,28 @@ module Kitchen
|
|
|
182
198
|
|
|
183
199
|
validate!
|
|
184
200
|
|
|
201
|
+
# The disk configuration has to be settled first: the instance name is
|
|
202
|
+
# budgeted against the longest disk name derived from it.
|
|
203
|
+
create_disks_config
|
|
204
|
+
|
|
185
205
|
server_name = generate_server_name
|
|
186
206
|
|
|
187
|
-
|
|
207
|
+
# Pin the zone before anything billable exists. In region mode it is
|
|
208
|
+
# picked at random, so cleanup would otherwise have no way to know
|
|
209
|
+
# where to look for the disks created below.
|
|
210
|
+
state[:zone] = zone
|
|
211
|
+
|
|
212
|
+
# Record the name before the request goes out, not after it returns.
|
|
213
|
+
# GCE starts billing as soon as the insert is accepted, and it may
|
|
214
|
+
# accept one whose response never reaches us -- an interrupted or
|
|
215
|
+
# timed-out request can leave an instance running that the state file
|
|
216
|
+
# knows nothing about, holding disks that cleanup then cannot delete.
|
|
217
|
+
# `destroy` copes with a name that never became an instance.
|
|
218
|
+
state[:server_name] = server_name
|
|
188
219
|
|
|
189
220
|
info("Creating GCE instance <#{server_name}> in project #{project}, zone #{zone}...")
|
|
190
221
|
operation = connection.insert_instance(project, zone, create_instance_object(server_name))
|
|
191
222
|
|
|
192
|
-
# GCE starts billing for the instance as soon as the insert is
|
|
193
|
-
# accepted, so record it before waiting on the operation. Anything that
|
|
194
|
-
# goes wrong from here on can then be torn down by the rescue below,
|
|
195
|
-
# and by `kitchen destroy` if the process does not survive to run it.
|
|
196
|
-
state[:server_name] = server_name
|
|
197
|
-
state[:zone] = zone
|
|
198
|
-
|
|
199
223
|
wait_for_operation(operation)
|
|
200
224
|
|
|
201
225
|
state[:hostname] = ip_address_for(server_instance(server_name))
|
|
@@ -220,33 +244,101 @@ module Kitchen
|
|
|
220
244
|
raise
|
|
221
245
|
end
|
|
222
246
|
|
|
223
|
-
# Destroys the GCE instance recorded in the state file
|
|
247
|
+
# Destroys the GCE instance recorded in the state file, together with any
|
|
248
|
+
# standalone disks an earlier create left behind.
|
|
249
|
+
#
|
|
250
|
+
# An instance that no longer exists in GCE is treated as already
|
|
251
|
+
# destroyed, but the state file is still cleared: a create that fails
|
|
252
|
+
# after `insert_instance` records a server that may never have come into
|
|
253
|
+
# being, and leaving the name behind would make {#create}'s idempotency
|
|
254
|
+
# guard skip every subsequent retry.
|
|
224
255
|
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
256
|
+
# Disks are deleted after the instance, which holds them until it is
|
|
257
|
+
# gone. A create interrupted before it reached `insert_instance` records
|
|
258
|
+
# no server at all, so they are cleaned up whether or not there is one.
|
|
227
259
|
#
|
|
228
260
|
# @param state [Hash] the Test Kitchen state hash, mutated in place to
|
|
229
|
-
# remove `:server_name`, `:hostname` and `:
|
|
261
|
+
# remove `:server_name`, `:hostname`, `:zone` and `:created_disks`
|
|
230
262
|
# @return [void]
|
|
231
263
|
def destroy(state)
|
|
232
264
|
@state = state
|
|
233
265
|
server_name = state[:server_name]
|
|
234
|
-
return if server_name.nil?
|
|
235
266
|
|
|
236
|
-
unless
|
|
237
|
-
|
|
238
|
-
|
|
267
|
+
unless server_name.nil?
|
|
268
|
+
if server_exist?(server_name)
|
|
269
|
+
info("Destroying GCE instance <#{server_name}>...")
|
|
270
|
+
wait_for_operation(connection.delete_instance(project, zone, server_name))
|
|
271
|
+
info("GCE instance <#{server_name}> destroyed.")
|
|
272
|
+
else
|
|
273
|
+
info("GCE instance <#{server_name}> does not exist - assuming it has been already destroyed.")
|
|
274
|
+
end
|
|
239
275
|
end
|
|
240
276
|
|
|
241
|
-
|
|
242
|
-
wait_for_operation(connection.delete_instance(project, zone, server_name))
|
|
243
|
-
info("GCE instance <#{server_name}> destroyed.")
|
|
277
|
+
delete_created_disks
|
|
244
278
|
|
|
245
279
|
state.delete(:server_name)
|
|
246
280
|
state.delete(:hostname)
|
|
247
281
|
state.delete(:zone)
|
|
248
282
|
end
|
|
249
283
|
|
|
284
|
+
# Reports whether the instance recorded in the state file is actually
|
|
285
|
+
# running, by asking GCE rather than trusting the state file.
|
|
286
|
+
#
|
|
287
|
+
# Test Kitchen 4 calls this for `kitchen list --live` and the `kitchen
|
|
288
|
+
# status` alias. Earlier versions never call it, so implementing it is
|
|
289
|
+
# safe across the whole supported range.
|
|
290
|
+
#
|
|
291
|
+
# The state file records only what the last action did, which goes stale
|
|
292
|
+
# in both directions: a preemptible instance GCE has reclaimed still
|
|
293
|
+
# reads as "Created", and an instance deleted outside Test Kitchen leaves
|
|
294
|
+
# a server name behind that no longer resolves.
|
|
295
|
+
#
|
|
296
|
+
# @param state [Hash] the Test Kitchen state hash
|
|
297
|
+
# @return [Hash] the status, in the shape `Kitchen::Instance` expects
|
|
298
|
+
def status(state)
|
|
299
|
+
@state = state
|
|
300
|
+
server_name = state[:server_name]
|
|
301
|
+
|
|
302
|
+
return status_report(live: false, state_name: "not created") if server_name.nil?
|
|
303
|
+
|
|
304
|
+
gce_status = server_instance(server_name).status.to_s.downcase
|
|
305
|
+
|
|
306
|
+
status_report(
|
|
307
|
+
live: gce_status == "running",
|
|
308
|
+
state_name: gce_status,
|
|
309
|
+
resource_id: server_name
|
|
310
|
+
)
|
|
311
|
+
rescue Google::Apis::ClientError => e
|
|
312
|
+
debug("API error: #{e.message}")
|
|
313
|
+
|
|
314
|
+
status_report(
|
|
315
|
+
live: false,
|
|
316
|
+
state_name: "not found",
|
|
317
|
+
resource_id: server_name,
|
|
318
|
+
message: "Instance #{server_name} is recorded in the state file but no longer exists " \
|
|
319
|
+
"in project #{project}, zone #{zone}."
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Builds a status hash in the shape `Kitchen::Instance` expects.
|
|
324
|
+
#
|
|
325
|
+
# @param live [Boolean] whether the instance is up and usable
|
|
326
|
+
# @param state_name [String] the state to display
|
|
327
|
+
# @param resource_id [String, nil] the GCE instance name
|
|
328
|
+
# @param message [String, nil] anything the user should know
|
|
329
|
+
# @return [Hash] the status
|
|
330
|
+
# @api private
|
|
331
|
+
def status_report(live:, state_name:, resource_id: nil, message: nil)
|
|
332
|
+
{
|
|
333
|
+
live: live,
|
|
334
|
+
state: state_name,
|
|
335
|
+
source: "driver",
|
|
336
|
+
resource_id: resource_id,
|
|
337
|
+
message: message,
|
|
338
|
+
checked_at: Time.now.utc.iso8601,
|
|
339
|
+
}.compact
|
|
340
|
+
end
|
|
341
|
+
|
|
250
342
|
# Whether the deprecated single-boot-disk options are configured.
|
|
251
343
|
#
|
|
252
344
|
# @return [Boolean] true if any of `autodelete_disk`, `disk_size` or
|
|
@@ -317,8 +409,9 @@ module Kitchen
|
|
|
317
409
|
#
|
|
318
410
|
# @param disks [Hash] the raw `disks` configuration, keyed by disk name
|
|
319
411
|
# @return [Hash{Symbol => Hash}] the normalised disk configuration
|
|
320
|
-
# @raise [RuntimeError] if a disk name or type is invalid,
|
|
321
|
-
# one boot disk is specified
|
|
412
|
+
# @raise [RuntimeError] if a disk name or type is invalid, if more than
|
|
413
|
+
# one boot disk is specified, or if {#assign_boot_disk} finds no disks
|
|
414
|
+
# at all and none eligible to boot
|
|
322
415
|
# @api private
|
|
323
416
|
def normalize_disks(disks)
|
|
324
417
|
normalized = disks.each_with_object({}) do |(disk_name, disk_config), memo|
|
|
@@ -333,12 +426,15 @@ module Kitchen
|
|
|
333
426
|
# Applies the disk defaults to one disk entry and validates the result.
|
|
334
427
|
#
|
|
335
428
|
# @param disk_name [String, Symbol] the disk's name, used in error messages
|
|
336
|
-
# @param disk_config [Hash] the user-supplied configuration for this
|
|
429
|
+
# @param disk_config [Hash, nil] the user-supplied configuration for this
|
|
430
|
+
# disk. A bare `disk-name:` in `kitchen.yml` parses to nil rather than
|
|
431
|
+
# to an empty hash, and means the same thing: take the defaults.
|
|
337
432
|
# @return [Hash] the disk configuration with defaults applied
|
|
338
433
|
# @raise [RuntimeError] if the disk type is invalid, a local SSD is
|
|
339
434
|
# marked bootable, or a size is given for a local SSD
|
|
340
435
|
# @api private
|
|
341
436
|
def normalize_disk(disk_name, disk_config)
|
|
437
|
+
disk_config ||= {}
|
|
342
438
|
normalized = DISK_DEFAULT_CONFIG.merge(disk_config)
|
|
343
439
|
|
|
344
440
|
unless valid_disk_type?(normalized[:disk_type])
|
|
@@ -405,7 +501,8 @@ module Kitchen
|
|
|
405
501
|
#
|
|
406
502
|
# @return [void]
|
|
407
503
|
# @raise [RuntimeError] if any configured project, zone, region, machine
|
|
408
|
-
# type, network, subnet, image or disk setting is invalid
|
|
504
|
+
# type, network, subnet, image or disk setting is invalid, or if the
|
|
505
|
+
# WinRM transport is in use without `email` set
|
|
409
506
|
def validate!
|
|
410
507
|
raise "Project #{config[:project]} is not a valid project" unless valid_project?
|
|
411
508
|
raise "Either zone or region must be specified" unless config[:zone] || config[:region]
|
|
@@ -449,7 +546,8 @@ module Kitchen
|
|
|
449
546
|
|
|
450
547
|
# Application default credentials scoped for Compute Engine.
|
|
451
548
|
#
|
|
452
|
-
# @return [
|
|
549
|
+
# @return [Signet::OAuth2::Client] the resolved application default
|
|
550
|
+
# credentials
|
|
453
551
|
def authorization
|
|
454
552
|
@authorization ||= Google::Auth.get_application_default(
|
|
455
553
|
[
|
|
@@ -502,6 +600,7 @@ module Kitchen
|
|
|
502
600
|
# Runs an API call and reports whether it succeeded, swallowing client
|
|
503
601
|
# errors so callers can use it as a validity predicate.
|
|
504
602
|
#
|
|
603
|
+
# @param block [Proc] the API call to attempt, called with no arguments
|
|
505
604
|
# @yield the API call to attempt
|
|
506
605
|
# @return [Boolean] true if the call succeeded, false on a client error
|
|
507
606
|
def check_api_call(&block)
|
|
@@ -750,16 +849,97 @@ module Kitchen
|
|
|
750
849
|
# Builds a unique, GCE-legal instance name, falling back to a UUID when
|
|
751
850
|
# the Test Kitchen instance name would make it too long.
|
|
752
851
|
#
|
|
852
|
+
# Uppercase is folded rather than discarded. GCE disallows it, but it
|
|
853
|
+
# still carries meaning, and replacing it with hyphens turned an
|
|
854
|
+
# `inst_name` of `MyTestVM` into `-y-est--` -- a name the user could not
|
|
855
|
+
# recognise as anything they had typed.
|
|
856
|
+
#
|
|
753
857
|
# @return [String] the instance name
|
|
858
|
+
# @raise [RuntimeError] if no legal name can be salvaged
|
|
754
859
|
def generate_server_name
|
|
755
|
-
name = config[:inst_name] || "tk-#{instance.name
|
|
860
|
+
name = (config[:inst_name] || "tk-#{instance.name}-#{SecureRandom.hex(3)}").downcase
|
|
756
861
|
|
|
757
|
-
if name.length >
|
|
862
|
+
if name.length > max_server_name_length
|
|
758
863
|
warn("The TK instance name (#{instance.name}) has been removed from the GCE instance name due to size limitations. Consider setting shorter platform or suite names.")
|
|
759
|
-
name =
|
|
864
|
+
name = fallback_server_name
|
|
760
865
|
end
|
|
761
866
|
|
|
762
|
-
name.gsub(/([^-a-z0-9])/, "-")
|
|
867
|
+
validated_server_name(name.gsub(/([^-a-z0-9])/, "-"))
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Checks a generated instance name against the pattern GCE enforces.
|
|
871
|
+
#
|
|
872
|
+
# Substitution cannot fix every name -- a leading underscore becomes a
|
|
873
|
+
# leading hyphen, which GCE still rejects -- so say so here rather than
|
|
874
|
+
# letting the API refuse a name the user never wrote.
|
|
875
|
+
#
|
|
876
|
+
# @param name [String] the generated name
|
|
877
|
+
# @return [String] the same name, once it is known to be legal
|
|
878
|
+
# @raise [RuntimeError] if the name does not match {RESOURCE_NAME_REGEX}
|
|
879
|
+
# @api private
|
|
880
|
+
def validated_server_name(name)
|
|
881
|
+
return name if name.match?(/\A#{RESOURCE_NAME_REGEX}\z/)
|
|
882
|
+
|
|
883
|
+
raise "Instance name #{name} is not valid. GCE names must match " \
|
|
884
|
+
"#{RESOURCE_NAME_REGEX.source} - they start with a lowercase letter, and end " \
|
|
885
|
+
"with a lowercase letter or a digit."
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Longest instance name that still leaves room for every disk named
|
|
889
|
+
# after it.
|
|
890
|
+
#
|
|
891
|
+
# GCE allows {MAX_INSTANCE_NAME_LENGTH} characters for an instance name
|
|
892
|
+
# and the same for a disk name, and {#create_disks} names each disk
|
|
893
|
+
# `"<instance>-<disk>"`. Spending the whole budget on the instance is
|
|
894
|
+
# what let a perfectly legal instance name produce an illegal disk name.
|
|
895
|
+
#
|
|
896
|
+
# @return [Integer] the maximum length of a generated instance name
|
|
897
|
+
# @raise [RuntimeError] if a configured disk name is long enough that
|
|
898
|
+
# fewer than {#fallback_server_name_length} characters remain
|
|
899
|
+
# @api private
|
|
900
|
+
def max_server_name_length
|
|
901
|
+
limit = MAX_INSTANCE_NAME_LENGTH - longest_disk_name_suffix
|
|
902
|
+
|
|
903
|
+
if limit < fallback_server_name_length
|
|
904
|
+
raise "Disk name #{longest_disk_name} is too long. GCE allows " \
|
|
905
|
+
"#{MAX_INSTANCE_NAME_LENGTH} characters for the disk name " \
|
|
906
|
+
"<instance name>-#{longest_disk_name}, which leaves no room for the instance name."
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
limit
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# The longest disk name configured, which bounds the instance name.
|
|
913
|
+
#
|
|
914
|
+
# @return [String, nil] the longest disk name
|
|
915
|
+
# @api private
|
|
916
|
+
def longest_disk_name
|
|
917
|
+
Array(config[:disks]&.keys).map(&:to_s).max_by(&:length)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Characters the longest disk name costs an instance name, including the
|
|
921
|
+
# hyphen joining the two.
|
|
922
|
+
#
|
|
923
|
+
# @return [Integer] the reserved length
|
|
924
|
+
# @api private
|
|
925
|
+
def longest_disk_name_suffix
|
|
926
|
+
longest_disk_name.nil? ? 0 : longest_disk_name.length + 1
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# Name used when the Test Kitchen instance name will not fit.
|
|
930
|
+
#
|
|
931
|
+
# @return [String] a UUID-based instance name
|
|
932
|
+
# @api private
|
|
933
|
+
def fallback_server_name
|
|
934
|
+
"tk-#{SecureRandom.uuid}"
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# Length of a {#fallback_server_name}, which is fixed.
|
|
938
|
+
#
|
|
939
|
+
# @return [Integer] the fallback name's length
|
|
940
|
+
# @api private
|
|
941
|
+
def fallback_server_name_length
|
|
942
|
+
@fallback_server_name_length ||= fallback_server_name.length
|
|
763
943
|
end
|
|
764
944
|
|
|
765
945
|
# Builds every disk for the instance, creating standalone persistent
|
|
@@ -809,10 +989,11 @@ module Kitchen
|
|
|
809
989
|
params.source_image = boot_disk_source_image
|
|
810
990
|
params.disk_name = unique_disk_name
|
|
811
991
|
else
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
params.
|
|
992
|
+
custom_image = disk_config[:custom_image]
|
|
993
|
+
params.source_image = custom_image_url(custom_image)
|
|
994
|
+
params.disk_size_gb = disk_size_for_image(disk_config[:disk_size], custom_image)
|
|
815
995
|
params.disk_name = unique_disk_name
|
|
996
|
+
info("Creating a #{params.disk_size_gb} GB extra disk named #{unique_disk_name} from image #{custom_image}...")
|
|
816
997
|
end
|
|
817
998
|
disk.initialize_params = params
|
|
818
999
|
disk
|
|
@@ -886,21 +1067,28 @@ module Kitchen
|
|
|
886
1067
|
attached_disk
|
|
887
1068
|
end
|
|
888
1069
|
|
|
889
|
-
# Names of the standalone disks this driver created
|
|
890
|
-
#
|
|
1070
|
+
# Names of the standalone disks this driver has created, held in the Test
|
|
1071
|
+
# Kitchen state file rather than in memory.
|
|
1072
|
+
#
|
|
1073
|
+
# These disks bill from the moment GCE creates them, which is before the
|
|
1074
|
+
# instance exists, and the driver's own cleanup cannot be relied on to
|
|
1075
|
+
# remove them: `Interrupt` is not a `StandardError`, so pressing Ctrl-C
|
|
1076
|
+
# during a create skips the rescue entirely. Test Kitchen writes the
|
|
1077
|
+
# state file whatever happens, so recording the names there is what makes
|
|
1078
|
+
# a later `kitchen destroy` able to find them.
|
|
891
1079
|
#
|
|
892
1080
|
# @return [Array<String>] the created disk names
|
|
893
1081
|
def created_disk_names
|
|
894
|
-
|
|
1082
|
+
state[:created_disks] ||= []
|
|
895
1083
|
end
|
|
896
1084
|
|
|
897
|
-
# Deletes every standalone disk
|
|
898
|
-
#
|
|
1085
|
+
# Deletes every standalone disk this driver created, so a partial run
|
|
1086
|
+
# does not leave billable disks behind.
|
|
899
1087
|
#
|
|
900
1088
|
# @return [void]
|
|
901
1089
|
def delete_created_disks
|
|
902
1090
|
created_disk_names.each { |disk_name| delete_disk(disk_name) }
|
|
903
|
-
|
|
1091
|
+
state.delete(:created_disks)
|
|
904
1092
|
end
|
|
905
1093
|
|
|
906
1094
|
# Deletes a standalone persistent disk, tolerating one that is already
|
|
@@ -952,13 +1140,44 @@ module Kitchen
|
|
|
952
1140
|
"projects/#{image_project}/global/images/#{image}" if image_exist?(image)
|
|
953
1141
|
end
|
|
954
1142
|
|
|
1143
|
+
# URL of a non-boot disk's `custom_image`, which must exist.
|
|
1144
|
+
#
|
|
1145
|
+
# GCE reads a missing `sourceImage` as a request for a blank disk, so an
|
|
1146
|
+
# image the driver cannot resolve would otherwise produce an empty disk
|
|
1147
|
+
# and a successful run -- a test that silently verifies nothing. The boot
|
|
1148
|
+
# disk has been guarded against exactly this in {#validate!} for years.
|
|
1149
|
+
#
|
|
1150
|
+
# The message names the project searched because that is where the
|
|
1151
|
+
# surprise usually is: `custom_image` is resolved in `image_project`, so
|
|
1152
|
+
# booting from a public image such as ubuntu-os-cloud sends the lookup
|
|
1153
|
+
# there rather than to the user's own project.
|
|
1154
|
+
#
|
|
1155
|
+
# @param image [String] the custom image name
|
|
1156
|
+
# @return [String] the image URL
|
|
1157
|
+
# @raise [RuntimeError] if the image does not exist in the image project
|
|
1158
|
+
# @api private
|
|
1159
|
+
def custom_image_url(image)
|
|
1160
|
+
image_url(image) ||
|
|
1161
|
+
raise("Disk image #{image} was not found in project #{image_project} - check the " \
|
|
1162
|
+
"custom_image name, and note that custom images are looked up in image_project.")
|
|
1163
|
+
end
|
|
1164
|
+
|
|
955
1165
|
# Resolves the current image name for an image family.
|
|
956
1166
|
#
|
|
1167
|
+
# A family the API cannot see is a configuration mistake like any other,
|
|
1168
|
+
# so report it the way {#validate!} reports an unresolvable `image_name`
|
|
1169
|
+
# rather than letting the Google client's own error escape.
|
|
1170
|
+
#
|
|
957
1171
|
# @param image_family [String] the image family
|
|
958
1172
|
# @return [String] the image name
|
|
1173
|
+
# @raise [RuntimeError] if the family does not exist in the image project
|
|
959
1174
|
def image_name_for_family(image_family)
|
|
960
1175
|
image = connection.get_image_from_family(image_project, image_family)
|
|
961
1176
|
image.name
|
|
1177
|
+
rescue Google::Apis::ClientError => e
|
|
1178
|
+
debug("API error: #{e.message}")
|
|
1179
|
+
raise "Image family #{image_family} is not valid - it was not found in project " \
|
|
1180
|
+
"#{image_project}. Check the family name and image_project."
|
|
962
1181
|
end
|
|
963
1182
|
|
|
964
1183
|
# Partial URL identifying the machine type in the target zone.
|
|
@@ -1000,10 +1219,11 @@ module Kitchen
|
|
|
1000
1219
|
guest_accelerator_configs
|
|
1001
1220
|
end
|
|
1002
1221
|
|
|
1003
|
-
# The instance metadata
|
|
1004
|
-
# configured
|
|
1222
|
+
# The instance metadata: the driver's own keys, overlaid with any the
|
|
1223
|
+
# user configured, plus a WinRM bootstrap script for Windows guests.
|
|
1005
1224
|
#
|
|
1006
|
-
# @return [Hash{String =>
|
|
1225
|
+
# @return [Hash{String => Object}] the metadata, string-keyed; user
|
|
1226
|
+
# values keep whatever type `kitchen.yml` gave them
|
|
1007
1227
|
def metadata
|
|
1008
1228
|
default_metadata = {
|
|
1009
1229
|
"created-by" => "test-kitchen",
|
|
@@ -1018,7 +1238,28 @@ module Kitchen
|
|
|
1018
1238
|
end
|
|
1019
1239
|
end
|
|
1020
1240
|
|
|
1021
|
-
|
|
1241
|
+
default_metadata.merge(user_metadata(default_metadata.keys))
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
# The user's own metadata, keyed the way the GCE API expects.
|
|
1245
|
+
#
|
|
1246
|
+
# Test Kitchen symbolises every key it loads from `kitchen.yml`, so a
|
|
1247
|
+
# user writing `created-by:` reaches the driver as `:"created-by"` and
|
|
1248
|
+
# never as the string the driver sets itself. Merging the two hashes
|
|
1249
|
+
# untouched therefore sends both spellings of the same key, and GCE
|
|
1250
|
+
# rejects the instance outright with "Metadata has duplicate keys".
|
|
1251
|
+
#
|
|
1252
|
+
# @param driver_keys [Array<String>] keys the driver sets itself, warned
|
|
1253
|
+
# about when the user replaces one
|
|
1254
|
+
# @return [Hash{String => Object}] the user's metadata, string-keyed
|
|
1255
|
+
# @api private
|
|
1256
|
+
def user_metadata(driver_keys)
|
|
1257
|
+
metadata = config[:metadata].transform_keys(&:to_s)
|
|
1258
|
+
|
|
1259
|
+
overridden = driver_keys & metadata.keys
|
|
1260
|
+
warn("Overriding metadata set by the driver: #{overridden.join(", ")}") unless overridden.empty?
|
|
1261
|
+
|
|
1262
|
+
metadata
|
|
1022
1263
|
end
|
|
1023
1264
|
|
|
1024
1265
|
# The metadata in the form the GCE API expects.
|
|
@@ -1204,6 +1445,7 @@ module Kitchen
|
|
|
1204
1445
|
# logging each status change.
|
|
1205
1446
|
#
|
|
1206
1447
|
# @param requested_status [String] the status to wait for
|
|
1448
|
+
# @param block [Proc] called once per poll to re-fetch the resource
|
|
1207
1449
|
# @yieldreturn [#status] the resource to poll
|
|
1208
1450
|
# @return [void]
|
|
1209
1451
|
# @raise [Timeout::Error] if the status is not reached within {#wait_time}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-google
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Test Kitchen Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-apis-compute_v1
|
|
@@ -100,6 +100,20 @@ dependencies:
|
|
|
100
100
|
- - ">="
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
102
|
version: '0.1'
|
|
103
|
+
- !ruby/object:Gem::Dependency
|
|
104
|
+
name: time
|
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.1'
|
|
110
|
+
type: :runtime
|
|
111
|
+
prerelease: false
|
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.1'
|
|
103
117
|
- !ruby/object:Gem::Dependency
|
|
104
118
|
name: timeout
|
|
105
119
|
requirement: !ruby/object:Gem::Requirement
|