kitchen-google 2.7.0 → 2.8.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.
- checksums.yaml +4 -4
- data/lib/kitchen/driver/gce.rb +513 -75
- data/lib/kitchen/driver/gce_version.rb +9 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d96f1f6b4c605799e8b3af57a4e8966a786f808f1c3e5e2c201d34e306da454d
|
|
4
|
+
data.tar.gz: 2f8f62792d8c6b15a568dd5f8abdf177acafad3762df1e04ccacf4b69f502fe2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb7e05264bf801fc495576aca5ebd797193ea5e6140702e26d4248a0a3c54892702422fef3a563b2e78882f92dc2abedbae99b00371afcefdadeecc4bafa27bc
|
|
7
|
+
data.tar.gz: de05cdf3ae3b40aac2d40fb7fb3e3154dafb16d01979632a83ea7de5b4fdc249343c10d63407c0a36e09bd22ec945a829468a9d61f1caca58e52a2bfffa1fbfa
|
data/lib/kitchen/driver/gce.rb
CHANGED
|
@@ -21,15 +21,32 @@ require "google/apis/compute_v1"
|
|
|
21
21
|
require "kitchen"
|
|
22
22
|
require_relative "gce_version"
|
|
23
23
|
require "securerandom" unless defined?(SecureRandom)
|
|
24
|
+
require "timeout" unless defined?(Timeout)
|
|
24
25
|
|
|
25
26
|
module Kitchen
|
|
26
27
|
module Driver
|
|
27
|
-
# Google Compute Engine driver for Test Kitchen
|
|
28
|
+
# Google Compute Engine driver for Test Kitchen.
|
|
29
|
+
#
|
|
30
|
+
# Creates and destroys GCE instances for Test Kitchen suites, translating
|
|
31
|
+
# `kitchen.yml` driver configuration into Google Compute Engine API calls.
|
|
28
32
|
#
|
|
29
33
|
# @author Andrew Leonard <andy@hurricane-ridge.com>
|
|
34
|
+
#
|
|
35
|
+
# @example Minimal kitchen.yml configuration
|
|
36
|
+
# driver:
|
|
37
|
+
# name: gce
|
|
38
|
+
# project: my-gcp-project
|
|
39
|
+
# zone: us-central1-a
|
|
40
|
+
# image_family: ubuntu-2204-lts
|
|
41
|
+
# image_project: ubuntu-os-cloud
|
|
30
42
|
class Gce < Kitchen::Driver::Base
|
|
43
|
+
# @return [Hash] the Test Kitchen state hash for the action in progress
|
|
31
44
|
attr_accessor :state
|
|
32
45
|
|
|
46
|
+
# Maps the short scope aliases accepted by `gcloud` onto the scope
|
|
47
|
+
# segment of their fully-qualified OAuth 2.0 URL.
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash{String => String}] alias to scope-path mapping
|
|
33
50
|
SCOPE_ALIAS_MAP = {
|
|
34
51
|
"bigquery" => "bigquery",
|
|
35
52
|
"cloud-platform" => "cloud-platform",
|
|
@@ -85,12 +102,55 @@ module Kitchen
|
|
|
85
102
|
default_config :metadata, {}
|
|
86
103
|
default_config :labels, {}
|
|
87
104
|
|
|
105
|
+
# Pattern a GCE disk name must match in full.
|
|
106
|
+
#
|
|
107
|
+
# @return [Regexp] the permitted disk-name pattern
|
|
88
108
|
DISK_NAME_REGEX = /(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)/
|
|
89
109
|
|
|
110
|
+
# Longest instance name GCE accepts.
|
|
111
|
+
#
|
|
112
|
+
# @return [Integer] the maximum instance-name length
|
|
113
|
+
MAX_INSTANCE_NAME_LENGTH = 63
|
|
114
|
+
|
|
115
|
+
# Fixed size, in gigabytes, of every GCE local SSD.
|
|
116
|
+
#
|
|
117
|
+
# @see https://cloud.google.com/compute/docs/disks/#localssds
|
|
118
|
+
# @return [Integer] the local SSD size
|
|
119
|
+
LOCAL_SSD_SIZE_GB = 375
|
|
120
|
+
|
|
121
|
+
# Disk type identifying a local SSD rather than a persistent disk.
|
|
122
|
+
#
|
|
123
|
+
# @return [String] the local SSD disk type
|
|
124
|
+
LOCAL_SSD_TYPE = "local-ssd".freeze
|
|
125
|
+
|
|
126
|
+
# Configuration applied to every disk before the user's own settings.
|
|
127
|
+
#
|
|
128
|
+
# @return [Hash] the per-disk defaults
|
|
129
|
+
DISK_DEFAULT_CONFIG = {
|
|
130
|
+
autodelete_disk: true,
|
|
131
|
+
disk_size: 10,
|
|
132
|
+
disk_type: "pd-standard",
|
|
133
|
+
}.freeze
|
|
134
|
+
|
|
135
|
+
# Human-readable driver name shown in Test Kitchen output.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] the driver's display name
|
|
90
138
|
def name
|
|
91
139
|
"Google Compute (GCE)"
|
|
92
140
|
end
|
|
93
141
|
|
|
142
|
+
# Creates a GCE instance for the Test Kitchen suite and waits until its
|
|
143
|
+
# transport is reachable.
|
|
144
|
+
#
|
|
145
|
+
# Returns immediately if the state file already records a server, making
|
|
146
|
+
# the action idempotent. If any step fails, the partially-created
|
|
147
|
+
# instance and any standalone disks created along the way are torn down
|
|
148
|
+
# before the error is re-raised.
|
|
149
|
+
#
|
|
150
|
+
# @param state [Hash] the Test Kitchen state hash, mutated in place with
|
|
151
|
+
# `:server_name`, `:hostname` and `:zone`
|
|
152
|
+
# @return [void]
|
|
153
|
+
# @raise [StandardError] if instance creation fails for any reason
|
|
94
154
|
def create(state)
|
|
95
155
|
@state = state
|
|
96
156
|
return if state[:server_name]
|
|
@@ -121,10 +181,24 @@ module Kitchen
|
|
|
121
181
|
info("GCE instance <#{server_name}> created and ready.")
|
|
122
182
|
rescue => e
|
|
123
183
|
error("Error encountered during server creation: #{e.class}: #{e.message}")
|
|
124
|
-
|
|
184
|
+
begin
|
|
185
|
+
# The instance must go first: its disks cannot be deleted while it
|
|
186
|
+
# still holds them.
|
|
187
|
+
destroy(state)
|
|
188
|
+
ensure
|
|
189
|
+
delete_created_disks
|
|
190
|
+
end
|
|
125
191
|
raise
|
|
126
192
|
end
|
|
127
193
|
|
|
194
|
+
# Destroys the GCE instance recorded in the state file.
|
|
195
|
+
#
|
|
196
|
+
# Does nothing when the state file records no server, or when the
|
|
197
|
+
# instance no longer exists in GCE.
|
|
198
|
+
#
|
|
199
|
+
# @param state [Hash] the Test Kitchen state hash, mutated in place to
|
|
200
|
+
# remove `:server_name`, `:hostname` and `:zone`
|
|
201
|
+
# @return [void]
|
|
128
202
|
def destroy(state)
|
|
129
203
|
@state = state
|
|
130
204
|
server_name = state[:server_name]
|
|
@@ -144,78 +218,162 @@ module Kitchen
|
|
|
144
218
|
state.delete(:zone)
|
|
145
219
|
end
|
|
146
220
|
|
|
221
|
+
# Whether the deprecated single-boot-disk options are configured.
|
|
222
|
+
#
|
|
223
|
+
# @return [Boolean] true if any of `autodelete_disk`, `disk_size` or
|
|
224
|
+
# `disk_type` is set
|
|
147
225
|
def old_disk_configuration_present?
|
|
148
226
|
!config[:autodelete_disk].nil? || !config[:disk_size].nil? || !config[:disk_type].nil?
|
|
149
227
|
end
|
|
150
228
|
|
|
229
|
+
# Whether the multi-disk `disks` option is configured.
|
|
230
|
+
#
|
|
231
|
+
# @return [Boolean] true if `disks` is set
|
|
151
232
|
def new_disk_configuration_present?
|
|
152
233
|
!config[:disks].nil?
|
|
153
234
|
end
|
|
154
235
|
|
|
236
|
+
# Normalises whichever disk configuration style the user supplied into
|
|
237
|
+
# the canonical `disks` hash the rest of the driver consumes.
|
|
238
|
+
#
|
|
239
|
+
# Deprecated single-disk options are converted to a one-entry `disks`
|
|
240
|
+
# hash; an explicit `disks` hash has defaults applied, is validated, and
|
|
241
|
+
# has a boot disk chosen when none was flagged. When neither is present a
|
|
242
|
+
# single default boot disk is configured.
|
|
243
|
+
#
|
|
244
|
+
# @return [Hash{Symbol => Hash}] the normalised disk configuration, also
|
|
245
|
+
# written back to `config[:disks]`
|
|
246
|
+
# @raise [RuntimeError] if a disk name, disk type or boot-disk
|
|
247
|
+
# arrangement is invalid
|
|
155
248
|
def create_disks_config
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
249
|
+
# These defaults cannot live in default_config: their absence is what
|
|
250
|
+
# tells us which of the two configuration styles the user chose.
|
|
251
|
+
config[:disks] =
|
|
252
|
+
if old_disk_configuration_present?
|
|
253
|
+
{ disk1: legacy_disk_config }
|
|
254
|
+
elsif new_disk_configuration_present?
|
|
255
|
+
normalize_disks(config[:disks])
|
|
256
|
+
else
|
|
257
|
+
{ disk1: DISK_DEFAULT_CONFIG.merge(boot: true) }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Builds the single boot disk described by the deprecated
|
|
262
|
+
# `autodelete_disk`, `disk_size` and `disk_type` options.
|
|
263
|
+
#
|
|
264
|
+
# @return [Hash] the normalised boot disk configuration
|
|
265
|
+
# @raise [RuntimeError] if the configured disk type is not valid
|
|
266
|
+
# @api private
|
|
267
|
+
def legacy_disk_config
|
|
268
|
+
disk_config = {
|
|
269
|
+
boot: true,
|
|
270
|
+
autodelete_disk: config.fetch(:autodelete_disk, DISK_DEFAULT_CONFIG[:autodelete_disk]),
|
|
271
|
+
disk_size: config.fetch(:disk_size, DISK_DEFAULT_CONFIG[:disk_size]),
|
|
272
|
+
disk_type: config.fetch(:disk_type, DISK_DEFAULT_CONFIG[:disk_type]),
|
|
162
273
|
}
|
|
163
274
|
|
|
164
|
-
|
|
165
|
-
# If the old disk configuration is used,
|
|
166
|
-
# we'll convert it to the new one
|
|
167
|
-
config[:disks] = {
|
|
168
|
-
disk1: {
|
|
169
|
-
boot: true,
|
|
170
|
-
autodelete_disk: config.fetch(:autodelete_disk, disk_default_config[:autodelete_disk]),
|
|
171
|
-
disk_size: config.fetch(:disk_size, disk_default_config[:disk_size]),
|
|
172
|
-
disk_type: config.fetch(:disk_type, disk_default_config[:disk_type]),
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
raise "Disk type #{config[:disks][:disk1][:disk_type]} is not valid" unless valid_disk_type?(config[:disks][:disk1][:disk_type])
|
|
176
|
-
elsif new_disk_configuration_present?
|
|
177
|
-
# If the new disk configuration is present, ensure that for
|
|
178
|
-
# every disk the needed configuration is set
|
|
179
|
-
boot_disk_counter = 0
|
|
180
|
-
config[:disks].each do |disk_name, disk_config|
|
|
181
|
-
# te&/ => te
|
|
182
|
-
raise "Disk name invalid. Must match #{DISK_NAME_REGEX}." unless valid_disk_name?(disk_name)
|
|
183
|
-
|
|
184
|
-
# Update the config for the disk with the fixed config
|
|
185
|
-
config[:disks][disk_name.to_sym] = disk_default_config.merge(disk_config)
|
|
186
|
-
|
|
187
|
-
# Since the config was altered, we can't use disk_config (as it will be different or keys will not be present)
|
|
188
|
-
raise "Disk type #{config[:disks][disk_name.to_sym][:disk_type]} for disk #{disk_name} is not valid" unless valid_disk_type?(config[:disks][disk_name.to_sym][:disk_type])
|
|
189
|
-
|
|
190
|
-
unless disk_config[:boot].nil?
|
|
191
|
-
boot_disk_counter += 1
|
|
192
|
-
raise "Boot disk cannot be local SSD." if disk_config[:disk_type] == "local-ssd"
|
|
193
|
-
end
|
|
275
|
+
raise "Disk type #{disk_config[:disk_type]} is not valid" unless valid_disk_type?(disk_config[:disk_type])
|
|
194
276
|
|
|
195
|
-
|
|
196
|
-
|
|
277
|
+
disk_config
|
|
278
|
+
end
|
|
197
279
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"disk1": disk_default_config.merge({ boot: true }),
|
|
215
|
-
}
|
|
280
|
+
# Applies defaults to and validates every entry of a user-supplied
|
|
281
|
+
# `disks` hash, then ensures exactly one disk is marked bootable.
|
|
282
|
+
#
|
|
283
|
+
# Builds a new hash rather than mutating the one being iterated, so that
|
|
284
|
+
# string keys from `kitchen.yml` can be symbolised safely.
|
|
285
|
+
#
|
|
286
|
+
# @param disks [Hash] the raw `disks` configuration, keyed by disk name
|
|
287
|
+
# @return [Hash{Symbol => Hash}] the normalised disk configuration
|
|
288
|
+
# @raise [RuntimeError] if a disk name or type is invalid, or more than
|
|
289
|
+
# one boot disk is specified
|
|
290
|
+
# @api private
|
|
291
|
+
def normalize_disks(disks)
|
|
292
|
+
normalized = disks.each_with_object({}) do |(disk_name, disk_config), memo|
|
|
293
|
+
raise "Disk name invalid. Must match #{DISK_NAME_REGEX}." unless valid_disk_name?(disk_name)
|
|
294
|
+
|
|
295
|
+
memo[disk_name.to_sym] = normalize_disk(disk_name, disk_config)
|
|
216
296
|
end
|
|
297
|
+
|
|
298
|
+
assign_boot_disk(normalized)
|
|
217
299
|
end
|
|
218
300
|
|
|
301
|
+
# Applies the disk defaults to one disk entry and validates the result.
|
|
302
|
+
#
|
|
303
|
+
# @param disk_name [String, Symbol] the disk's name, used in error messages
|
|
304
|
+
# @param disk_config [Hash] the user-supplied configuration for this disk
|
|
305
|
+
# @return [Hash] the disk configuration with defaults applied
|
|
306
|
+
# @raise [RuntimeError] if the disk type is invalid, a local SSD is
|
|
307
|
+
# marked bootable, or a size is given for a local SSD
|
|
308
|
+
# @api private
|
|
309
|
+
def normalize_disk(disk_name, disk_config)
|
|
310
|
+
normalized = DISK_DEFAULT_CONFIG.merge(disk_config)
|
|
311
|
+
|
|
312
|
+
unless valid_disk_type?(normalized[:disk_type])
|
|
313
|
+
raise "Disk type #{normalized[:disk_type]} for disk #{disk_name} is not valid"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
return normalized unless local_ssd?(normalized)
|
|
317
|
+
|
|
318
|
+
raise "Boot disk cannot be local SSD." if normalized[:boot]
|
|
319
|
+
|
|
320
|
+
unless disk_config[:disk_size].nil?
|
|
321
|
+
raise "#{disk_name}: Cannot use 'disk_size' with local SSD. They always have " \
|
|
322
|
+
"#{LOCAL_SSD_SIZE_GB} GB (https://cloud.google.com/compute/docs/disks/#localssds)."
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# disk_size defaults to 10 above, which must not be sent for a local SSD.
|
|
326
|
+
normalized.merge(disk_size: nil)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Ensures exactly one disk in the set is marked as the boot disk,
|
|
330
|
+
# promoting the first eligible disk when the user flagged none.
|
|
331
|
+
#
|
|
332
|
+
# A disk is eligible unless it is a local SSD, which cannot boot, or the
|
|
333
|
+
# user explicitly set `boot: false` on it.
|
|
334
|
+
#
|
|
335
|
+
# @param disks [Hash{Symbol => Hash}] the normalised disk configuration
|
|
336
|
+
# @return [Hash{Symbol => Hash}] the configuration with one boot disk
|
|
337
|
+
# @raise [RuntimeError] if more than one boot disk is specified, no disks
|
|
338
|
+
# were given, or no disk is eligible to boot
|
|
339
|
+
# @api private
|
|
340
|
+
def assign_boot_disk(disks)
|
|
341
|
+
boot_disks = disks.select { |_disk_name, disk_config| disk_config[:boot] }
|
|
342
|
+
|
|
343
|
+
raise "More than one boot disk specified" if boot_disks.size > 1
|
|
344
|
+
return disks unless boot_disks.empty?
|
|
345
|
+
|
|
346
|
+
raise "No disks specified" if disks.empty?
|
|
347
|
+
|
|
348
|
+
bootable = disks.find do |_disk_name, disk_config|
|
|
349
|
+
!local_ssd?(disk_config) && disk_config[:boot] != false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
if bootable.nil?
|
|
353
|
+
raise "No boot disk specified, and no disk is eligible to become one. " \
|
|
354
|
+
"Local SSDs cannot boot, and disks set to 'boot: false' are excluded."
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
disk_name = bootable.first
|
|
358
|
+
warn("No bootdisk found - Assuming #{disk_name} will be boot disk")
|
|
359
|
+
disks.merge(disk_name => disks[disk_name].merge(boot: true))
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Whether a disk configuration describes a local SSD.
|
|
363
|
+
#
|
|
364
|
+
# @param disk_config [Hash] a disk configuration
|
|
365
|
+
# @return [Boolean] true if the disk type is `local-ssd`
|
|
366
|
+
# @api private
|
|
367
|
+
def local_ssd?(disk_config)
|
|
368
|
+
disk_config[:disk_type] == LOCAL_SSD_TYPE
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Validates the driver configuration against the GCE API, raising on the
|
|
372
|
+
# first problem found and warning about ambiguous or deprecated settings.
|
|
373
|
+
#
|
|
374
|
+
# @return [void]
|
|
375
|
+
# @raise [RuntimeError] if any configured project, zone, region, machine
|
|
376
|
+
# type, network, subnet, image or disk setting is invalid
|
|
219
377
|
def validate!
|
|
220
378
|
raise "Project #{config[:project]} is not a valid project" unless valid_project?
|
|
221
379
|
raise "Either zone or region must be specified" unless config[:zone] || config[:region]
|
|
@@ -239,6 +397,9 @@ module Kitchen
|
|
|
239
397
|
warn("These configs are deprecated - consider using new disks configuration") if old_disk_configuration_present?
|
|
240
398
|
end
|
|
241
399
|
|
|
400
|
+
# Memoised, authorised Compute Engine API client.
|
|
401
|
+
#
|
|
402
|
+
# @return [Google::Apis::ComputeV1::ComputeService] the API client
|
|
242
403
|
def connection
|
|
243
404
|
return @connection unless @connection.nil?
|
|
244
405
|
|
|
@@ -252,6 +413,9 @@ module Kitchen
|
|
|
252
413
|
@connection
|
|
253
414
|
end
|
|
254
415
|
|
|
416
|
+
# Application default credentials scoped for Compute Engine.
|
|
417
|
+
#
|
|
418
|
+
# @return [Google::Auth::Credentials] the resolved credentials
|
|
255
419
|
def authorization
|
|
256
420
|
@authorization ||= Google::Auth.get_application_default(
|
|
257
421
|
[
|
|
@@ -261,10 +425,18 @@ module Kitchen
|
|
|
261
425
|
)
|
|
262
426
|
end
|
|
263
427
|
|
|
428
|
+
# Whether the suite's transport is WinRM, implying a Windows guest.
|
|
429
|
+
#
|
|
430
|
+
# @return [Boolean] true when the transport is WinRM
|
|
264
431
|
def winrm_transport?
|
|
265
432
|
instance.transport.name.casecmp("winrm") == 0
|
|
266
433
|
end
|
|
267
434
|
|
|
435
|
+
# Resets the Windows password for the transport's user and stores it in
|
|
436
|
+
# the state file. A no-op for non-WinRM transports.
|
|
437
|
+
#
|
|
438
|
+
# @param server_name [String] the GCE instance name
|
|
439
|
+
# @return [void]
|
|
268
440
|
def update_windows_password(server_name)
|
|
269
441
|
return unless winrm_transport?
|
|
270
442
|
|
|
@@ -282,9 +454,14 @@ module Kitchen
|
|
|
282
454
|
opts[:timeout] = config[:winpass_timeout] unless config[:winpass_timeout].nil?
|
|
283
455
|
state[:password] = GoogleComputeWindowsPassword.new(**opts).new_password
|
|
284
456
|
|
|
285
|
-
info("Password reset complete on #{server_name}
|
|
457
|
+
info("Password reset complete on #{server_name}.")
|
|
286
458
|
end
|
|
287
459
|
|
|
460
|
+
# Runs an API call and reports whether it succeeded, swallowing client
|
|
461
|
+
# errors so callers can use it as a validity predicate.
|
|
462
|
+
#
|
|
463
|
+
# @yield the API call to attempt
|
|
464
|
+
# @return [Boolean] true if the call succeeded, false on a client error
|
|
288
465
|
def check_api_call(&block)
|
|
289
466
|
yield
|
|
290
467
|
rescue Google::Apis::ClientError => e
|
|
@@ -294,94 +471,161 @@ module Kitchen
|
|
|
294
471
|
true
|
|
295
472
|
end
|
|
296
473
|
|
|
474
|
+
# Whether the configured project exists and is reachable.
|
|
475
|
+
#
|
|
476
|
+
# @return [Boolean] true if the project is valid
|
|
297
477
|
def valid_project?
|
|
298
478
|
check_api_call { connection.get_project(project) }
|
|
299
479
|
end
|
|
300
480
|
|
|
481
|
+
# Whether the configured machine type exists in the target zone.
|
|
482
|
+
#
|
|
483
|
+
# @return [Boolean] true if the machine type is valid
|
|
301
484
|
def valid_machine_type?
|
|
302
485
|
return false if config[:machine_type].nil?
|
|
303
486
|
|
|
304
487
|
check_api_call { connection.get_machine_type(project, zone, config[:machine_type]) }
|
|
305
488
|
end
|
|
306
489
|
|
|
490
|
+
# Whether the configured network exists in the network project.
|
|
491
|
+
#
|
|
492
|
+
# @return [Boolean] true if the network is valid
|
|
307
493
|
def valid_network?
|
|
308
494
|
return false if config[:network].nil?
|
|
309
495
|
|
|
310
496
|
check_api_call { connection.get_network(network_project, config[:network]) }
|
|
311
497
|
end
|
|
312
498
|
|
|
499
|
+
# Whether the configured subnet exists in the subnet project and region.
|
|
500
|
+
#
|
|
501
|
+
# @return [Boolean] true if the subnet is valid
|
|
313
502
|
def valid_subnet?
|
|
314
503
|
return false if config[:subnet].nil?
|
|
315
504
|
|
|
316
505
|
check_api_call { connection.get_subnetwork(subnet_project, region, config[:subnet]) }
|
|
317
506
|
end
|
|
318
507
|
|
|
508
|
+
# Whether the configured zone exists in the project.
|
|
509
|
+
#
|
|
510
|
+
# @return [Boolean] true if the zone is valid
|
|
319
511
|
def valid_zone?
|
|
320
512
|
return false if config[:zone].nil?
|
|
321
513
|
|
|
322
514
|
check_api_call { connection.get_zone(project, config[:zone]) }
|
|
323
515
|
end
|
|
324
516
|
|
|
517
|
+
# Whether the configured region exists in the project.
|
|
518
|
+
#
|
|
519
|
+
# @return [Boolean] true if the region is valid
|
|
325
520
|
def valid_region?
|
|
326
521
|
return false if config[:region].nil?
|
|
327
522
|
|
|
328
523
|
check_api_call { connection.get_region(project, config[:region]) }
|
|
329
524
|
end
|
|
330
525
|
|
|
526
|
+
# Whether a disk type exists in the target zone.
|
|
527
|
+
#
|
|
528
|
+
# @param disk_type [String, nil] the disk type to check
|
|
529
|
+
# @return [Boolean] true if the disk type is valid
|
|
331
530
|
def valid_disk_type?(disk_type)
|
|
332
531
|
return false if disk_type.nil?
|
|
333
532
|
|
|
334
533
|
check_api_call { connection.get_disk_type(project, zone, disk_type) }
|
|
335
534
|
end
|
|
336
535
|
|
|
536
|
+
# Whether a disk name matches {DISK_NAME_REGEX} in full.
|
|
537
|
+
#
|
|
538
|
+
# @param disk_name [String, Symbol] the disk name to check
|
|
539
|
+
# @return [Boolean] true if the whole name matches the pattern
|
|
337
540
|
def valid_disk_name?(disk_name)
|
|
338
|
-
disk_name.to_s.match(DISK_NAME_REGEX)
|
|
541
|
+
disk_name.to_s.match?(/\A#{DISK_NAME_REGEX}\z/)
|
|
339
542
|
end
|
|
340
543
|
|
|
544
|
+
# Whether an image exists in the image project.
|
|
545
|
+
#
|
|
546
|
+
# @param image [String] the image name, defaulting to the configured one
|
|
547
|
+
# @return [Boolean] true if the image exists
|
|
341
548
|
def image_exist?(image = image_name)
|
|
342
549
|
check_api_call { connection.get_image(image_project, image) }
|
|
343
550
|
end
|
|
344
551
|
|
|
552
|
+
# Whether a GCE instance exists in the target project and zone.
|
|
553
|
+
#
|
|
554
|
+
# @param server_name [String] the instance name
|
|
555
|
+
# @return [Boolean] true if the instance exists
|
|
345
556
|
def server_exist?(server_name)
|
|
346
557
|
check_api_call { server_instance(server_name) }
|
|
347
558
|
end
|
|
348
559
|
|
|
560
|
+
# The configured GCP project.
|
|
561
|
+
#
|
|
562
|
+
# @return [String] the project ID
|
|
349
563
|
def project
|
|
350
564
|
config[:project]
|
|
351
565
|
end
|
|
352
566
|
|
|
567
|
+
# Name of the boot image, resolved from the image family when only a
|
|
568
|
+
# family was configured.
|
|
569
|
+
#
|
|
570
|
+
# @return [String] the image name
|
|
353
571
|
def image_name
|
|
354
572
|
@image_name ||= config[:image_name] || image_name_for_family(config[:image_family])
|
|
355
573
|
end
|
|
356
574
|
|
|
575
|
+
# Project searched for images, defaulting to the instance's own project.
|
|
576
|
+
#
|
|
577
|
+
# @return [String] the image project ID
|
|
357
578
|
def image_project
|
|
358
579
|
config[:image_project].nil? ? project : config[:image_project]
|
|
359
580
|
end
|
|
360
581
|
|
|
582
|
+
# Project searched for subnets, defaulting to the instance's own project.
|
|
583
|
+
#
|
|
584
|
+
# @return [String] the subnet project ID
|
|
361
585
|
def subnet_project
|
|
362
586
|
config[:subnet_project].nil? ? project : config[:subnet_project]
|
|
363
587
|
end
|
|
364
588
|
|
|
589
|
+
# Project searched for networks, defaulting to the instance's own project.
|
|
590
|
+
#
|
|
591
|
+
# @return [String] the network project ID
|
|
365
592
|
def network_project
|
|
366
593
|
config[:network_project].nil? ? project : config[:network_project]
|
|
367
594
|
end
|
|
368
595
|
|
|
596
|
+
# The static internal IP to assign, if one was configured.
|
|
597
|
+
#
|
|
598
|
+
# @return [String, nil] the internal IP address
|
|
369
599
|
def network_ip
|
|
370
600
|
config[:network_ip]
|
|
371
601
|
end
|
|
372
602
|
|
|
603
|
+
# The target region, derived from the zone when not configured directly.
|
|
604
|
+
#
|
|
605
|
+
# @return [String] the region name
|
|
373
606
|
def region
|
|
374
607
|
config[:region].nil? ? region_for_zone : config[:region]
|
|
375
608
|
end
|
|
376
609
|
|
|
610
|
+
# Looks up which region the target zone belongs to.
|
|
611
|
+
#
|
|
612
|
+
# @return [String] the region name
|
|
377
613
|
def region_for_zone
|
|
378
614
|
@region_for_zone ||= connection.get_zone(project, zone).region.split("/").last
|
|
379
615
|
end
|
|
380
616
|
|
|
617
|
+
# The target zone, taken from the state file or configuration, or chosen
|
|
618
|
+
# at random from the configured region.
|
|
619
|
+
#
|
|
620
|
+
# @return [String] the zone name
|
|
381
621
|
def zone
|
|
382
622
|
@zone ||= state[:zone] || config[:zone] || find_zone
|
|
383
623
|
end
|
|
384
624
|
|
|
625
|
+
# Picks a random zone that is up in the configured region.
|
|
626
|
+
#
|
|
627
|
+
# @return [String] the chosen zone name
|
|
628
|
+
# @raise [RuntimeError] if no zone in the region is available
|
|
385
629
|
def find_zone
|
|
386
630
|
zone = zones_in_region.sample
|
|
387
631
|
raise "Unable to find a suitable zone in #{region}" if zone.nil?
|
|
@@ -389,6 +633,9 @@ module Kitchen
|
|
|
389
633
|
zone.name
|
|
390
634
|
end
|
|
391
635
|
|
|
636
|
+
# All zones in the configured region whose status is `UP`.
|
|
637
|
+
#
|
|
638
|
+
# @return [Array<Google::Apis::ComputeV1::Zone>] the available zones
|
|
392
639
|
def zones_in_region
|
|
393
640
|
connection.list_zones(project).items.select do |zone|
|
|
394
641
|
zone.status == "UP" &&
|
|
@@ -396,26 +643,49 @@ module Kitchen
|
|
|
396
643
|
end
|
|
397
644
|
end
|
|
398
645
|
|
|
646
|
+
# Fetches a GCE instance.
|
|
647
|
+
#
|
|
648
|
+
# @param server_name [String] the instance name
|
|
649
|
+
# @return [Google::Apis::ComputeV1::Instance] the instance
|
|
399
650
|
def server_instance(server_name)
|
|
400
651
|
connection.get_instance(project, zone, server_name)
|
|
401
652
|
end
|
|
402
653
|
|
|
654
|
+
# The IP address Test Kitchen should connect to, honouring
|
|
655
|
+
# `use_private_ip`.
|
|
656
|
+
#
|
|
657
|
+
# @param server [Google::Apis::ComputeV1::Instance] the instance
|
|
658
|
+
# @return [String] the IP address
|
|
403
659
|
def ip_address_for(server)
|
|
404
660
|
config[:use_private_ip] ? private_ip_for(server) : public_ip_for(server)
|
|
405
661
|
end
|
|
406
662
|
|
|
663
|
+
# The instance's internal IP address.
|
|
664
|
+
#
|
|
665
|
+
# @param server [Google::Apis::ComputeV1::Instance] the instance
|
|
666
|
+
# @return [String] the private IP address
|
|
667
|
+
# @raise [RuntimeError] if the instance has no network interface
|
|
407
668
|
def private_ip_for(server)
|
|
408
669
|
server.network_interfaces.first.network_ip
|
|
409
670
|
rescue NoMethodError
|
|
410
671
|
raise "Unable to determine private IP for instance"
|
|
411
672
|
end
|
|
412
673
|
|
|
674
|
+
# The instance's external NAT IP address.
|
|
675
|
+
#
|
|
676
|
+
# @param server [Google::Apis::ComputeV1::Instance] the instance
|
|
677
|
+
# @return [String] the public IP address
|
|
678
|
+
# @raise [RuntimeError] if the instance has no external access config
|
|
413
679
|
def public_ip_for(server)
|
|
414
680
|
server.network_interfaces.first.access_configs.first.nat_ip
|
|
415
681
|
rescue NoMethodError
|
|
416
682
|
raise "Unable to determine public IP for instance"
|
|
417
683
|
end
|
|
418
684
|
|
|
685
|
+
# Assembles the full instance definition sent to the GCE API.
|
|
686
|
+
#
|
|
687
|
+
# @param server_name [String] the instance name
|
|
688
|
+
# @return [Google::Apis::ComputeV1::Instance] the instance to create
|
|
419
689
|
def create_instance_object(server_name)
|
|
420
690
|
inst_obj = Google::Apis::ComputeV1::Instance.new
|
|
421
691
|
inst_obj.name = server_name
|
|
@@ -432,10 +702,14 @@ module Kitchen
|
|
|
432
702
|
inst_obj
|
|
433
703
|
end
|
|
434
704
|
|
|
705
|
+
# Builds a unique, GCE-legal instance name, falling back to a UUID when
|
|
706
|
+
# the Test Kitchen instance name would make it too long.
|
|
707
|
+
#
|
|
708
|
+
# @return [String] the instance name
|
|
435
709
|
def generate_server_name
|
|
436
710
|
name = config[:inst_name] || "tk-#{instance.name.downcase}-#{SecureRandom.hex(3)}"
|
|
437
711
|
|
|
438
|
-
if name.length >
|
|
712
|
+
if name.length > MAX_INSTANCE_NAME_LENGTH
|
|
439
713
|
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.")
|
|
440
714
|
name = "tk-#{SecureRandom.uuid}"
|
|
441
715
|
end
|
|
@@ -443,6 +717,11 @@ module Kitchen
|
|
|
443
717
|
name.gsub(/([^-a-z0-9])/, "-")
|
|
444
718
|
end
|
|
445
719
|
|
|
720
|
+
# Builds every disk for the instance, creating standalone persistent
|
|
721
|
+
# disks up front where required. The boot disk is always listed first.
|
|
722
|
+
#
|
|
723
|
+
# @param server_name [String] the instance name, used to derive disk names
|
|
724
|
+
# @return [Array<Google::Apis::ComputeV1::AttachedDisk>] the disks
|
|
446
725
|
def create_disks(server_name)
|
|
447
726
|
disks = []
|
|
448
727
|
config[:disks].each do |disk_name, disk_config|
|
|
@@ -450,7 +729,7 @@ module Kitchen
|
|
|
450
729
|
if disk_config[:boot]
|
|
451
730
|
disk = create_local_disk(unique_disk_name, disk_config)
|
|
452
731
|
disks.unshift(disk)
|
|
453
|
-
elsif (disk_config
|
|
732
|
+
elsif local_ssd?(disk_config) || disk_config[:custom_image]
|
|
454
733
|
disk = create_local_disk(unique_disk_name, disk_config)
|
|
455
734
|
disks.push(disk)
|
|
456
735
|
else
|
|
@@ -461,32 +740,43 @@ module Kitchen
|
|
|
461
740
|
disks
|
|
462
741
|
end
|
|
463
742
|
|
|
743
|
+
# Builds a disk created inline with the instance, from either the boot
|
|
744
|
+
# image, a custom image, or as local SSD scratch space.
|
|
745
|
+
#
|
|
746
|
+
# @param unique_disk_name [String] the disk's name
|
|
747
|
+
# @param disk_config [Hash] the normalised disk configuration
|
|
748
|
+
# @return [Google::Apis::ComputeV1::AttachedDisk] the disk
|
|
464
749
|
def create_local_disk(unique_disk_name, disk_config)
|
|
465
750
|
disk = Google::Apis::ComputeV1::AttachedDisk.new
|
|
466
751
|
# Specifies the parameters for a new disk that will be created alongside the new instance.
|
|
467
752
|
params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new
|
|
468
|
-
disk.boot = true if
|
|
753
|
+
disk.boot = true if disk_config[:boot]
|
|
469
754
|
disk.auto_delete = disk_config[:autodelete_disk]
|
|
470
755
|
params.disk_size_gb = disk_config[:disk_size]
|
|
471
756
|
params.disk_type = disk_type_url_for(disk_config[:disk_type])
|
|
472
757
|
|
|
473
|
-
if disk_config
|
|
474
|
-
info("Creating a
|
|
758
|
+
if local_ssd?(disk_config)
|
|
759
|
+
info("Creating a #{LOCAL_SSD_SIZE_GB} GB local ssd as scratch disk (https://cloud.google.com/compute/docs/disks/#localssds).")
|
|
475
760
|
disk.type = "SCRATCH"
|
|
476
761
|
elsif disk.boot
|
|
477
762
|
info("Creating a #{disk_config[:disk_size]} GB boot disk named #{unique_disk_name} from image #{image_name}...")
|
|
478
|
-
params.source_image = boot_disk_source_image
|
|
479
|
-
params.disk_name = unique_disk_name
|
|
763
|
+
params.source_image = boot_disk_source_image
|
|
764
|
+
params.disk_name = unique_disk_name
|
|
480
765
|
else
|
|
481
766
|
info("Creating a #{disk_config[:disk_size]} GB extra disk named #{unique_disk_name} from image #{disk_config[:custom_image]}...")
|
|
482
|
-
params.source_image = image_url(disk_config[:custom_image])
|
|
483
|
-
params.disk_name = unique_disk_name
|
|
484
|
-
|
|
767
|
+
params.source_image = image_url(disk_config[:custom_image])
|
|
768
|
+
params.disk_name = unique_disk_name
|
|
485
769
|
end
|
|
486
770
|
disk.initialize_params = params
|
|
487
771
|
disk
|
|
488
772
|
end
|
|
489
773
|
|
|
774
|
+
# Creates a standalone persistent disk, waits for it to become ready, and
|
|
775
|
+
# returns a reference attaching it to the instance.
|
|
776
|
+
#
|
|
777
|
+
# @param unique_disk_name [String] the disk's name
|
|
778
|
+
# @param disk_config [Hash] the normalised disk configuration
|
|
779
|
+
# @return [Google::Apis::ComputeV1::AttachedDisk] the attachment
|
|
490
780
|
def create_attached_disk(unique_disk_name, disk_config)
|
|
491
781
|
disk = Google::Apis::ComputeV1::Disk.new
|
|
492
782
|
disk.name = unique_disk_name
|
|
@@ -495,6 +785,7 @@ module Kitchen
|
|
|
495
785
|
|
|
496
786
|
info("Creating a #{disk_config[:disk_size]} GB disk named #{unique_disk_name}...")
|
|
497
787
|
wait_for_operation(connection.insert_disk(project, zone, disk))
|
|
788
|
+
created_disk_names << unique_disk_name
|
|
498
789
|
info("Waiting for disk to be ready...")
|
|
499
790
|
wait_for_status("READY") { connection.get_disk(project, zone, unique_disk_name) }
|
|
500
791
|
info("Disk created successfully.")
|
|
@@ -504,6 +795,28 @@ module Kitchen
|
|
|
504
795
|
attached_disk
|
|
505
796
|
end
|
|
506
797
|
|
|
798
|
+
# Names of the standalone disks this driver created during the current
|
|
799
|
+
# action, tracked so they can be cleaned up if creation fails.
|
|
800
|
+
#
|
|
801
|
+
# @return [Array<String>] the created disk names
|
|
802
|
+
def created_disk_names
|
|
803
|
+
@created_disk_names ||= []
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Deletes every standalone disk created during a failed create, so a
|
|
807
|
+
# partial run does not leave billable disks behind.
|
|
808
|
+
#
|
|
809
|
+
# @return [void]
|
|
810
|
+
def delete_created_disks
|
|
811
|
+
created_disk_names.each { |disk_name| delete_disk(disk_name) }
|
|
812
|
+
created_disk_names.clear
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Deletes a standalone persistent disk, tolerating one that is already
|
|
816
|
+
# gone.
|
|
817
|
+
#
|
|
818
|
+
# @param unique_disk_name [String] the disk's name
|
|
819
|
+
# @return [void]
|
|
507
820
|
def delete_disk(unique_disk_name)
|
|
508
821
|
begin
|
|
509
822
|
connection.get_disk(project, zone, unique_disk_name)
|
|
@@ -517,35 +830,64 @@ module Kitchen
|
|
|
517
830
|
info("Disk #{unique_disk_name} deleted successfully.")
|
|
518
831
|
end
|
|
519
832
|
|
|
833
|
+
# Partial URL identifying a disk type in the target zone.
|
|
834
|
+
#
|
|
835
|
+
# @param type [String] the disk type
|
|
836
|
+
# @return [String] the disk type URL
|
|
520
837
|
def disk_type_url_for(type)
|
|
521
838
|
"zones/#{zone}/diskTypes/#{type}"
|
|
522
839
|
end
|
|
523
840
|
|
|
841
|
+
# Partial URL identifying a disk in the target project and zone.
|
|
842
|
+
#
|
|
843
|
+
# @param unique_disk_name [String] the disk's name
|
|
844
|
+
# @return [String] the disk's self link
|
|
524
845
|
def disk_self_link(unique_disk_name)
|
|
525
846
|
"projects/#{project}/zones/#{zone}/disks/#{unique_disk_name}"
|
|
526
847
|
end
|
|
527
848
|
|
|
849
|
+
# Memoised URL of the image the boot disk is created from.
|
|
850
|
+
#
|
|
851
|
+
# @return [String, nil] the image URL, or nil if the image is missing
|
|
528
852
|
def boot_disk_source_image
|
|
529
853
|
@boot_disk_source ||= image_url
|
|
530
854
|
end
|
|
531
855
|
|
|
856
|
+
# URL of an image, provided it exists in the image project.
|
|
857
|
+
#
|
|
858
|
+
# @param image [String] the image name, defaulting to the configured one
|
|
859
|
+
# @return [String, nil] the image URL, or nil if the image is missing
|
|
532
860
|
def image_url(image = image_name)
|
|
533
861
|
"projects/#{image_project}/global/images/#{image}" if image_exist?(image)
|
|
534
862
|
end
|
|
535
863
|
|
|
864
|
+
# Resolves the current image name for an image family.
|
|
865
|
+
#
|
|
866
|
+
# @param image_family [String] the image family
|
|
867
|
+
# @return [String] the image name
|
|
536
868
|
def image_name_for_family(image_family)
|
|
537
869
|
image = connection.get_image_from_family(image_project, image_family)
|
|
538
870
|
image.name
|
|
539
871
|
end
|
|
540
872
|
|
|
873
|
+
# Partial URL identifying the machine type in the target zone.
|
|
874
|
+
#
|
|
875
|
+
# @return [String] the machine type URL
|
|
541
876
|
def machine_type_url
|
|
542
877
|
"zones/#{zone}/machineTypes/#{config[:machine_type]}"
|
|
543
878
|
end
|
|
544
879
|
|
|
880
|
+
# The configured guest accelerators.
|
|
881
|
+
#
|
|
882
|
+
# @return [Array<Hash>] the accelerator configurations
|
|
545
883
|
def guest_accelerators
|
|
546
884
|
config[:guest_accelerators]
|
|
547
885
|
end
|
|
548
886
|
|
|
887
|
+
# Builds accelerator definitions for the instance, skipping any entry
|
|
888
|
+
# that does not name a type and defaulting the count to one.
|
|
889
|
+
#
|
|
890
|
+
# @return [Array<Google::Apis::ComputeV1::AcceleratorConfig>] the accelerators
|
|
549
891
|
def instance_guest_accelerators
|
|
550
892
|
guest_accelerator_configs = []
|
|
551
893
|
|
|
@@ -567,6 +909,10 @@ module Kitchen
|
|
|
567
909
|
guest_accelerator_configs
|
|
568
910
|
end
|
|
569
911
|
|
|
912
|
+
# The instance metadata, merging the driver's own keys over any the user
|
|
913
|
+
# configured and adding a WinRM bootstrap script for Windows guests.
|
|
914
|
+
#
|
|
915
|
+
# @return [Hash{String => String}] the metadata
|
|
570
916
|
def metadata
|
|
571
917
|
default_metadata = {
|
|
572
918
|
"created-by" => "test-kitchen",
|
|
@@ -584,6 +930,9 @@ module Kitchen
|
|
|
584
930
|
config[:metadata].merge(default_metadata)
|
|
585
931
|
end
|
|
586
932
|
|
|
933
|
+
# The metadata in the form the GCE API expects.
|
|
934
|
+
#
|
|
935
|
+
# @return [Google::Apis::ComputeV1::Metadata] the metadata object
|
|
587
936
|
def instance_metadata
|
|
588
937
|
Google::Apis::ComputeV1::Metadata.new.tap do |metadata_obj|
|
|
589
938
|
metadata_obj.items = metadata.each_with_object([]) do |(k, v), memo|
|
|
@@ -595,14 +944,23 @@ module Kitchen
|
|
|
595
944
|
end
|
|
596
945
|
end
|
|
597
946
|
|
|
947
|
+
# The configured instance labels.
|
|
948
|
+
#
|
|
949
|
+
# @return [Hash] the labels
|
|
598
950
|
def instance_labels
|
|
599
951
|
config[:labels]
|
|
600
952
|
end
|
|
601
953
|
|
|
954
|
+
# The username recorded in instance metadata.
|
|
955
|
+
#
|
|
956
|
+
# @return [String] the current user, or `"unknown"`
|
|
602
957
|
def env_user
|
|
603
958
|
ENV["USER"] || "unknown"
|
|
604
959
|
end
|
|
605
960
|
|
|
961
|
+
# Builds the instance's single network interface.
|
|
962
|
+
#
|
|
963
|
+
# @return [Array<Google::Apis::ComputeV1::NetworkInterface>] the interface
|
|
606
964
|
def instance_network_interfaces
|
|
607
965
|
interface = Google::Apis::ComputeV1::NetworkInterface.new
|
|
608
966
|
interface.network = network_url if config[:subnet_project].nil?
|
|
@@ -613,16 +971,26 @@ module Kitchen
|
|
|
613
971
|
Array(interface)
|
|
614
972
|
end
|
|
615
973
|
|
|
974
|
+
# Partial URL identifying the configured network.
|
|
975
|
+
#
|
|
976
|
+
# @return [String] the network URL
|
|
616
977
|
def network_url
|
|
617
978
|
"projects/#{network_project}/global/networks/#{config[:network]}"
|
|
618
979
|
end
|
|
619
980
|
|
|
981
|
+
# Partial URL identifying the configured subnet.
|
|
982
|
+
#
|
|
983
|
+
# @return [String, nil] the subnet URL, or nil when no subnet is set
|
|
620
984
|
def subnet_url
|
|
621
985
|
return unless config[:subnet]
|
|
622
986
|
|
|
623
987
|
"projects/#{subnet_project}/regions/#{region}/subnetworks/#{config[:subnet]}"
|
|
624
988
|
end
|
|
625
989
|
|
|
990
|
+
# The interface's external access configuration, omitted entirely when
|
|
991
|
+
# `use_private_ip` is set.
|
|
992
|
+
#
|
|
993
|
+
# @return [Array<Google::Apis::ComputeV1::AccessConfig>] the access configs
|
|
626
994
|
def interface_access_configs
|
|
627
995
|
return [] if config[:use_private_ip]
|
|
628
996
|
|
|
@@ -633,30 +1001,55 @@ module Kitchen
|
|
|
633
1001
|
Array(access_config)
|
|
634
1002
|
end
|
|
635
1003
|
|
|
1004
|
+
# The instance's scheduling options.
|
|
1005
|
+
#
|
|
1006
|
+
# @return [Google::Apis::ComputeV1::Scheduling] the scheduling options
|
|
636
1007
|
def instance_scheduling
|
|
637
1008
|
Google::Apis::ComputeV1::Scheduling.new.tap do |scheduling|
|
|
638
|
-
scheduling.automatic_restart = auto_restart
|
|
639
|
-
scheduling.preemptible = preemptible
|
|
1009
|
+
scheduling.automatic_restart = auto_restart?
|
|
1010
|
+
scheduling.preemptible = preemptible?
|
|
640
1011
|
scheduling.on_host_maintenance = migrate_setting
|
|
641
1012
|
end
|
|
642
1013
|
end
|
|
643
1014
|
|
|
1015
|
+
# Whether the instance should be preemptible.
|
|
1016
|
+
#
|
|
1017
|
+
# @return [Boolean] true if preemptible
|
|
644
1018
|
def preemptible?
|
|
645
|
-
config[:preemptible]
|
|
1019
|
+
config[:preemptible] ? true : false
|
|
646
1020
|
end
|
|
647
1021
|
|
|
1022
|
+
# Whether the instance may live-migrate. Always false when preemptible,
|
|
1023
|
+
# which GCE does not allow to migrate.
|
|
1024
|
+
#
|
|
1025
|
+
# @return [Boolean] true if live migration is enabled
|
|
648
1026
|
def auto_migrate?
|
|
649
|
-
|
|
1027
|
+
return false if preemptible?
|
|
1028
|
+
|
|
1029
|
+
config[:auto_migrate] ? true : false
|
|
650
1030
|
end
|
|
651
1031
|
|
|
1032
|
+
# Whether the instance should restart automatically. Always false when
|
|
1033
|
+
# preemptible, which GCE does not allow to auto-restart.
|
|
1034
|
+
#
|
|
1035
|
+
# @return [Boolean] true if auto-restart is enabled
|
|
652
1036
|
def auto_restart?
|
|
653
|
-
|
|
1037
|
+
return false if preemptible?
|
|
1038
|
+
|
|
1039
|
+
config[:auto_restart] ? true : false
|
|
654
1040
|
end
|
|
655
1041
|
|
|
1042
|
+
# The host maintenance behaviour implied by {#auto_migrate?}.
|
|
1043
|
+
#
|
|
1044
|
+
# @return [String] `"MIGRATE"` or `"TERMINATE"`
|
|
656
1045
|
def migrate_setting
|
|
657
1046
|
auto_migrate? ? "MIGRATE" : "TERMINATE"
|
|
658
1047
|
end
|
|
659
1048
|
|
|
1049
|
+
# The service account and scopes attached to the instance.
|
|
1050
|
+
#
|
|
1051
|
+
# @return [Array<Google::Apis::ComputeV1::ServiceAccount>, nil] the
|
|
1052
|
+
# service accounts, or nil when no scopes are configured
|
|
660
1053
|
def instance_service_accounts
|
|
661
1054
|
return if config[:service_account_scopes].nil? || config[:service_account_scopes].empty?
|
|
662
1055
|
|
|
@@ -667,28 +1060,54 @@ module Kitchen
|
|
|
667
1060
|
Array(service_account)
|
|
668
1061
|
end
|
|
669
1062
|
|
|
1063
|
+
# Expands a scope alias or bare scope name into a full OAuth 2.0 URL,
|
|
1064
|
+
# passing through anything that is already one.
|
|
1065
|
+
#
|
|
1066
|
+
# @param scope [String] the scope, alias or URL
|
|
1067
|
+
# @return [String] the fully-qualified scope URL
|
|
670
1068
|
def service_account_scope_url(scope)
|
|
671
1069
|
return scope if scope.start_with?("https://www.googleapis.com/auth/")
|
|
672
1070
|
|
|
673
1071
|
"https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
|
|
674
1072
|
end
|
|
675
1073
|
|
|
1074
|
+
# Translates a `gcloud` scope alias into its scope path, returning the
|
|
1075
|
+
# input unchanged when it is not a known alias.
|
|
1076
|
+
#
|
|
1077
|
+
# @param scope_alias [String] the alias to translate
|
|
1078
|
+
# @return [String] the scope path
|
|
676
1079
|
def translate_scope_alias(scope_alias)
|
|
677
1080
|
SCOPE_ALIAS_MAP.fetch(scope_alias, scope_alias)
|
|
678
1081
|
end
|
|
679
1082
|
|
|
1083
|
+
# The configured network tags in the form the GCE API expects.
|
|
1084
|
+
#
|
|
1085
|
+
# @return [Google::Apis::ComputeV1::Tags] the tags object
|
|
680
1086
|
def instance_tags
|
|
681
1087
|
Google::Apis::ComputeV1::Tags.new.tap { |tag_obj| tag_obj.items = config[:tags] }
|
|
682
1088
|
end
|
|
683
1089
|
|
|
1090
|
+
# How long, in seconds, to wait for an operation or status change.
|
|
1091
|
+
#
|
|
1092
|
+
# @return [Integer] the wait timeout
|
|
684
1093
|
def wait_time
|
|
685
1094
|
config[:wait_time]
|
|
686
1095
|
end
|
|
687
1096
|
|
|
1097
|
+
# How long, in seconds, to sleep between status polls.
|
|
1098
|
+
#
|
|
1099
|
+
# @return [Integer] the poll interval
|
|
688
1100
|
def refresh_rate
|
|
689
1101
|
config[:refresh_rate]
|
|
690
1102
|
end
|
|
691
1103
|
|
|
1104
|
+
# Polls the yielded resource until it reports the requested status,
|
|
1105
|
+
# logging each status change.
|
|
1106
|
+
#
|
|
1107
|
+
# @param requested_status [String] the status to wait for
|
|
1108
|
+
# @yieldreturn [#status] the resource to poll
|
|
1109
|
+
# @return [void]
|
|
1110
|
+
# @raise [Timeout::Error] if the status is not reached within {#wait_time}
|
|
692
1111
|
def wait_for_status(requested_status, &block)
|
|
693
1112
|
last_status = ""
|
|
694
1113
|
|
|
@@ -714,6 +1133,12 @@ module Kitchen
|
|
|
714
1133
|
end
|
|
715
1134
|
end
|
|
716
1135
|
|
|
1136
|
+
# Waits for a zone operation to finish and raises if it reported errors.
|
|
1137
|
+
#
|
|
1138
|
+
# @param operation [Google::Apis::ComputeV1::Operation] the operation
|
|
1139
|
+
# @return [void]
|
|
1140
|
+
# @raise [RuntimeError] if the operation completed with errors
|
|
1141
|
+
# @raise [Timeout::Error] if the operation did not finish in time
|
|
717
1142
|
def wait_for_operation(operation)
|
|
718
1143
|
operation_name = operation.name
|
|
719
1144
|
|
|
@@ -729,6 +1154,11 @@ module Kitchen
|
|
|
729
1154
|
raise "Operation #{operation_name} failed."
|
|
730
1155
|
end
|
|
731
1156
|
|
|
1157
|
+
# Waits until the suite's transport can reach the instance, destroying it
|
|
1158
|
+
# if it never becomes reachable.
|
|
1159
|
+
#
|
|
1160
|
+
# @return [void]
|
|
1161
|
+
# @raise [StandardError] if the server cannot be reached
|
|
732
1162
|
def wait_for_server
|
|
733
1163
|
instance.transport.connection(state).wait_until_ready
|
|
734
1164
|
rescue
|
|
@@ -737,10 +1167,18 @@ module Kitchen
|
|
|
737
1167
|
raise
|
|
738
1168
|
end
|
|
739
1169
|
|
|
1170
|
+
# Fetches the current state of a zone operation.
|
|
1171
|
+
#
|
|
1172
|
+
# @param operation_name [String] the operation name
|
|
1173
|
+
# @return [Google::Apis::ComputeV1::Operation] the operation
|
|
740
1174
|
def zone_operation(operation_name)
|
|
741
1175
|
connection.get_zone_operation(project, zone, operation_name)
|
|
742
1176
|
end
|
|
743
1177
|
|
|
1178
|
+
# The errors a zone operation reported, if any.
|
|
1179
|
+
#
|
|
1180
|
+
# @param operation_name [String] the operation name
|
|
1181
|
+
# @return [Array<Google::Apis::ComputeV1::Operation::Error::Error>] the errors
|
|
744
1182
|
def operation_errors(operation_name)
|
|
745
1183
|
operation = zone_operation(operation_name)
|
|
746
1184
|
return [] if operation.error.nil?
|
|
@@ -18,8 +18,16 @@
|
|
|
18
18
|
# limitations under the License.
|
|
19
19
|
#
|
|
20
20
|
|
|
21
|
+
# Test Kitchen's top-level namespace.
|
|
21
22
|
module Kitchen
|
|
23
|
+
# Namespace for Test Kitchen driver plugins.
|
|
22
24
|
module Driver
|
|
23
|
-
|
|
25
|
+
# Version of the kitchen-google gem.
|
|
26
|
+
#
|
|
27
|
+
# Maintained by release-please, which rewrites the literal below on each
|
|
28
|
+
# release. Keep it on one line.
|
|
29
|
+
#
|
|
30
|
+
# @return [String] the gem version
|
|
31
|
+
GCE_VERSION = "2.8.0"
|
|
24
32
|
end
|
|
25
33
|
end
|
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: 2.
|
|
4
|
+
version: 2.8.0
|
|
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-08-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: gcewinpass
|