kitchen-dokken 2.23.3 → 2.23.4
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/Gemfile +10 -1
- data/Rakefile +18 -0
- data/lib/kitchen/driver/dokken.rb +309 -23
- data/lib/kitchen/driver/dokken_version.rb +1 -1
- data/lib/kitchen/helpers.rb +260 -30
- data/lib/kitchen/provisioner/dokken.rb +53 -4
- data/lib/kitchen/transport/dokken.rb +294 -73
- metadata +2 -2
|
@@ -20,6 +20,7 @@ require "json" unless defined?(JSON)
|
|
|
20
20
|
require "kitchen"
|
|
21
21
|
require "tmpdir" unless defined?(Dir.mktmpdir)
|
|
22
22
|
require "docker"
|
|
23
|
+
require "shellwords" unless defined?(Shellwords)
|
|
23
24
|
require "base64" unless defined?(Base64)
|
|
24
25
|
require_relative "../helpers"
|
|
25
26
|
|
|
@@ -29,9 +30,16 @@ include Dokken::Helpers
|
|
|
29
30
|
Excon.defaults[:ssl_verify_peer] = false
|
|
30
31
|
|
|
31
32
|
module Kitchen
|
|
33
|
+
# @see Kitchen::Driver::Dokken
|
|
32
34
|
module Driver
|
|
33
35
|
# Dokken driver for Kitchen.
|
|
34
36
|
#
|
|
37
|
+
# Creates three containers per instance: a *runner*, which the converge
|
|
38
|
+
# actually happens in; a *chef* volume container, whose /opt/chef is
|
|
39
|
+
# mounted into the runner instead of installing chef; and -- only when the
|
|
40
|
+
# docker daemon cannot read the local filesystem -- a *data* container that
|
|
41
|
+
# serves the kitchen sandbox over ssh.
|
|
42
|
+
#
|
|
35
43
|
# @author Sean OMeara <sean@sean.io>
|
|
36
44
|
class Dokken < Kitchen::Driver::Base
|
|
37
45
|
default_config :api_retries, 20
|
|
@@ -84,6 +92,9 @@ module Kitchen
|
|
|
84
92
|
default_config :docker_config_creds, true
|
|
85
93
|
|
|
86
94
|
# (see Base#create)
|
|
95
|
+
#
|
|
96
|
+
# @param state [Hash] mutable instance state
|
|
97
|
+
# @return [void]
|
|
87
98
|
def create(state)
|
|
88
99
|
# Authenticate the private registry
|
|
89
100
|
authenticate!
|
|
@@ -116,6 +127,10 @@ module Kitchen
|
|
|
116
127
|
save_misc_state state
|
|
117
128
|
end
|
|
118
129
|
|
|
130
|
+
# (see Base#destroy)
|
|
131
|
+
#
|
|
132
|
+
# The chef volume container and the dokken network are deliberately left
|
|
133
|
+
# behind: both are shared by every instance using the same chef version.
|
|
119
134
|
def destroy(_state)
|
|
120
135
|
if remote_docker_host? || running_inside_docker?
|
|
121
136
|
stop_data_container
|
|
@@ -130,6 +145,10 @@ module Kitchen
|
|
|
130
145
|
|
|
131
146
|
private
|
|
132
147
|
|
|
148
|
+
# Attach DNS settings to a network endpoint configuration, in place.
|
|
149
|
+
#
|
|
150
|
+
# @param endpoint_config [Hash] the EndpointsConfig entry to extend
|
|
151
|
+
# @return [void]
|
|
133
152
|
def add_dns_config(endpoint_config)
|
|
134
153
|
return unless self[:dns] || self[:dns_search]
|
|
135
154
|
|
|
@@ -138,16 +157,30 @@ module Kitchen
|
|
|
138
157
|
endpoint_config["DNSConfig"]["Search"] = self[:dns_search] if self[:dns_search]
|
|
139
158
|
end
|
|
140
159
|
|
|
160
|
+
# A Hash that compares equal to any Hash containing all of its pairs.
|
|
161
|
+
#
|
|
162
|
+
# The daemon echoes back more volumes than we asked for, so a plain
|
|
163
|
+
# equality check against the requested set would never match.
|
|
141
164
|
class PartialHash < Hash
|
|
165
|
+
# Whether `other` contains every pair this hash holds.
|
|
166
|
+
#
|
|
167
|
+
# @param other [Object] the value to compare against
|
|
168
|
+
# @return [Boolean] true when `other` is a superset hash
|
|
142
169
|
def ==(other)
|
|
143
170
|
other.is_a?(Hash) && all? { |key, val| other.key?(key) && other[key] == val }
|
|
144
171
|
end
|
|
145
172
|
end
|
|
146
173
|
|
|
174
|
+
# How many times to retry a retryable docker API call.
|
|
175
|
+
#
|
|
176
|
+
# @return [Integer] the retry count
|
|
147
177
|
def api_retries
|
|
148
178
|
config[:api_retries]
|
|
149
179
|
end
|
|
150
180
|
|
|
181
|
+
# The docker-api connection this driver talks to.
|
|
182
|
+
#
|
|
183
|
+
# @return [::Docker::Connection] a memoised connection
|
|
151
184
|
def docker_connection
|
|
152
185
|
opts = ::Docker.options
|
|
153
186
|
opts[:read_timeout] = config[:read_timeout]
|
|
@@ -155,6 +188,9 @@ module Kitchen
|
|
|
155
188
|
@docker_connection ||= ::Docker::Connection.new(config[:docker_host_url], opts)
|
|
156
189
|
end
|
|
157
190
|
|
|
191
|
+
# Remove this instance's work image, if nothing else still needs it.
|
|
192
|
+
#
|
|
193
|
+
# @return [void]
|
|
158
194
|
def delete_work_image
|
|
159
195
|
return unless ::Docker::Image.exist?(work_image, { "platform" => oci_platform(config[:platform]) }, docker_connection)
|
|
160
196
|
|
|
@@ -167,6 +203,11 @@ module Kitchen
|
|
|
167
203
|
end
|
|
168
204
|
end
|
|
169
205
|
|
|
206
|
+
# Build the per-instance image the runner container is created from.
|
|
207
|
+
#
|
|
208
|
+
# @param state [Hash] mutable instance state
|
|
209
|
+
# @return [void]
|
|
210
|
+
# @raise [RuntimeError] if the daemon rejects the build
|
|
170
211
|
def build_work_image(state)
|
|
171
212
|
info("Building work image..")
|
|
172
213
|
return if ::Docker::Image.exist?(work_image, { "platform" => oci_platform(config[:platform]) }, docker_connection)
|
|
@@ -183,7 +224,7 @@ module Kitchen
|
|
|
183
224
|
# credit to https://github.com/someara/kitchen-dokken/issues/95#issue-224697526
|
|
184
225
|
rescue Docker::Error::UnexpectedResponseError => e
|
|
185
226
|
msg = "work_image build failed: "
|
|
186
|
-
msg +=
|
|
227
|
+
msg += build_error_detail(e)
|
|
187
228
|
msg += ". The common scenarios are incorrect intermediate "
|
|
188
229
|
msg += "instructions such as not including `-y` on an `apt-get` "
|
|
189
230
|
msg += "or similar. The other common scenario is a transient "
|
|
@@ -197,6 +238,24 @@ module Kitchen
|
|
|
197
238
|
state[:work_image] = work_image
|
|
198
239
|
end
|
|
199
240
|
|
|
241
|
+
# Pull the human-readable reason out of a failed build response.
|
|
242
|
+
#
|
|
243
|
+
# The daemon usually answers with a JSON document carrying an "error"
|
|
244
|
+
# key, but a proxy or a plain-text 500 does not -- and a JSON::ParserError
|
|
245
|
+
# raised from inside the rescue clause would bury the real failure.
|
|
246
|
+
#
|
|
247
|
+
# @param error [Exception] the error docker-api raised
|
|
248
|
+
# @return [String] the daemon's explanation, or the raw response
|
|
249
|
+
def build_error_detail(error)
|
|
250
|
+
last_line = error.to_s.split("\r\n").last.to_s
|
|
251
|
+
JSON.parse(last_line)["error"].to_s
|
|
252
|
+
rescue JSON::ParserError, TypeError
|
|
253
|
+
last_line
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The Dockerfile for the work image.
|
|
257
|
+
#
|
|
258
|
+
# @return [String] the Dockerfile contents
|
|
200
259
|
def work_image_dockerfile
|
|
201
260
|
from = registry_image_path(platform_image)
|
|
202
261
|
debug("driver - Building work image from #{from}")
|
|
@@ -210,6 +269,10 @@ module Kitchen
|
|
|
210
269
|
dockerfile_contents.join("\n")
|
|
211
270
|
end
|
|
212
271
|
|
|
272
|
+
# Record the values the transport, provisioner and verifier read later.
|
|
273
|
+
#
|
|
274
|
+
# @param state [Hash] mutable instance state
|
|
275
|
+
# @return [void]
|
|
213
276
|
def save_misc_state(state)
|
|
214
277
|
state[:platform_image] = platform_image
|
|
215
278
|
state[:instance_name] = instance_name
|
|
@@ -217,47 +280,81 @@ module Kitchen
|
|
|
217
280
|
state[:image_prefix] = image_prefix
|
|
218
281
|
end
|
|
219
282
|
|
|
283
|
+
# Delete the shared chef volume container.
|
|
284
|
+
#
|
|
285
|
+
# Not called during {#destroy}; it exists for callers that really do want
|
|
286
|
+
# to reclaim the shared container.
|
|
287
|
+
#
|
|
288
|
+
# @return [void]
|
|
220
289
|
def delete_chef_container
|
|
221
290
|
debug "driver - deleting container #{chef_container_name}"
|
|
222
291
|
delete_container chef_container_name
|
|
223
292
|
end
|
|
224
293
|
|
|
294
|
+
# Delete this instance's data container.
|
|
295
|
+
#
|
|
296
|
+
# @return [void]
|
|
225
297
|
def delete_data_container
|
|
226
298
|
debug "driver - deleting container #{data_container_name}"
|
|
227
299
|
delete_container data_container_name
|
|
228
300
|
end
|
|
229
301
|
|
|
302
|
+
# Delete this instance's runner container.
|
|
303
|
+
#
|
|
304
|
+
# @return [void]
|
|
230
305
|
def delete_runner_container
|
|
231
306
|
debug "driver - deleting container #{runner_container_name}"
|
|
232
307
|
delete_container runner_container_name
|
|
233
308
|
end
|
|
234
309
|
|
|
310
|
+
# The configured image name prefix, if any.
|
|
311
|
+
#
|
|
312
|
+
# @return [String, nil] the prefix
|
|
235
313
|
def image_prefix
|
|
236
314
|
config[:image_prefix]
|
|
237
315
|
end
|
|
238
316
|
|
|
317
|
+
# The kitchen platform name, e.g. `almalinux-9`.
|
|
318
|
+
#
|
|
319
|
+
# @return [String] the platform name
|
|
239
320
|
def instance_platform_name
|
|
240
321
|
instance.platform.name
|
|
241
322
|
end
|
|
242
323
|
|
|
324
|
+
# Stop this instance's runner container.
|
|
325
|
+
#
|
|
326
|
+
# @return [void]
|
|
243
327
|
def stop_runner_container
|
|
244
328
|
debug "driver - stopping container #{runner_container_name}"
|
|
245
329
|
stop_container runner_container_name
|
|
246
330
|
end
|
|
247
331
|
|
|
332
|
+
# Stop this instance's data container.
|
|
333
|
+
#
|
|
334
|
+
# @return [void]
|
|
248
335
|
def stop_data_container
|
|
249
336
|
debug "driver - stopping container #{data_container_name}"
|
|
250
337
|
stop_container data_container_name
|
|
251
338
|
end
|
|
252
339
|
|
|
340
|
+
# The name of the per-instance image the runner is created from.
|
|
341
|
+
#
|
|
342
|
+
# @return [String] a lowercase image name
|
|
253
343
|
def work_image
|
|
254
344
|
[image_prefix, instance_name].compact.join("/").downcase
|
|
255
345
|
end
|
|
256
346
|
|
|
347
|
+
# The `Tmpfs` value for the runner container.
|
|
348
|
+
#
|
|
349
|
+
# @return [Hash, nil] a Docker API Tmpfs hash
|
|
257
350
|
def dokken_tmpfs
|
|
258
351
|
coerce_tmpfs(config[:tmpfs])
|
|
259
352
|
end
|
|
260
353
|
|
|
354
|
+
# Normalise a `tmpfs:` setting into a Docker API Tmpfs hash.
|
|
355
|
+
#
|
|
356
|
+
# @param v [Hash, Array<String>, nil] the configured tmpfs mounts
|
|
357
|
+
# @return [Hash, nil] a Tmpfs hash, or nil to omit the key
|
|
261
358
|
def coerce_tmpfs(v)
|
|
262
359
|
case v
|
|
263
360
|
when Hash, nil
|
|
@@ -270,6 +367,9 @@ module Kitchen
|
|
|
270
367
|
end
|
|
271
368
|
end
|
|
272
369
|
|
|
370
|
+
# The containers whose volumes the runner mounts.
|
|
371
|
+
#
|
|
372
|
+
# @return [Array<String>] container names
|
|
273
373
|
def dokken_volumes_from
|
|
274
374
|
ret = []
|
|
275
375
|
ret << chef_container_name
|
|
@@ -277,6 +377,14 @@ module Kitchen
|
|
|
277
377
|
ret
|
|
278
378
|
end
|
|
279
379
|
|
|
380
|
+
# Split a `volumes:` setting into anonymous volumes and bind mounts.
|
|
381
|
+
#
|
|
382
|
+
# Entries containing a colon are bind mounts and are moved into `binds`;
|
|
383
|
+
# what is left becomes the `Volumes` hash. `binds` is mutated in place.
|
|
384
|
+
#
|
|
385
|
+
# @param v [Hash, Array<String>, nil] the configured volumes
|
|
386
|
+
# @param binds [Array] the bind list to append to
|
|
387
|
+
# @return [Hash, nil] a Volumes hash, or nil to omit the key
|
|
280
388
|
def coerce_volumes(v, binds)
|
|
281
389
|
case v
|
|
282
390
|
when PartialHash, nil
|
|
@@ -297,8 +405,17 @@ module Kitchen
|
|
|
297
405
|
end
|
|
298
406
|
end
|
|
299
407
|
|
|
408
|
+
# Work out the runner container's `Volumes` and `Binds` values.
|
|
409
|
+
#
|
|
410
|
+
# The sandboxes are only bind-mounted when the daemon can read the host
|
|
411
|
+
# filesystem; otherwise the transport ships them into the data container.
|
|
412
|
+
#
|
|
413
|
+
# @return [Array(Hash, Array<String>)] the volumes and binds
|
|
300
414
|
def calc_volumes_binds
|
|
301
|
-
|
|
415
|
+
# Array() on a Hash yields its pairs, which would destroy an explicit
|
|
416
|
+
# `volumes:` mapping before coerce_volumes ever sees it.
|
|
417
|
+
configured_volumes = config[:volumes]
|
|
418
|
+
volumes = configured_volumes.is_a?(Hash) ? configured_volumes : Array.new(Array(configured_volumes))
|
|
302
419
|
binds = Array.new(Array(config[:binds]))
|
|
303
420
|
|
|
304
421
|
# Binds is mutated in-place, volumes *may* be.
|
|
@@ -312,6 +429,10 @@ module Kitchen
|
|
|
312
429
|
[volumes, binds_ret.flatten]
|
|
313
430
|
end
|
|
314
431
|
|
|
432
|
+
# Create and start the container the converge actually runs in.
|
|
433
|
+
#
|
|
434
|
+
# @param state [Hash] mutable instance state
|
|
435
|
+
# @return [void]
|
|
315
436
|
def start_runner_container(state)
|
|
316
437
|
debug "driver - starting #{runner_container_name}"
|
|
317
438
|
|
|
@@ -375,6 +496,10 @@ module Kitchen
|
|
|
375
496
|
state[:runner_container] = runner_container.json
|
|
376
497
|
end
|
|
377
498
|
|
|
499
|
+
# Create and start the container that serves the sandboxes over ssh.
|
|
500
|
+
#
|
|
501
|
+
# @param state [Hash] mutable instance state
|
|
502
|
+
# @return [void]
|
|
378
503
|
def start_data_container(state)
|
|
379
504
|
debug "driver - creating #{data_container_name}"
|
|
380
505
|
config = {
|
|
@@ -408,6 +533,12 @@ module Kitchen
|
|
|
408
533
|
state[:data_container] = data_container.json
|
|
409
534
|
end
|
|
410
535
|
|
|
536
|
+
# Create the shared `dokken` network, unless it already exists.
|
|
537
|
+
#
|
|
538
|
+
# Several instances converge in parallel and all race to create the one
|
|
539
|
+
# shared network, so this is both file-locked and forgiving of a lost race.
|
|
540
|
+
#
|
|
541
|
+
# @return [void]
|
|
411
542
|
def make_dokken_network
|
|
412
543
|
return unless self[:network_mode] == "dokken"
|
|
413
544
|
|
|
@@ -416,17 +547,28 @@ module Kitchen
|
|
|
416
547
|
rescue ::Docker::Error::NotFoundError
|
|
417
548
|
begin
|
|
418
549
|
with_retries { ::Docker::Network.create("dokken", network_settings) }
|
|
419
|
-
rescue ::Docker::Error => e
|
|
550
|
+
rescue ::Docker::Error::DockerError => e
|
|
420
551
|
debug "driver - error :#{e}:"
|
|
421
552
|
end
|
|
422
553
|
end
|
|
423
554
|
end
|
|
424
555
|
|
|
556
|
+
# Build the data image, unless it is already present.
|
|
557
|
+
#
|
|
558
|
+
# @return [void]
|
|
425
559
|
def make_data_image
|
|
426
560
|
debug "driver - calling create_data_image"
|
|
427
561
|
create_data_image(config[:docker_registry])
|
|
428
562
|
end
|
|
429
563
|
|
|
564
|
+
# Create the volume container /opt/chef is mounted into the runner from.
|
|
565
|
+
#
|
|
566
|
+
# The container is shared by every instance on the same chef version and
|
|
567
|
+
# platform, so creation is guarded by a file lock.
|
|
568
|
+
#
|
|
569
|
+
# @param state [Hash] mutable instance state
|
|
570
|
+
# @return [void]
|
|
571
|
+
# @raise [RuntimeError] if the container could not be created
|
|
430
572
|
def create_chef_container(state)
|
|
431
573
|
with_file_lock("#{home_dir}/.dokken-#{chef_container_name}.lock") do
|
|
432
574
|
with_retries do
|
|
@@ -454,12 +596,20 @@ module Kitchen
|
|
|
454
596
|
# has to be built for the same architecture as the runner.
|
|
455
597
|
chef_container = create_container(config, platform: self[:platform])
|
|
456
598
|
state[:chef_container] = chef_container.json
|
|
457
|
-
|
|
599
|
+
# Both constants have to be spelled out: bare `StandardError` inside
|
|
600
|
+
# module Kitchen resolves to Kitchen::StandardError, and Docker::Error
|
|
601
|
+
# is a namespace module that no exception class includes.
|
|
602
|
+
rescue ::Docker::Error::DockerError, ::StandardError => e
|
|
458
603
|
raise "driver - #{chef_container_name} failed to create #{e}"
|
|
459
604
|
end
|
|
460
605
|
end
|
|
461
606
|
end
|
|
462
607
|
|
|
608
|
+
# Run a block holding an exclusive lock on a file.
|
|
609
|
+
#
|
|
610
|
+
# @param path [String] the lock file to create and hold
|
|
611
|
+
# @yield with the lock held
|
|
612
|
+
# @return [void]
|
|
463
613
|
def with_file_lock(path)
|
|
464
614
|
File.open(path, File::RDWR | File::CREAT, 0o644) do |f|
|
|
465
615
|
f.flock(File::LOCK_EX)
|
|
@@ -467,6 +617,9 @@ module Kitchen
|
|
|
467
617
|
end
|
|
468
618
|
end
|
|
469
619
|
|
|
620
|
+
# Log the docker client in, when a creds_file has been configured.
|
|
621
|
+
#
|
|
622
|
+
# @return [void]
|
|
470
623
|
def authenticate!
|
|
471
624
|
# No need to authenticate if the credentials are empty
|
|
472
625
|
return if docker_creds.empty?
|
|
@@ -474,6 +627,9 @@ module Kitchen
|
|
|
474
627
|
::Docker.authenticate! docker_creds
|
|
475
628
|
end
|
|
476
629
|
|
|
630
|
+
# The credentials read from the configured creds_file.
|
|
631
|
+
#
|
|
632
|
+
# @return [Hash] the credentials, or an empty hash
|
|
477
633
|
def docker_creds
|
|
478
634
|
@docker_creds ||= if config[:creds_file]
|
|
479
635
|
JSON.parse(IO.read(config[:creds_file]))
|
|
@@ -482,6 +638,12 @@ module Kitchen
|
|
|
482
638
|
end
|
|
483
639
|
end
|
|
484
640
|
|
|
641
|
+
# The credentials found in ~/.docker/config.json, keyed by registry.
|
|
642
|
+
#
|
|
643
|
+
# `auths` entries resolve to a credentials hash; `credHelpers` entries
|
|
644
|
+
# resolve to a proc that shells out to the helper only if it is needed.
|
|
645
|
+
#
|
|
646
|
+
# @return [Hash{String => Hash, Proc}] credentials by registry key
|
|
485
647
|
def docker_config_creds
|
|
486
648
|
return @docker_config_creds if @docker_config_creds
|
|
487
649
|
|
|
@@ -493,7 +655,8 @@ module Kitchen
|
|
|
493
655
|
config["auths"].each do |k, v|
|
|
494
656
|
next if v["auth"].nil?
|
|
495
657
|
|
|
496
|
-
|
|
658
|
+
# Split once: a colon is legal inside a registry password.
|
|
659
|
+
username, password = Base64.decode64(v["auth"]).split(":", 2)
|
|
497
660
|
@docker_config_creds[k] = { serveraddress: k, username:, password: }
|
|
498
661
|
end
|
|
499
662
|
end
|
|
@@ -533,6 +696,9 @@ module Kitchen
|
|
|
533
696
|
# a registry when it contains a "." or a ":", or is exactly "localhost" --
|
|
534
697
|
# the same heuristic Docker uses to tell "quay.io/org/image" apart from
|
|
535
698
|
# the Hub shorthand "dokken/almalinux-8".
|
|
699
|
+
#
|
|
700
|
+
# @param image [String] an image reference
|
|
701
|
+
# @return [String, nil] the registry host, or nil for Docker Hub
|
|
536
702
|
def image_registry_host(image)
|
|
537
703
|
first, remainder = image.split("/", 2)
|
|
538
704
|
return nil if remainder.nil?
|
|
@@ -548,6 +714,7 @@ module Kitchen
|
|
|
548
714
|
# Pick the config.json key that holds the Docker Hub credentials. A
|
|
549
715
|
# config may spell Hub several ways, so prefer the canonical key and then
|
|
550
716
|
# a fixed alias order rather than whichever happens to be listed first.
|
|
717
|
+
# @return [String, nil] the config.json key holding Docker Hub credentials
|
|
551
718
|
def docker_hub_creds_key
|
|
552
719
|
keys = docker_config_creds.keys.select { |k| DOCKER_HUB_HOSTS.include?(parse_registry_host(k)) }
|
|
553
720
|
return if keys.empty?
|
|
@@ -556,6 +723,10 @@ module Kitchen
|
|
|
556
723
|
keys.min_by { |k| DOCKER_HUB_HOSTS.index(parse_registry_host(k)) }
|
|
557
724
|
end
|
|
558
725
|
|
|
726
|
+
# The ~/.docker/config.json credentials that apply to an image.
|
|
727
|
+
#
|
|
728
|
+
# @param image [String] an image reference
|
|
729
|
+
# @return [Hash, nil] the credentials, or nil when none apply
|
|
559
730
|
def docker_config_creds_for_image(image)
|
|
560
731
|
host = image_registry_host(image)
|
|
561
732
|
|
|
@@ -570,6 +741,10 @@ module Kitchen
|
|
|
570
741
|
c.respond_to?(:call) ? c.call : c
|
|
571
742
|
end
|
|
572
743
|
|
|
744
|
+
# The credentials to send when pulling an image.
|
|
745
|
+
#
|
|
746
|
+
# @param image [String] an image reference
|
|
747
|
+
# @return [Hash] credentials, or {NO_DOCKER_CREDS} to pull anonymously
|
|
573
748
|
def docker_creds_for_image(image)
|
|
574
749
|
return docker_creds if config[:creds_file]
|
|
575
750
|
return NO_DOCKER_CREDS unless config[:docker_config_creds]
|
|
@@ -577,39 +752,53 @@ module Kitchen
|
|
|
577
752
|
docker_config_creds_for_image(image) || NO_DOCKER_CREDS
|
|
578
753
|
end
|
|
579
754
|
|
|
755
|
+
# Pull the platform image the work image is built from.
|
|
756
|
+
#
|
|
757
|
+
# @return [void]
|
|
580
758
|
def pull_platform_image
|
|
581
759
|
debug "driver - pulling #{short_image_path(platform_image)}"
|
|
582
760
|
config[:pull_platform_image] ? pull_image(platform_image) : pull_if_missing(platform_image)
|
|
583
761
|
end
|
|
584
762
|
|
|
763
|
+
# Pull the chef/cinc image the volume container is built from.
|
|
764
|
+
#
|
|
765
|
+
# @return [void]
|
|
585
766
|
def pull_chef_image
|
|
586
767
|
debug "driver - pulling #{short_image_path(chef_image)}"
|
|
587
768
|
config[:pull_chef_image] ? pull_image(chef_image) : pull_if_missing(chef_image)
|
|
588
769
|
end
|
|
589
770
|
|
|
771
|
+
# Force-remove an image.
|
|
772
|
+
#
|
|
773
|
+
# @param name [String] an image reference
|
|
774
|
+
# @return [void]
|
|
590
775
|
def delete_image(name)
|
|
591
776
|
with_retries { @image = ::Docker::Image.get(name, { "platform" => oci_platform(config[:platform]) }, docker_connection) }
|
|
592
777
|
with_retries { @image.remove(force: true) }
|
|
593
|
-
rescue ::Docker::Error
|
|
778
|
+
rescue ::Docker::Error::DockerError
|
|
594
779
|
puts "Image #{name} not found. Nothing to delete."
|
|
595
780
|
end
|
|
596
781
|
|
|
782
|
+
# Whether the daemon knows about a container.
|
|
783
|
+
#
|
|
784
|
+
# @param name [String] the container name
|
|
785
|
+
# @return [Boolean] true when the container exists
|
|
597
786
|
def container_exist?(name)
|
|
598
787
|
true if ::Docker::Container.get(name, {}, docker_connection)
|
|
599
|
-
rescue StandardError
|
|
788
|
+
rescue ::StandardError
|
|
600
789
|
false
|
|
601
790
|
end
|
|
602
791
|
|
|
792
|
+
# Split an image reference into its repo and tag.
|
|
793
|
+
#
|
|
794
|
+
# @param image [String] the docker image path to parse
|
|
795
|
+
# @return [Array(String, String)] the repo and tag
|
|
603
796
|
def parse_image_name(image)
|
|
604
|
-
|
|
797
|
+
repo, separator, tag = image.rpartition(":")
|
|
605
798
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
else
|
|
610
|
-
tag = parts[1] || "latest"
|
|
611
|
-
repo = parts[0]
|
|
612
|
-
end
|
|
799
|
+
# A colon before a slash delimits a registry port, not a tag:
|
|
800
|
+
# "localhost:5000/almalinux" is an untagged image on a local registry.
|
|
801
|
+
return [image, "latest"] if separator.empty? || tag.include?("/")
|
|
613
802
|
|
|
614
803
|
[repo, tag]
|
|
615
804
|
end
|
|
@@ -653,27 +842,45 @@ module Kitchen
|
|
|
653
842
|
end
|
|
654
843
|
end
|
|
655
844
|
|
|
845
|
+
# Fetch a container by name, creating it if it does not exist.
|
|
846
|
+
#
|
|
847
|
+
# @param args [Hash] the `/containers/create` body
|
|
848
|
+
# @param platform [String, nil] an OCI platform to pin the container to
|
|
849
|
+
# @return [::Docker::Container] the container
|
|
850
|
+
# @raise [RuntimeError] if the container could not be created
|
|
656
851
|
def create_container(args, platform: nil)
|
|
657
852
|
with_retries { @container = ::Docker::Container.get(args["name"], {}, docker_connection) }
|
|
658
853
|
rescue
|
|
659
854
|
with_retries do
|
|
660
|
-
|
|
661
|
-
args["Env"]
|
|
662
|
-
|
|
855
|
+
# Merge rather than append: start_runner_container passes config[:env]
|
|
856
|
+
# straight through, so mutating args["Env"] would edit the driver's
|
|
857
|
+
# own configuration -- and stamp it again on every retry.
|
|
858
|
+
create_args = args.merge("Env" => Array(args["Env"]) + container_env)
|
|
663
859
|
info "Creating container #{args["name"]}"
|
|
664
|
-
debug "driver - create_container args #{
|
|
860
|
+
debug "driver - create_container args #{create_args}"
|
|
665
861
|
with_retries do
|
|
666
|
-
@container = create_container_for_platform(
|
|
862
|
+
@container = create_container_for_platform(create_args, platform)
|
|
667
863
|
rescue ::Docker::Error::ConflictError
|
|
668
864
|
debug "driver - rescue ConflictError: #{args["name"]}"
|
|
669
865
|
with_retries { @container = ::Docker::Container.get(args["name"], {}, docker_connection) }
|
|
670
866
|
end
|
|
671
|
-
rescue ::Docker::Error => e
|
|
867
|
+
rescue ::Docker::Error::DockerError => e
|
|
672
868
|
debug "driver - error :#{e}:"
|
|
673
869
|
raise "driver - failed to create_container #{args["name"]}"
|
|
674
870
|
end
|
|
675
871
|
end
|
|
676
872
|
|
|
873
|
+
# Environment variables kitchen-dokken stamps onto every container it
|
|
874
|
+
# creates, so that recipes and tests can tell they are running under
|
|
875
|
+
# Test Kitchen.
|
|
876
|
+
#
|
|
877
|
+
# @return [Array<String>] `KEY=value` strings
|
|
878
|
+
def container_env
|
|
879
|
+
env = ["TEST_KITCHEN=1"]
|
|
880
|
+
env << "CI=#{ENV["CI"]}" if ENV.include? "CI"
|
|
881
|
+
env
|
|
882
|
+
end
|
|
883
|
+
|
|
677
884
|
# Create a container, optionally pinned to an OCI platform.
|
|
678
885
|
#
|
|
679
886
|
# /containers/create takes `platform` as a *query* parameter, but
|
|
@@ -682,6 +889,10 @@ module Kitchen
|
|
|
682
889
|
# is therefore accepted and silently ignored by the daemon, which is why
|
|
683
890
|
# the platform config had no effect on container creation. Post directly
|
|
684
891
|
# when a platform is wanted; otherwise use the gem as before.
|
|
892
|
+
#
|
|
893
|
+
# @param args [Hash] the `/containers/create` body
|
|
894
|
+
# @param platform [String, nil] an OCI platform such as `linux/arm64/v8`
|
|
895
|
+
# @return [::Docker::Container] the created container
|
|
685
896
|
def create_container_for_platform(args, platform)
|
|
686
897
|
return ::Docker::Container.create(args, docker_connection) if platform.to_s.empty?
|
|
687
898
|
|
|
@@ -691,8 +902,13 @@ module Kitchen
|
|
|
691
902
|
::Docker::Container.get(args["name"], {}, docker_connection)
|
|
692
903
|
end
|
|
693
904
|
|
|
905
|
+
# Create a container if needed, then start it and wait for it to run.
|
|
906
|
+
#
|
|
907
|
+
# @param args [Hash] the `/containers/create` body
|
|
908
|
+
# @param platform [String, nil] an OCI platform to pin the container to
|
|
909
|
+
# @return [::Docker::Container] the running container
|
|
694
910
|
def run_container(args, platform: nil)
|
|
695
|
-
create_container(args, platform: platform)
|
|
911
|
+
@container = create_container(args, platform: platform)
|
|
696
912
|
with_retries do
|
|
697
913
|
@container.start
|
|
698
914
|
@container = ::Docker::Container.get(args["name"], {}, docker_connection)
|
|
@@ -701,10 +917,17 @@ module Kitchen
|
|
|
701
917
|
@container
|
|
702
918
|
end
|
|
703
919
|
|
|
920
|
+
# The current container's `State` payload.
|
|
921
|
+
#
|
|
922
|
+
# @return [Hash] the state, or an empty hash if there is no container
|
|
704
923
|
def container_state
|
|
705
924
|
@container ? @container.info["State"] : {}
|
|
706
925
|
end
|
|
707
926
|
|
|
927
|
+
# Stop a container and wait for it to leave the running state.
|
|
928
|
+
#
|
|
929
|
+
# @param name [String] the container name
|
|
930
|
+
# @return [void]
|
|
708
931
|
def stop_container(name)
|
|
709
932
|
with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
|
|
710
933
|
with_retries do
|
|
@@ -715,6 +938,10 @@ module Kitchen
|
|
|
715
938
|
debug "Container #{name} not found. Nothing to stop."
|
|
716
939
|
end
|
|
717
940
|
|
|
941
|
+
# Force-remove a container along with its anonymous volumes.
|
|
942
|
+
#
|
|
943
|
+
# @param name [String] the container name
|
|
944
|
+
# @return [void]
|
|
718
945
|
def delete_container(name)
|
|
719
946
|
with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
|
|
720
947
|
with_retries { @container.delete(force: true, v: true) }
|
|
@@ -722,6 +949,14 @@ module Kitchen
|
|
|
722
949
|
debug "Container #{name} not found. Nothing to delete."
|
|
723
950
|
end
|
|
724
951
|
|
|
952
|
+
# Poll a container until it reaches the wanted running state.
|
|
953
|
+
#
|
|
954
|
+
# Gives up after a bounded number of polls rather than blocking a converge
|
|
955
|
+
# forever, and stops early once the container has actually finished.
|
|
956
|
+
#
|
|
957
|
+
# @param name [String] the container name
|
|
958
|
+
# @param v [Boolean] the running state to wait for
|
|
959
|
+
# @return [void]
|
|
725
960
|
def wait_running_state(name, v)
|
|
726
961
|
@container = ::Docker::Container.get(name, {}, docker_connection)
|
|
727
962
|
i = 0
|
|
@@ -735,29 +970,51 @@ module Kitchen
|
|
|
735
970
|
end
|
|
736
971
|
end
|
|
737
972
|
|
|
973
|
+
# The name of the shared chef volume container.
|
|
974
|
+
#
|
|
975
|
+
# @return [String] the container name
|
|
738
976
|
def chef_container_name
|
|
739
977
|
prefix = instance.provisioner[:product_name] == "cinc" ? "cinc" : "chef"
|
|
740
|
-
|
|
978
|
+
# `platform: ~` in a kitchen.yml yields nil rather than the "" default.
|
|
979
|
+
platform = config[:platform].to_s
|
|
980
|
+
return "#{prefix}-#{chef_version}" if platform.empty?
|
|
981
|
+
|
|
982
|
+
"#{prefix}-#{chef_version}-#{platform.tr("/", "-")}"
|
|
741
983
|
end
|
|
742
984
|
|
|
985
|
+
# The chef/cinc image the volume container is built from.
|
|
986
|
+
#
|
|
987
|
+
# @return [String] an image reference
|
|
743
988
|
def chef_image
|
|
744
989
|
"#{config[:chef_image]}:#{chef_version}"
|
|
745
990
|
end
|
|
746
991
|
|
|
992
|
+
# The chef version to use, as an image tag.
|
|
993
|
+
#
|
|
994
|
+
# @return [String] a tag
|
|
747
995
|
def chef_version
|
|
748
996
|
return "latest" if config[:chef_version] == "stable"
|
|
749
997
|
|
|
750
998
|
config[:chef_version]
|
|
751
999
|
end
|
|
752
1000
|
|
|
1001
|
+
# The name of this instance's data container.
|
|
1002
|
+
#
|
|
1003
|
+
# @return [String] the container name
|
|
753
1004
|
def data_container_name
|
|
754
1005
|
"#{instance_name}-data"
|
|
755
1006
|
end
|
|
756
1007
|
|
|
1008
|
+
# The image the data container is built from.
|
|
1009
|
+
#
|
|
1010
|
+
# @return [String] an image reference
|
|
757
1011
|
def data_image
|
|
758
1012
|
config[:data_image]
|
|
759
1013
|
end
|
|
760
1014
|
|
|
1015
|
+
# The `PortBindings` value for the data container.
|
|
1016
|
+
#
|
|
1017
|
+
# @return [Hash] a Docker API PortBindings hash
|
|
761
1018
|
def data_port_bindings
|
|
762
1019
|
return port_bindings unless config[:data_ssh_port]
|
|
763
1020
|
|
|
@@ -779,15 +1036,25 @@ module Kitchen
|
|
|
779
1036
|
end
|
|
780
1037
|
end
|
|
781
1038
|
|
|
1039
|
+
# The base image for the platform under test.
|
|
1040
|
+
#
|
|
1041
|
+
# @return [String] an image reference
|
|
782
1042
|
def platform_image
|
|
783
1043
|
config[:image] || platform_image_from_name
|
|
784
1044
|
end
|
|
785
1045
|
|
|
1046
|
+
# Derive an image reference from the kitchen platform name.
|
|
1047
|
+
#
|
|
1048
|
+
# @return [String] an image reference
|
|
786
1049
|
def platform_image_from_name
|
|
787
1050
|
platform, release = instance.platform.name.split("-")
|
|
788
1051
|
release ? [platform, release].join(":") : platform
|
|
789
1052
|
end
|
|
790
1053
|
|
|
1054
|
+
# Pull an image only when the daemon does not already have it.
|
|
1055
|
+
#
|
|
1056
|
+
# @param image [String] an image reference
|
|
1057
|
+
# @return [void]
|
|
791
1058
|
def pull_if_missing(image)
|
|
792
1059
|
return if ::Docker::Image.exist?(registry_image_path(image), { "platform" => oci_platform(config[:platform]) }, docker_connection)
|
|
793
1060
|
|
|
@@ -795,10 +1062,18 @@ module Kitchen
|
|
|
795
1062
|
end
|
|
796
1063
|
|
|
797
1064
|
# https://github.com/docker/docker/blob/4fcb9ac40ce33c4d6e08d5669af6be5e076e2574/registry/auth.go#L231
|
|
1065
|
+
# Reduce a config.json registry key to a bare host.
|
|
1066
|
+
#
|
|
1067
|
+
# @param val [String] a registry key, possibly a URL
|
|
1068
|
+
# @return [String] the host
|
|
798
1069
|
def parse_registry_host(val)
|
|
799
1070
|
val.sub(%r{https?://}, "").split("/").first
|
|
800
1071
|
end
|
|
801
1072
|
|
|
1073
|
+
# Pull an image from its registry.
|
|
1074
|
+
#
|
|
1075
|
+
# @param image [String] an image reference
|
|
1076
|
+
# @return [Boolean] true when the pull changed what is on disk
|
|
802
1077
|
def pull_image(image)
|
|
803
1078
|
path = registry_image_path(image)
|
|
804
1079
|
with_retries do
|
|
@@ -817,6 +1092,9 @@ module Kitchen
|
|
|
817
1092
|
# matters: the daemon will not match a filter of
|
|
818
1093
|
# {"os":"linux","architecture":"amd64"} against a linux/amd64/v2 image,
|
|
819
1094
|
# so dropping it makes every image lookup for a variant image miss.
|
|
1095
|
+
#
|
|
1096
|
+
# @param platform [String, nil] an "os/arch[/variant]" string
|
|
1097
|
+
# @return [String, nil] a JSON OCI platform spec, or the input unchanged
|
|
820
1098
|
def oci_platform(platform)
|
|
821
1099
|
return platform if platform.nil? || !platform.include?("/")
|
|
822
1100
|
|
|
@@ -826,10 +1104,18 @@ module Kitchen
|
|
|
826
1104
|
spec.to_json
|
|
827
1105
|
end
|
|
828
1106
|
|
|
1107
|
+
# The name of this instance's runner container.
|
|
1108
|
+
#
|
|
1109
|
+
# @return [String] the container name
|
|
829
1110
|
def runner_container_name
|
|
830
1111
|
instance_name.to_s
|
|
831
1112
|
end
|
|
832
1113
|
|
|
1114
|
+
# Retry a docker API call through the errors that retrying can fix.
|
|
1115
|
+
#
|
|
1116
|
+
# @yield the API call to attempt
|
|
1117
|
+
# @return [Object] the block's value
|
|
1118
|
+
# @raise [::Docker::Error::DockerError] if every attempt failed
|
|
833
1119
|
def with_retries
|
|
834
1120
|
tries = api_retries
|
|
835
1121
|
begin
|