kitchen-google 2.6.2 → 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 +523 -82
- 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",
|
|
@@ -80,16 +97,60 @@ module Kitchen
|
|
|
80
97
|
default_config :use_private_ip, false
|
|
81
98
|
default_config :wait_time, 600
|
|
82
99
|
default_config :refresh_rate, 2
|
|
100
|
+
default_config :winpass_timeout, nil
|
|
83
101
|
default_config :guest_accelerators, []
|
|
84
102
|
default_config :metadata, {}
|
|
85
103
|
default_config :labels, {}
|
|
86
104
|
|
|
105
|
+
# Pattern a GCE disk name must match in full.
|
|
106
|
+
#
|
|
107
|
+
# @return [Regexp] the permitted disk-name pattern
|
|
87
108
|
DISK_NAME_REGEX = /(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)/
|
|
88
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
|
|
89
138
|
def name
|
|
90
139
|
"Google Compute (GCE)"
|
|
91
140
|
end
|
|
92
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
|
|
93
154
|
def create(state)
|
|
94
155
|
@state = state
|
|
95
156
|
return if state[:server_name]
|
|
@@ -120,10 +181,24 @@ module Kitchen
|
|
|
120
181
|
info("GCE instance <#{server_name}> created and ready.")
|
|
121
182
|
rescue => e
|
|
122
183
|
error("Error encountered during server creation: #{e.class}: #{e.message}")
|
|
123
|
-
|
|
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
|
|
124
191
|
raise
|
|
125
192
|
end
|
|
126
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]
|
|
127
202
|
def destroy(state)
|
|
128
203
|
@state = state
|
|
129
204
|
server_name = state[:server_name]
|
|
@@ -143,78 +218,162 @@ module Kitchen
|
|
|
143
218
|
state.delete(:zone)
|
|
144
219
|
end
|
|
145
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
|
|
146
225
|
def old_disk_configuration_present?
|
|
147
226
|
!config[:autodelete_disk].nil? || !config[:disk_size].nil? || !config[:disk_type].nil?
|
|
148
227
|
end
|
|
149
228
|
|
|
229
|
+
# Whether the multi-disk `disks` option is configured.
|
|
230
|
+
#
|
|
231
|
+
# @return [Boolean] true if `disks` is set
|
|
150
232
|
def new_disk_configuration_present?
|
|
151
233
|
!config[:disks].nil?
|
|
152
234
|
end
|
|
153
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
|
|
154
248
|
def create_disks_config
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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]),
|
|
161
273
|
}
|
|
162
274
|
|
|
163
|
-
|
|
164
|
-
# If the old disk configuration is used,
|
|
165
|
-
# we'll convert it to the new one
|
|
166
|
-
config[:disks] = {
|
|
167
|
-
disk1: {
|
|
168
|
-
boot: true,
|
|
169
|
-
autodelete_disk: config.fetch(:autodelete_disk, disk_default_config[:autodelete_disk]),
|
|
170
|
-
disk_size: config.fetch(:disk_size, disk_default_config[:disk_size]),
|
|
171
|
-
disk_type: config.fetch(:disk_type, disk_default_config[:disk_type]),
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
raise "Disk type #{config[:disks][:disk1][:disk_type]} is not valid" unless valid_disk_type?(config[:disks][:disk1][:disk_type])
|
|
175
|
-
elsif new_disk_configuration_present?
|
|
176
|
-
# If the new disk configuration is present, ensure that for
|
|
177
|
-
# every disk the needed configuration is set
|
|
178
|
-
boot_disk_counter = 0
|
|
179
|
-
config[:disks].each do |disk_name, disk_config|
|
|
180
|
-
# te&/ => te
|
|
181
|
-
raise "Disk name invalid. Must match #{DISK_NAME_REGEX}." unless valid_disk_name?(disk_name)
|
|
182
|
-
|
|
183
|
-
# Update the config for the disk with the fixed config
|
|
184
|
-
config[:disks][disk_name.to_sym] = disk_default_config.merge(disk_config)
|
|
185
|
-
|
|
186
|
-
# Since the config was altered, we can't use disk_config (as it will be different or keys will not be present)
|
|
187
|
-
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])
|
|
188
|
-
|
|
189
|
-
unless disk_config[:boot].nil?
|
|
190
|
-
boot_disk_counter += 1
|
|
191
|
-
raise "Boot disk cannot be local SSD." if disk_config[:disk_type] == "local-ssd"
|
|
192
|
-
end
|
|
275
|
+
raise "Disk type #{disk_config[:disk_type]} is not valid" unless valid_disk_type?(disk_config[:disk_type])
|
|
193
276
|
|
|
194
|
-
|
|
195
|
-
|
|
277
|
+
disk_config
|
|
278
|
+
end
|
|
196
279
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
assign_boot_disk(normalized)
|
|
299
|
+
end
|
|
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)."
|
|
215
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)
|
|
216
327
|
end
|
|
217
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
|
|
218
377
|
def validate!
|
|
219
378
|
raise "Project #{config[:project]} is not a valid project" unless valid_project?
|
|
220
379
|
raise "Either zone or region must be specified" unless config[:zone] || config[:region]
|
|
@@ -238,6 +397,9 @@ module Kitchen
|
|
|
238
397
|
warn("These configs are deprecated - consider using new disks configuration") if old_disk_configuration_present?
|
|
239
398
|
end
|
|
240
399
|
|
|
400
|
+
# Memoised, authorised Compute Engine API client.
|
|
401
|
+
#
|
|
402
|
+
# @return [Google::Apis::ComputeV1::ComputeService] the API client
|
|
241
403
|
def connection
|
|
242
404
|
return @connection unless @connection.nil?
|
|
243
405
|
|
|
@@ -251,6 +413,9 @@ module Kitchen
|
|
|
251
413
|
@connection
|
|
252
414
|
end
|
|
253
415
|
|
|
416
|
+
# Application default credentials scoped for Compute Engine.
|
|
417
|
+
#
|
|
418
|
+
# @return [Google::Auth::Credentials] the resolved credentials
|
|
254
419
|
def authorization
|
|
255
420
|
@authorization ||= Google::Auth.get_application_default(
|
|
256
421
|
[
|
|
@@ -260,10 +425,18 @@ module Kitchen
|
|
|
260
425
|
)
|
|
261
426
|
end
|
|
262
427
|
|
|
428
|
+
# Whether the suite's transport is WinRM, implying a Windows guest.
|
|
429
|
+
#
|
|
430
|
+
# @return [Boolean] true when the transport is WinRM
|
|
263
431
|
def winrm_transport?
|
|
264
432
|
instance.transport.name.casecmp("winrm") == 0
|
|
265
433
|
end
|
|
266
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]
|
|
267
440
|
def update_windows_password(server_name)
|
|
268
441
|
return unless winrm_transport?
|
|
269
442
|
|
|
@@ -271,17 +444,24 @@ module Kitchen
|
|
|
271
444
|
|
|
272
445
|
info("Resetting the Windows password for user #{username} on #{server_name}...")
|
|
273
446
|
|
|
274
|
-
|
|
275
|
-
project
|
|
276
|
-
zone
|
|
447
|
+
opts = {
|
|
448
|
+
project: project,
|
|
449
|
+
zone: zone,
|
|
277
450
|
instance_name: server_name,
|
|
278
|
-
email:
|
|
279
|
-
username:
|
|
280
|
-
|
|
451
|
+
email: config[:email],
|
|
452
|
+
username: username,
|
|
453
|
+
}
|
|
454
|
+
opts[:timeout] = config[:winpass_timeout] unless config[:winpass_timeout].nil?
|
|
455
|
+
state[:password] = GoogleComputeWindowsPassword.new(**opts).new_password
|
|
281
456
|
|
|
282
|
-
info("Password reset complete on #{server_name}
|
|
457
|
+
info("Password reset complete on #{server_name}.")
|
|
283
458
|
end
|
|
284
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
|
|
285
465
|
def check_api_call(&block)
|
|
286
466
|
yield
|
|
287
467
|
rescue Google::Apis::ClientError => e
|
|
@@ -291,94 +471,161 @@ module Kitchen
|
|
|
291
471
|
true
|
|
292
472
|
end
|
|
293
473
|
|
|
474
|
+
# Whether the configured project exists and is reachable.
|
|
475
|
+
#
|
|
476
|
+
# @return [Boolean] true if the project is valid
|
|
294
477
|
def valid_project?
|
|
295
478
|
check_api_call { connection.get_project(project) }
|
|
296
479
|
end
|
|
297
480
|
|
|
481
|
+
# Whether the configured machine type exists in the target zone.
|
|
482
|
+
#
|
|
483
|
+
# @return [Boolean] true if the machine type is valid
|
|
298
484
|
def valid_machine_type?
|
|
299
485
|
return false if config[:machine_type].nil?
|
|
300
486
|
|
|
301
487
|
check_api_call { connection.get_machine_type(project, zone, config[:machine_type]) }
|
|
302
488
|
end
|
|
303
489
|
|
|
490
|
+
# Whether the configured network exists in the network project.
|
|
491
|
+
#
|
|
492
|
+
# @return [Boolean] true if the network is valid
|
|
304
493
|
def valid_network?
|
|
305
494
|
return false if config[:network].nil?
|
|
306
495
|
|
|
307
496
|
check_api_call { connection.get_network(network_project, config[:network]) }
|
|
308
497
|
end
|
|
309
498
|
|
|
499
|
+
# Whether the configured subnet exists in the subnet project and region.
|
|
500
|
+
#
|
|
501
|
+
# @return [Boolean] true if the subnet is valid
|
|
310
502
|
def valid_subnet?
|
|
311
503
|
return false if config[:subnet].nil?
|
|
312
504
|
|
|
313
505
|
check_api_call { connection.get_subnetwork(subnet_project, region, config[:subnet]) }
|
|
314
506
|
end
|
|
315
507
|
|
|
508
|
+
# Whether the configured zone exists in the project.
|
|
509
|
+
#
|
|
510
|
+
# @return [Boolean] true if the zone is valid
|
|
316
511
|
def valid_zone?
|
|
317
512
|
return false if config[:zone].nil?
|
|
318
513
|
|
|
319
514
|
check_api_call { connection.get_zone(project, config[:zone]) }
|
|
320
515
|
end
|
|
321
516
|
|
|
517
|
+
# Whether the configured region exists in the project.
|
|
518
|
+
#
|
|
519
|
+
# @return [Boolean] true if the region is valid
|
|
322
520
|
def valid_region?
|
|
323
521
|
return false if config[:region].nil?
|
|
324
522
|
|
|
325
523
|
check_api_call { connection.get_region(project, config[:region]) }
|
|
326
524
|
end
|
|
327
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
|
|
328
530
|
def valid_disk_type?(disk_type)
|
|
329
531
|
return false if disk_type.nil?
|
|
330
532
|
|
|
331
533
|
check_api_call { connection.get_disk_type(project, zone, disk_type) }
|
|
332
534
|
end
|
|
333
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
|
|
334
540
|
def valid_disk_name?(disk_name)
|
|
335
|
-
disk_name.to_s.match(DISK_NAME_REGEX)
|
|
541
|
+
disk_name.to_s.match?(/\A#{DISK_NAME_REGEX}\z/)
|
|
336
542
|
end
|
|
337
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
|
|
338
548
|
def image_exist?(image = image_name)
|
|
339
549
|
check_api_call { connection.get_image(image_project, image) }
|
|
340
550
|
end
|
|
341
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
|
|
342
556
|
def server_exist?(server_name)
|
|
343
557
|
check_api_call { server_instance(server_name) }
|
|
344
558
|
end
|
|
345
559
|
|
|
560
|
+
# The configured GCP project.
|
|
561
|
+
#
|
|
562
|
+
# @return [String] the project ID
|
|
346
563
|
def project
|
|
347
564
|
config[:project]
|
|
348
565
|
end
|
|
349
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
|
|
350
571
|
def image_name
|
|
351
572
|
@image_name ||= config[:image_name] || image_name_for_family(config[:image_family])
|
|
352
573
|
end
|
|
353
574
|
|
|
575
|
+
# Project searched for images, defaulting to the instance's own project.
|
|
576
|
+
#
|
|
577
|
+
# @return [String] the image project ID
|
|
354
578
|
def image_project
|
|
355
579
|
config[:image_project].nil? ? project : config[:image_project]
|
|
356
580
|
end
|
|
357
581
|
|
|
582
|
+
# Project searched for subnets, defaulting to the instance's own project.
|
|
583
|
+
#
|
|
584
|
+
# @return [String] the subnet project ID
|
|
358
585
|
def subnet_project
|
|
359
586
|
config[:subnet_project].nil? ? project : config[:subnet_project]
|
|
360
587
|
end
|
|
361
588
|
|
|
589
|
+
# Project searched for networks, defaulting to the instance's own project.
|
|
590
|
+
#
|
|
591
|
+
# @return [String] the network project ID
|
|
362
592
|
def network_project
|
|
363
593
|
config[:network_project].nil? ? project : config[:network_project]
|
|
364
594
|
end
|
|
365
595
|
|
|
596
|
+
# The static internal IP to assign, if one was configured.
|
|
597
|
+
#
|
|
598
|
+
# @return [String, nil] the internal IP address
|
|
366
599
|
def network_ip
|
|
367
600
|
config[:network_ip]
|
|
368
601
|
end
|
|
369
602
|
|
|
603
|
+
# The target region, derived from the zone when not configured directly.
|
|
604
|
+
#
|
|
605
|
+
# @return [String] the region name
|
|
370
606
|
def region
|
|
371
607
|
config[:region].nil? ? region_for_zone : config[:region]
|
|
372
608
|
end
|
|
373
609
|
|
|
610
|
+
# Looks up which region the target zone belongs to.
|
|
611
|
+
#
|
|
612
|
+
# @return [String] the region name
|
|
374
613
|
def region_for_zone
|
|
375
614
|
@region_for_zone ||= connection.get_zone(project, zone).region.split("/").last
|
|
376
615
|
end
|
|
377
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
|
|
378
621
|
def zone
|
|
379
622
|
@zone ||= state[:zone] || config[:zone] || find_zone
|
|
380
623
|
end
|
|
381
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
|
|
382
629
|
def find_zone
|
|
383
630
|
zone = zones_in_region.sample
|
|
384
631
|
raise "Unable to find a suitable zone in #{region}" if zone.nil?
|
|
@@ -386,6 +633,9 @@ module Kitchen
|
|
|
386
633
|
zone.name
|
|
387
634
|
end
|
|
388
635
|
|
|
636
|
+
# All zones in the configured region whose status is `UP`.
|
|
637
|
+
#
|
|
638
|
+
# @return [Array<Google::Apis::ComputeV1::Zone>] the available zones
|
|
389
639
|
def zones_in_region
|
|
390
640
|
connection.list_zones(project).items.select do |zone|
|
|
391
641
|
zone.status == "UP" &&
|
|
@@ -393,26 +643,49 @@ module Kitchen
|
|
|
393
643
|
end
|
|
394
644
|
end
|
|
395
645
|
|
|
646
|
+
# Fetches a GCE instance.
|
|
647
|
+
#
|
|
648
|
+
# @param server_name [String] the instance name
|
|
649
|
+
# @return [Google::Apis::ComputeV1::Instance] the instance
|
|
396
650
|
def server_instance(server_name)
|
|
397
651
|
connection.get_instance(project, zone, server_name)
|
|
398
652
|
end
|
|
399
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
|
|
400
659
|
def ip_address_for(server)
|
|
401
660
|
config[:use_private_ip] ? private_ip_for(server) : public_ip_for(server)
|
|
402
661
|
end
|
|
403
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
|
|
404
668
|
def private_ip_for(server)
|
|
405
669
|
server.network_interfaces.first.network_ip
|
|
406
670
|
rescue NoMethodError
|
|
407
671
|
raise "Unable to determine private IP for instance"
|
|
408
672
|
end
|
|
409
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
|
|
410
679
|
def public_ip_for(server)
|
|
411
680
|
server.network_interfaces.first.access_configs.first.nat_ip
|
|
412
681
|
rescue NoMethodError
|
|
413
682
|
raise "Unable to determine public IP for instance"
|
|
414
683
|
end
|
|
415
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
|
|
416
689
|
def create_instance_object(server_name)
|
|
417
690
|
inst_obj = Google::Apis::ComputeV1::Instance.new
|
|
418
691
|
inst_obj.name = server_name
|
|
@@ -429,10 +702,14 @@ module Kitchen
|
|
|
429
702
|
inst_obj
|
|
430
703
|
end
|
|
431
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
|
|
432
709
|
def generate_server_name
|
|
433
710
|
name = config[:inst_name] || "tk-#{instance.name.downcase}-#{SecureRandom.hex(3)}"
|
|
434
711
|
|
|
435
|
-
if name.length >
|
|
712
|
+
if name.length > MAX_INSTANCE_NAME_LENGTH
|
|
436
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.")
|
|
437
714
|
name = "tk-#{SecureRandom.uuid}"
|
|
438
715
|
end
|
|
@@ -440,6 +717,11 @@ module Kitchen
|
|
|
440
717
|
name.gsub(/([^-a-z0-9])/, "-")
|
|
441
718
|
end
|
|
442
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
|
|
443
725
|
def create_disks(server_name)
|
|
444
726
|
disks = []
|
|
445
727
|
config[:disks].each do |disk_name, disk_config|
|
|
@@ -447,7 +729,7 @@ module Kitchen
|
|
|
447
729
|
if disk_config[:boot]
|
|
448
730
|
disk = create_local_disk(unique_disk_name, disk_config)
|
|
449
731
|
disks.unshift(disk)
|
|
450
|
-
elsif (disk_config
|
|
732
|
+
elsif local_ssd?(disk_config) || disk_config[:custom_image]
|
|
451
733
|
disk = create_local_disk(unique_disk_name, disk_config)
|
|
452
734
|
disks.push(disk)
|
|
453
735
|
else
|
|
@@ -458,32 +740,43 @@ module Kitchen
|
|
|
458
740
|
disks
|
|
459
741
|
end
|
|
460
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
|
|
461
749
|
def create_local_disk(unique_disk_name, disk_config)
|
|
462
750
|
disk = Google::Apis::ComputeV1::AttachedDisk.new
|
|
463
751
|
# Specifies the parameters for a new disk that will be created alongside the new instance.
|
|
464
752
|
params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new
|
|
465
|
-
disk.boot = true if
|
|
753
|
+
disk.boot = true if disk_config[:boot]
|
|
466
754
|
disk.auto_delete = disk_config[:autodelete_disk]
|
|
467
755
|
params.disk_size_gb = disk_config[:disk_size]
|
|
468
756
|
params.disk_type = disk_type_url_for(disk_config[:disk_type])
|
|
469
757
|
|
|
470
|
-
if disk_config
|
|
471
|
-
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).")
|
|
472
760
|
disk.type = "SCRATCH"
|
|
473
761
|
elsif disk.boot
|
|
474
762
|
info("Creating a #{disk_config[:disk_size]} GB boot disk named #{unique_disk_name} from image #{image_name}...")
|
|
475
|
-
params.source_image = boot_disk_source_image
|
|
476
|
-
params.disk_name = unique_disk_name
|
|
763
|
+
params.source_image = boot_disk_source_image
|
|
764
|
+
params.disk_name = unique_disk_name
|
|
477
765
|
else
|
|
478
766
|
info("Creating a #{disk_config[:disk_size]} GB extra disk named #{unique_disk_name} from image #{disk_config[:custom_image]}...")
|
|
479
|
-
params.source_image = image_url(disk_config[:custom_image])
|
|
480
|
-
params.disk_name = unique_disk_name
|
|
481
|
-
|
|
767
|
+
params.source_image = image_url(disk_config[:custom_image])
|
|
768
|
+
params.disk_name = unique_disk_name
|
|
482
769
|
end
|
|
483
770
|
disk.initialize_params = params
|
|
484
771
|
disk
|
|
485
772
|
end
|
|
486
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
|
|
487
780
|
def create_attached_disk(unique_disk_name, disk_config)
|
|
488
781
|
disk = Google::Apis::ComputeV1::Disk.new
|
|
489
782
|
disk.name = unique_disk_name
|
|
@@ -492,6 +785,7 @@ module Kitchen
|
|
|
492
785
|
|
|
493
786
|
info("Creating a #{disk_config[:disk_size]} GB disk named #{unique_disk_name}...")
|
|
494
787
|
wait_for_operation(connection.insert_disk(project, zone, disk))
|
|
788
|
+
created_disk_names << unique_disk_name
|
|
495
789
|
info("Waiting for disk to be ready...")
|
|
496
790
|
wait_for_status("READY") { connection.get_disk(project, zone, unique_disk_name) }
|
|
497
791
|
info("Disk created successfully.")
|
|
@@ -501,6 +795,28 @@ module Kitchen
|
|
|
501
795
|
attached_disk
|
|
502
796
|
end
|
|
503
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]
|
|
504
820
|
def delete_disk(unique_disk_name)
|
|
505
821
|
begin
|
|
506
822
|
connection.get_disk(project, zone, unique_disk_name)
|
|
@@ -514,35 +830,64 @@ module Kitchen
|
|
|
514
830
|
info("Disk #{unique_disk_name} deleted successfully.")
|
|
515
831
|
end
|
|
516
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
|
|
517
837
|
def disk_type_url_for(type)
|
|
518
838
|
"zones/#{zone}/diskTypes/#{type}"
|
|
519
839
|
end
|
|
520
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
|
|
521
845
|
def disk_self_link(unique_disk_name)
|
|
522
846
|
"projects/#{project}/zones/#{zone}/disks/#{unique_disk_name}"
|
|
523
847
|
end
|
|
524
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
|
|
525
852
|
def boot_disk_source_image
|
|
526
853
|
@boot_disk_source ||= image_url
|
|
527
854
|
end
|
|
528
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
|
|
529
860
|
def image_url(image = image_name)
|
|
530
|
-
|
|
861
|
+
"projects/#{image_project}/global/images/#{image}" if image_exist?(image)
|
|
531
862
|
end
|
|
532
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
|
|
533
868
|
def image_name_for_family(image_family)
|
|
534
869
|
image = connection.get_image_from_family(image_project, image_family)
|
|
535
870
|
image.name
|
|
536
871
|
end
|
|
537
872
|
|
|
873
|
+
# Partial URL identifying the machine type in the target zone.
|
|
874
|
+
#
|
|
875
|
+
# @return [String] the machine type URL
|
|
538
876
|
def machine_type_url
|
|
539
877
|
"zones/#{zone}/machineTypes/#{config[:machine_type]}"
|
|
540
878
|
end
|
|
541
879
|
|
|
880
|
+
# The configured guest accelerators.
|
|
881
|
+
#
|
|
882
|
+
# @return [Array<Hash>] the accelerator configurations
|
|
542
883
|
def guest_accelerators
|
|
543
884
|
config[:guest_accelerators]
|
|
544
885
|
end
|
|
545
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
|
|
546
891
|
def instance_guest_accelerators
|
|
547
892
|
guest_accelerator_configs = []
|
|
548
893
|
|
|
@@ -564,6 +909,10 @@ module Kitchen
|
|
|
564
909
|
guest_accelerator_configs
|
|
565
910
|
end
|
|
566
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
|
|
567
916
|
def metadata
|
|
568
917
|
default_metadata = {
|
|
569
918
|
"created-by" => "test-kitchen",
|
|
@@ -581,6 +930,9 @@ module Kitchen
|
|
|
581
930
|
config[:metadata].merge(default_metadata)
|
|
582
931
|
end
|
|
583
932
|
|
|
933
|
+
# The metadata in the form the GCE API expects.
|
|
934
|
+
#
|
|
935
|
+
# @return [Google::Apis::ComputeV1::Metadata] the metadata object
|
|
584
936
|
def instance_metadata
|
|
585
937
|
Google::Apis::ComputeV1::Metadata.new.tap do |metadata_obj|
|
|
586
938
|
metadata_obj.items = metadata.each_with_object([]) do |(k, v), memo|
|
|
@@ -592,14 +944,23 @@ module Kitchen
|
|
|
592
944
|
end
|
|
593
945
|
end
|
|
594
946
|
|
|
947
|
+
# The configured instance labels.
|
|
948
|
+
#
|
|
949
|
+
# @return [Hash] the labels
|
|
595
950
|
def instance_labels
|
|
596
951
|
config[:labels]
|
|
597
952
|
end
|
|
598
953
|
|
|
954
|
+
# The username recorded in instance metadata.
|
|
955
|
+
#
|
|
956
|
+
# @return [String] the current user, or `"unknown"`
|
|
599
957
|
def env_user
|
|
600
958
|
ENV["USER"] || "unknown"
|
|
601
959
|
end
|
|
602
960
|
|
|
961
|
+
# Builds the instance's single network interface.
|
|
962
|
+
#
|
|
963
|
+
# @return [Array<Google::Apis::ComputeV1::NetworkInterface>] the interface
|
|
603
964
|
def instance_network_interfaces
|
|
604
965
|
interface = Google::Apis::ComputeV1::NetworkInterface.new
|
|
605
966
|
interface.network = network_url if config[:subnet_project].nil?
|
|
@@ -610,16 +971,26 @@ module Kitchen
|
|
|
610
971
|
Array(interface)
|
|
611
972
|
end
|
|
612
973
|
|
|
974
|
+
# Partial URL identifying the configured network.
|
|
975
|
+
#
|
|
976
|
+
# @return [String] the network URL
|
|
613
977
|
def network_url
|
|
614
978
|
"projects/#{network_project}/global/networks/#{config[:network]}"
|
|
615
979
|
end
|
|
616
980
|
|
|
981
|
+
# Partial URL identifying the configured subnet.
|
|
982
|
+
#
|
|
983
|
+
# @return [String, nil] the subnet URL, or nil when no subnet is set
|
|
617
984
|
def subnet_url
|
|
618
985
|
return unless config[:subnet]
|
|
619
986
|
|
|
620
987
|
"projects/#{subnet_project}/regions/#{region}/subnetworks/#{config[:subnet]}"
|
|
621
988
|
end
|
|
622
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
|
|
623
994
|
def interface_access_configs
|
|
624
995
|
return [] if config[:use_private_ip]
|
|
625
996
|
|
|
@@ -630,30 +1001,55 @@ module Kitchen
|
|
|
630
1001
|
Array(access_config)
|
|
631
1002
|
end
|
|
632
1003
|
|
|
1004
|
+
# The instance's scheduling options.
|
|
1005
|
+
#
|
|
1006
|
+
# @return [Google::Apis::ComputeV1::Scheduling] the scheduling options
|
|
633
1007
|
def instance_scheduling
|
|
634
1008
|
Google::Apis::ComputeV1::Scheduling.new.tap do |scheduling|
|
|
635
|
-
scheduling.automatic_restart = auto_restart
|
|
636
|
-
scheduling.preemptible = preemptible
|
|
1009
|
+
scheduling.automatic_restart = auto_restart?
|
|
1010
|
+
scheduling.preemptible = preemptible?
|
|
637
1011
|
scheduling.on_host_maintenance = migrate_setting
|
|
638
1012
|
end
|
|
639
1013
|
end
|
|
640
1014
|
|
|
1015
|
+
# Whether the instance should be preemptible.
|
|
1016
|
+
#
|
|
1017
|
+
# @return [Boolean] true if preemptible
|
|
641
1018
|
def preemptible?
|
|
642
|
-
config[:preemptible]
|
|
1019
|
+
config[:preemptible] ? true : false
|
|
643
1020
|
end
|
|
644
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
|
|
645
1026
|
def auto_migrate?
|
|
646
|
-
|
|
1027
|
+
return false if preemptible?
|
|
1028
|
+
|
|
1029
|
+
config[:auto_migrate] ? true : false
|
|
647
1030
|
end
|
|
648
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
|
|
649
1036
|
def auto_restart?
|
|
650
|
-
|
|
1037
|
+
return false if preemptible?
|
|
1038
|
+
|
|
1039
|
+
config[:auto_restart] ? true : false
|
|
651
1040
|
end
|
|
652
1041
|
|
|
1042
|
+
# The host maintenance behaviour implied by {#auto_migrate?}.
|
|
1043
|
+
#
|
|
1044
|
+
# @return [String] `"MIGRATE"` or `"TERMINATE"`
|
|
653
1045
|
def migrate_setting
|
|
654
1046
|
auto_migrate? ? "MIGRATE" : "TERMINATE"
|
|
655
1047
|
end
|
|
656
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
|
|
657
1053
|
def instance_service_accounts
|
|
658
1054
|
return if config[:service_account_scopes].nil? || config[:service_account_scopes].empty?
|
|
659
1055
|
|
|
@@ -664,28 +1060,54 @@ module Kitchen
|
|
|
664
1060
|
Array(service_account)
|
|
665
1061
|
end
|
|
666
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
|
|
667
1068
|
def service_account_scope_url(scope)
|
|
668
1069
|
return scope if scope.start_with?("https://www.googleapis.com/auth/")
|
|
669
1070
|
|
|
670
1071
|
"https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
|
|
671
1072
|
end
|
|
672
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
|
|
673
1079
|
def translate_scope_alias(scope_alias)
|
|
674
1080
|
SCOPE_ALIAS_MAP.fetch(scope_alias, scope_alias)
|
|
675
1081
|
end
|
|
676
1082
|
|
|
1083
|
+
# The configured network tags in the form the GCE API expects.
|
|
1084
|
+
#
|
|
1085
|
+
# @return [Google::Apis::ComputeV1::Tags] the tags object
|
|
677
1086
|
def instance_tags
|
|
678
1087
|
Google::Apis::ComputeV1::Tags.new.tap { |tag_obj| tag_obj.items = config[:tags] }
|
|
679
1088
|
end
|
|
680
1089
|
|
|
1090
|
+
# How long, in seconds, to wait for an operation or status change.
|
|
1091
|
+
#
|
|
1092
|
+
# @return [Integer] the wait timeout
|
|
681
1093
|
def wait_time
|
|
682
1094
|
config[:wait_time]
|
|
683
1095
|
end
|
|
684
1096
|
|
|
1097
|
+
# How long, in seconds, to sleep between status polls.
|
|
1098
|
+
#
|
|
1099
|
+
# @return [Integer] the poll interval
|
|
685
1100
|
def refresh_rate
|
|
686
1101
|
config[:refresh_rate]
|
|
687
1102
|
end
|
|
688
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}
|
|
689
1111
|
def wait_for_status(requested_status, &block)
|
|
690
1112
|
last_status = ""
|
|
691
1113
|
|
|
@@ -711,6 +1133,12 @@ module Kitchen
|
|
|
711
1133
|
end
|
|
712
1134
|
end
|
|
713
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
|
|
714
1142
|
def wait_for_operation(operation)
|
|
715
1143
|
operation_name = operation.name
|
|
716
1144
|
|
|
@@ -726,6 +1154,11 @@ module Kitchen
|
|
|
726
1154
|
raise "Operation #{operation_name} failed."
|
|
727
1155
|
end
|
|
728
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
|
|
729
1162
|
def wait_for_server
|
|
730
1163
|
instance.transport.connection(state).wait_until_ready
|
|
731
1164
|
rescue
|
|
@@ -734,10 +1167,18 @@ module Kitchen
|
|
|
734
1167
|
raise
|
|
735
1168
|
end
|
|
736
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
|
|
737
1174
|
def zone_operation(operation_name)
|
|
738
1175
|
connection.get_zone_operation(project, zone, operation_name)
|
|
739
1176
|
end
|
|
740
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
|
|
741
1182
|
def operation_errors(operation_name)
|
|
742
1183
|
operation = zone_operation(operation_name)
|
|
743
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
|