kitchen-google 3.0.1 → 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 +373 -44
- 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
|
#
|
|
@@ -89,7 +96,10 @@ module Kitchen
|
|
|
89
96
|
default_config :tags, []
|
|
90
97
|
default_config :preemptible, false
|
|
91
98
|
default_config :auto_restart, false
|
|
92
|
-
|
|
99
|
+
# Matches GCE's own default for onHostMaintenance. Several machine
|
|
100
|
+
# families, E2 among them, reject TERMINATE unless the instance is
|
|
101
|
+
# preemptible, so defaulting this off makes them unusable.
|
|
102
|
+
default_config :auto_migrate, true
|
|
93
103
|
default_config :image_family, nil
|
|
94
104
|
default_config :image_name, nil
|
|
95
105
|
default_config :image_project, nil
|
|
@@ -102,10 +112,18 @@ module Kitchen
|
|
|
102
112
|
default_config :metadata, {}
|
|
103
113
|
default_config :labels, {}
|
|
104
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
|
+
|
|
105
121
|
# Pattern a GCE disk name must match in full.
|
|
106
122
|
#
|
|
123
|
+
# @deprecated Use {RESOURCE_NAME_REGEX}, which is the same pattern and
|
|
124
|
+
# says what it actually covers.
|
|
107
125
|
# @return [Regexp] the permitted disk-name pattern
|
|
108
|
-
DISK_NAME_REGEX =
|
|
126
|
+
DISK_NAME_REGEX = RESOURCE_NAME_REGEX
|
|
109
127
|
|
|
110
128
|
# Longest instance name GCE accepts.
|
|
111
129
|
#
|
|
@@ -138,6 +156,22 @@ module Kitchen
|
|
|
138
156
|
disk_size: 10,
|
|
139
157
|
}.freeze
|
|
140
158
|
|
|
159
|
+
# Local Windows account Test Kitchen's WinRM transport defaults to, and
|
|
160
|
+
# which Google's Windows images ship disabled.
|
|
161
|
+
#
|
|
162
|
+
# @return [String] the built-in administrator account name
|
|
163
|
+
BUILTIN_ADMINISTRATOR = "administrator".freeze
|
|
164
|
+
|
|
165
|
+
# Told to the user when they are about to wait out a WinRM timeout for a
|
|
166
|
+
# reason the driver can see coming.
|
|
167
|
+
#
|
|
168
|
+
# @return [String] the warning text
|
|
169
|
+
BUILTIN_ADMINISTRATOR_WARNING =
|
|
170
|
+
"The WinRM transport is connecting as the built-in Administrator account, which is " \
|
|
171
|
+
"disabled on Google's Windows images. The guest agent resets its password without " \
|
|
172
|
+
"enabling it, so the login will be refused. Set transport.username to any other name " \
|
|
173
|
+
"and the agent will create that account instead.".freeze
|
|
174
|
+
|
|
141
175
|
# Human-readable driver name shown in Test Kitchen output.
|
|
142
176
|
#
|
|
143
177
|
# @return [String] the driver's display name
|
|
@@ -154,7 +188,8 @@ module Kitchen
|
|
|
154
188
|
# before the error is re-raised.
|
|
155
189
|
#
|
|
156
190
|
# @param state [Hash] the Test Kitchen state hash, mutated in place with
|
|
157
|
-
# `:server_name`, `:hostname` and `:
|
|
191
|
+
# `:server_name`, `:hostname`, `:zone` and `:created_disks`, plus
|
|
192
|
+
# `:password` for Windows guests
|
|
158
193
|
# @return [void]
|
|
159
194
|
# @raise [StandardError] if instance creation fails for any reason
|
|
160
195
|
def create(state)
|
|
@@ -163,19 +198,31 @@ module Kitchen
|
|
|
163
198
|
|
|
164
199
|
validate!
|
|
165
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
|
+
|
|
166
205
|
server_name = generate_server_name
|
|
167
206
|
|
|
168
|
-
|
|
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
|
|
169
219
|
|
|
170
220
|
info("Creating GCE instance <#{server_name}> in project #{project}, zone #{zone}...")
|
|
171
221
|
operation = connection.insert_instance(project, zone, create_instance_object(server_name))
|
|
172
222
|
|
|
173
223
|
wait_for_operation(operation)
|
|
174
224
|
|
|
175
|
-
|
|
176
|
-
state[:server_name] = server_name
|
|
177
|
-
state[:hostname] = ip_address_for(server)
|
|
178
|
-
state[:zone] = zone
|
|
225
|
+
state[:hostname] = ip_address_for(server_instance(server_name))
|
|
179
226
|
|
|
180
227
|
info("Server <#{server_name}> created.")
|
|
181
228
|
|
|
@@ -197,33 +244,101 @@ module Kitchen
|
|
|
197
244
|
raise
|
|
198
245
|
end
|
|
199
246
|
|
|
200
|
-
# 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.
|
|
201
249
|
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
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.
|
|
255
|
+
#
|
|
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.
|
|
204
259
|
#
|
|
205
260
|
# @param state [Hash] the Test Kitchen state hash, mutated in place to
|
|
206
|
-
# remove `:server_name`, `:hostname` and `:
|
|
261
|
+
# remove `:server_name`, `:hostname`, `:zone` and `:created_disks`
|
|
207
262
|
# @return [void]
|
|
208
263
|
def destroy(state)
|
|
209
264
|
@state = state
|
|
210
265
|
server_name = state[:server_name]
|
|
211
|
-
return if server_name.nil?
|
|
212
266
|
|
|
213
|
-
unless
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
216
275
|
end
|
|
217
276
|
|
|
218
|
-
|
|
219
|
-
wait_for_operation(connection.delete_instance(project, zone, server_name))
|
|
220
|
-
info("GCE instance <#{server_name}> destroyed.")
|
|
277
|
+
delete_created_disks
|
|
221
278
|
|
|
222
279
|
state.delete(:server_name)
|
|
223
280
|
state.delete(:hostname)
|
|
224
281
|
state.delete(:zone)
|
|
225
282
|
end
|
|
226
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
|
+
|
|
227
342
|
# Whether the deprecated single-boot-disk options are configured.
|
|
228
343
|
#
|
|
229
344
|
# @return [Boolean] true if any of `autodelete_disk`, `disk_size` or
|
|
@@ -294,8 +409,9 @@ module Kitchen
|
|
|
294
409
|
#
|
|
295
410
|
# @param disks [Hash] the raw `disks` configuration, keyed by disk name
|
|
296
411
|
# @return [Hash{Symbol => Hash}] the normalised disk configuration
|
|
297
|
-
# @raise [RuntimeError] if a disk name or type is invalid,
|
|
298
|
-
# 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
|
|
299
415
|
# @api private
|
|
300
416
|
def normalize_disks(disks)
|
|
301
417
|
normalized = disks.each_with_object({}) do |(disk_name, disk_config), memo|
|
|
@@ -310,12 +426,15 @@ module Kitchen
|
|
|
310
426
|
# Applies the disk defaults to one disk entry and validates the result.
|
|
311
427
|
#
|
|
312
428
|
# @param disk_name [String, Symbol] the disk's name, used in error messages
|
|
313
|
-
# @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.
|
|
314
432
|
# @return [Hash] the disk configuration with defaults applied
|
|
315
433
|
# @raise [RuntimeError] if the disk type is invalid, a local SSD is
|
|
316
434
|
# marked bootable, or a size is given for a local SSD
|
|
317
435
|
# @api private
|
|
318
436
|
def normalize_disk(disk_name, disk_config)
|
|
437
|
+
disk_config ||= {}
|
|
319
438
|
normalized = DISK_DEFAULT_CONFIG.merge(disk_config)
|
|
320
439
|
|
|
321
440
|
unless valid_disk_type?(normalized[:disk_type])
|
|
@@ -382,7 +501,8 @@ module Kitchen
|
|
|
382
501
|
#
|
|
383
502
|
# @return [void]
|
|
384
503
|
# @raise [RuntimeError] if any configured project, zone, region, machine
|
|
385
|
-
# 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
|
|
386
506
|
def validate!
|
|
387
507
|
raise "Project #{config[:project]} is not a valid project" unless valid_project?
|
|
388
508
|
raise "Either zone or region must be specified" unless config[:zone] || config[:region]
|
|
@@ -397,11 +517,13 @@ module Kitchen
|
|
|
397
517
|
raise "You cannot use autodelete_disk, disk_size or disk_type with the new disks configuration" if old_disk_configuration_present? && new_disk_configuration_present?
|
|
398
518
|
raise "Disk image #{config[:image_name]} is not valid - check your image name and image project" if boot_disk_source_image.nil?
|
|
399
519
|
|
|
520
|
+
warn(BUILTIN_ADMINISTRATOR_WARNING) if winrm_transport? && builtin_administrator?
|
|
400
521
|
warn("Both zone and region specified - region will be ignored.") if config[:zone] && config[:region]
|
|
401
522
|
warn("Both image family and name specified - image family will be ignored") if config[:image_family] && config[:image_name]
|
|
402
523
|
warn("Image project not specified - searching current project only") unless config[:image_project]
|
|
403
524
|
warn("Subnet project not specified - searching current project only") if config[:subnet] && !config[:subnet_project]
|
|
404
525
|
warn("Auto-migrate disabled for preemptible instance") if preemptible? && config[:auto_migrate]
|
|
526
|
+
warn("Auto-migrate disabled for instance with guest accelerators") if guest_accelerators? && config[:auto_migrate]
|
|
405
527
|
warn("Auto-restart disabled for preemptible instance") if preemptible? && config[:auto_restart]
|
|
406
528
|
warn("These configs are deprecated - consider using new disks configuration") if old_disk_configuration_present?
|
|
407
529
|
end
|
|
@@ -424,7 +546,8 @@ module Kitchen
|
|
|
424
546
|
|
|
425
547
|
# Application default credentials scoped for Compute Engine.
|
|
426
548
|
#
|
|
427
|
-
# @return [
|
|
549
|
+
# @return [Signet::OAuth2::Client] the resolved application default
|
|
550
|
+
# credentials
|
|
428
551
|
def authorization
|
|
429
552
|
@authorization ||= Google::Auth.get_application_default(
|
|
430
553
|
[
|
|
@@ -434,6 +557,14 @@ module Kitchen
|
|
|
434
557
|
)
|
|
435
558
|
end
|
|
436
559
|
|
|
560
|
+
# Whether the WinRM transport is configured to log in as the built-in
|
|
561
|
+
# Administrator account.
|
|
562
|
+
#
|
|
563
|
+
# @return [Boolean] true if the transport username is `administrator`
|
|
564
|
+
def builtin_administrator?
|
|
565
|
+
instance.transport[:username].to_s.casecmp?(BUILTIN_ADMINISTRATOR)
|
|
566
|
+
end
|
|
567
|
+
|
|
437
568
|
# Whether the suite's transport is WinRM, implying a Windows guest.
|
|
438
569
|
#
|
|
439
570
|
# @return [Boolean] true when the transport is WinRM
|
|
@@ -469,6 +600,7 @@ module Kitchen
|
|
|
469
600
|
# Runs an API call and reports whether it succeeded, swallowing client
|
|
470
601
|
# errors so callers can use it as a validity predicate.
|
|
471
602
|
#
|
|
603
|
+
# @param block [Proc] the API call to attempt, called with no arguments
|
|
472
604
|
# @yield the API call to attempt
|
|
473
605
|
# @return [Boolean] true if the call succeeded, false on a client error
|
|
474
606
|
def check_api_call(&block)
|
|
@@ -717,16 +849,97 @@ module Kitchen
|
|
|
717
849
|
# Builds a unique, GCE-legal instance name, falling back to a UUID when
|
|
718
850
|
# the Test Kitchen instance name would make it too long.
|
|
719
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
|
+
#
|
|
720
857
|
# @return [String] the instance name
|
|
858
|
+
# @raise [RuntimeError] if no legal name can be salvaged
|
|
721
859
|
def generate_server_name
|
|
722
|
-
name = config[:inst_name] || "tk-#{instance.name
|
|
860
|
+
name = (config[:inst_name] || "tk-#{instance.name}-#{SecureRandom.hex(3)}").downcase
|
|
723
861
|
|
|
724
|
-
if name.length >
|
|
862
|
+
if name.length > max_server_name_length
|
|
725
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.")
|
|
726
|
-
name =
|
|
864
|
+
name = fallback_server_name
|
|
727
865
|
end
|
|
728
866
|
|
|
729
|
-
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
|
|
730
943
|
end
|
|
731
944
|
|
|
732
945
|
# Builds every disk for the instance, creating standalone persistent
|
|
@@ -771,18 +984,65 @@ module Kitchen
|
|
|
771
984
|
info("Creating a #{LOCAL_SSD_SIZE_GB} GB local ssd as scratch disk (https://cloud.google.com/compute/docs/disks/#localssds).")
|
|
772
985
|
disk.type = "SCRATCH"
|
|
773
986
|
elsif disk.boot
|
|
774
|
-
|
|
987
|
+
params.disk_size_gb = disk_size_for_image(disk_config[:disk_size], image_name)
|
|
988
|
+
info("Creating a #{params.disk_size_gb} GB boot disk named #{unique_disk_name} from image #{image_name}...")
|
|
775
989
|
params.source_image = boot_disk_source_image
|
|
776
990
|
params.disk_name = unique_disk_name
|
|
777
991
|
else
|
|
778
|
-
|
|
779
|
-
params.source_image =
|
|
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)
|
|
780
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}...")
|
|
781
997
|
end
|
|
782
998
|
disk.initialize_params = params
|
|
783
999
|
disk
|
|
784
1000
|
end
|
|
785
1001
|
|
|
1002
|
+
# The size to request for a disk cloned from an image.
|
|
1003
|
+
#
|
|
1004
|
+
# GCE refuses to create a disk smaller than the image it is cloned from,
|
|
1005
|
+
# and the driver's own 10 GB default is smaller than many stock images -
|
|
1006
|
+
# every Windows image is 50 GB, and Rocky and CentOS are 20 GB. Rather
|
|
1007
|
+
# than fail the run over a size the user never chose, raise the request
|
|
1008
|
+
# to what the image needs and say so.
|
|
1009
|
+
#
|
|
1010
|
+
# @param requested [Integer, nil] the configured size in gigabytes
|
|
1011
|
+
# @param image [String, nil] the image the disk is cloned from
|
|
1012
|
+
# @return [Integer, nil] the size to request
|
|
1013
|
+
# @api private
|
|
1014
|
+
def disk_size_for_image(requested, image)
|
|
1015
|
+
image_size = image_disk_size_gb(image)
|
|
1016
|
+
return requested if image_size.nil? || (!requested.nil? && requested >= image_size)
|
|
1017
|
+
|
|
1018
|
+
warn("Requested disk size of #{requested} GB is smaller than image #{image} " \
|
|
1019
|
+
"(#{image_size} GB) - creating a #{image_size} GB disk instead.")
|
|
1020
|
+
image_size
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# The size, in gigabytes, of an image in the image project.
|
|
1024
|
+
#
|
|
1025
|
+
# Memoised per image name, since the boot image is looked up more than
|
|
1026
|
+
# once during a single action.
|
|
1027
|
+
#
|
|
1028
|
+
# @param image [String, nil] the image name
|
|
1029
|
+
# @return [Integer, nil] the image's size, or nil if it cannot be read
|
|
1030
|
+
# @api private
|
|
1031
|
+
def image_disk_size_gb(image)
|
|
1032
|
+
return if image.nil?
|
|
1033
|
+
|
|
1034
|
+
@image_disk_sizes ||= {}
|
|
1035
|
+
return @image_disk_sizes[image] if @image_disk_sizes.key?(image)
|
|
1036
|
+
|
|
1037
|
+
@image_disk_sizes[image] =
|
|
1038
|
+
begin
|
|
1039
|
+
connection.get_image(image_project, image).disk_size_gb
|
|
1040
|
+
rescue Google::Apis::ClientError => e
|
|
1041
|
+
debug("Unable to read the size of image #{image}: #{e.message}")
|
|
1042
|
+
nil
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
|
|
786
1046
|
# Creates a standalone persistent disk, waits for it to become ready, and
|
|
787
1047
|
# returns a reference attaching it to the instance.
|
|
788
1048
|
#
|
|
@@ -807,21 +1067,28 @@ module Kitchen
|
|
|
807
1067
|
attached_disk
|
|
808
1068
|
end
|
|
809
1069
|
|
|
810
|
-
# Names of the standalone disks this driver created
|
|
811
|
-
#
|
|
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.
|
|
812
1079
|
#
|
|
813
1080
|
# @return [Array<String>] the created disk names
|
|
814
1081
|
def created_disk_names
|
|
815
|
-
|
|
1082
|
+
state[:created_disks] ||= []
|
|
816
1083
|
end
|
|
817
1084
|
|
|
818
|
-
# Deletes every standalone disk
|
|
819
|
-
#
|
|
1085
|
+
# Deletes every standalone disk this driver created, so a partial run
|
|
1086
|
+
# does not leave billable disks behind.
|
|
820
1087
|
#
|
|
821
1088
|
# @return [void]
|
|
822
1089
|
def delete_created_disks
|
|
823
1090
|
created_disk_names.each { |disk_name| delete_disk(disk_name) }
|
|
824
|
-
|
|
1091
|
+
state.delete(:created_disks)
|
|
825
1092
|
end
|
|
826
1093
|
|
|
827
1094
|
# Deletes a standalone persistent disk, tolerating one that is already
|
|
@@ -873,13 +1140,44 @@ module Kitchen
|
|
|
873
1140
|
"projects/#{image_project}/global/images/#{image}" if image_exist?(image)
|
|
874
1141
|
end
|
|
875
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
|
+
|
|
876
1165
|
# Resolves the current image name for an image family.
|
|
877
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
|
+
#
|
|
878
1171
|
# @param image_family [String] the image family
|
|
879
1172
|
# @return [String] the image name
|
|
1173
|
+
# @raise [RuntimeError] if the family does not exist in the image project
|
|
880
1174
|
def image_name_for_family(image_family)
|
|
881
1175
|
image = connection.get_image_from_family(image_project, image_family)
|
|
882
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."
|
|
883
1181
|
end
|
|
884
1182
|
|
|
885
1183
|
# Partial URL identifying the machine type in the target zone.
|
|
@@ -921,10 +1219,11 @@ module Kitchen
|
|
|
921
1219
|
guest_accelerator_configs
|
|
922
1220
|
end
|
|
923
1221
|
|
|
924
|
-
# The instance metadata
|
|
925
|
-
# 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.
|
|
926
1224
|
#
|
|
927
|
-
# @return [Hash{String =>
|
|
1225
|
+
# @return [Hash{String => Object}] the metadata, string-keyed; user
|
|
1226
|
+
# values keep whatever type `kitchen.yml` gave them
|
|
928
1227
|
def metadata
|
|
929
1228
|
default_metadata = {
|
|
930
1229
|
"created-by" => "test-kitchen",
|
|
@@ -939,7 +1238,28 @@ module Kitchen
|
|
|
939
1238
|
end
|
|
940
1239
|
end
|
|
941
1240
|
|
|
942
|
-
|
|
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
|
|
943
1263
|
end
|
|
944
1264
|
|
|
945
1265
|
# The metadata in the form the GCE API expects.
|
|
@@ -1031,16 +1351,24 @@ module Kitchen
|
|
|
1031
1351
|
config[:preemptible] ? true : false
|
|
1032
1352
|
end
|
|
1033
1353
|
|
|
1034
|
-
# Whether the instance may live-migrate. Always false
|
|
1035
|
-
#
|
|
1354
|
+
# Whether the instance may live-migrate. Always false for preemptible
|
|
1355
|
+
# instances and for instances with guest accelerators attached, neither
|
|
1356
|
+
# of which GCE will migrate.
|
|
1036
1357
|
#
|
|
1037
1358
|
# @return [Boolean] true if live migration is enabled
|
|
1038
1359
|
def auto_migrate?
|
|
1039
|
-
return false if preemptible?
|
|
1360
|
+
return false if preemptible? || guest_accelerators?
|
|
1040
1361
|
|
|
1041
1362
|
config[:auto_migrate] ? true : false
|
|
1042
1363
|
end
|
|
1043
1364
|
|
|
1365
|
+
# Whether any guest accelerators are attached.
|
|
1366
|
+
#
|
|
1367
|
+
# @return [Boolean] true if the instance has at least one accelerator
|
|
1368
|
+
def guest_accelerators?
|
|
1369
|
+
!Array(config[:guest_accelerators]).empty?
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1044
1372
|
# Whether the instance should restart automatically. Always false when
|
|
1045
1373
|
# preemptible, which GCE does not allow to auto-restart.
|
|
1046
1374
|
#
|
|
@@ -1117,6 +1445,7 @@ module Kitchen
|
|
|
1117
1445
|
# logging each status change.
|
|
1118
1446
|
#
|
|
1119
1447
|
# @param requested_status [String] the status to wait for
|
|
1448
|
+
# @param block [Proc] called once per poll to re-fetch the resource
|
|
1120
1449
|
# @yieldreturn [#status] the resource to poll
|
|
1121
1450
|
# @return [void]
|
|
1122
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
|