kitchen-dokken 2.23.2 → 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 +370 -36
- 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
data/lib/kitchen/helpers.rb
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
|
+
# Shared behaviour for the kitchen-dokken driver, provisioner and transport.
|
|
2
|
+
#
|
|
3
|
+
# Each of those three plugins mixes this in (at the top level, so the methods
|
|
4
|
+
# also land on Object) and supplies the small contract the helpers depend on:
|
|
5
|
+
# `config`, `instance`, `self[]`, `info` and `debug`.
|
|
1
6
|
module Dokken
|
|
7
|
+
# @see Kitchen::Driver::Dokken
|
|
8
|
+
# @see Kitchen::Provisioner::Dokken
|
|
9
|
+
# @see Kitchen::Transport::Dokken
|
|
2
10
|
module Helpers
|
|
11
|
+
require "digest" unless defined?(Digest)
|
|
12
|
+
require "fileutils" unless defined?(FileUtils)
|
|
3
13
|
# https://stackoverflow.com/questions/517219/ruby-see-if-a-port-is-open
|
|
4
14
|
require "socket" unless defined?(Socket)
|
|
5
15
|
require "timeout" unless defined?(Timeout)
|
|
16
|
+
require "tmpdir" unless defined?(Dir.mktmpdir)
|
|
6
17
|
require "resolv" unless defined?(Resolv)
|
|
7
18
|
|
|
19
|
+
# How long to wait for a TCP connect before calling a port closed.
|
|
20
|
+
PORT_PROBE_TIMEOUT = 1
|
|
21
|
+
|
|
22
|
+
# Check whether something is accepting TCP connections at `ip:port`.
|
|
23
|
+
#
|
|
24
|
+
# Used by the transport to pick a reachable address for the data
|
|
25
|
+
# container's sshd. Every failure mode -- refused, unroutable,
|
|
26
|
+
# unresolvable, timed out -- is a "no" rather than an exception, because
|
|
27
|
+
# the caller's job is to try the next candidate address, not to abort.
|
|
28
|
+
#
|
|
29
|
+
# @param ip [String] an address or hostname to probe
|
|
30
|
+
# @param port [String, Integer] the TCP port to probe
|
|
31
|
+
# @return [Boolean] true when the connection was accepted
|
|
8
32
|
def port_open?(ip, port)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ENETDOWN
|
|
15
|
-
return false
|
|
16
|
-
end
|
|
17
|
-
rescue Timeout::Error
|
|
33
|
+
Timeout.timeout(PORT_PROBE_TIMEOUT) do
|
|
34
|
+
TCPSocket.new(ip, port).close
|
|
35
|
+
true
|
|
36
|
+
rescue SystemCallError, SocketError, IOError
|
|
37
|
+
false
|
|
18
38
|
end
|
|
39
|
+
rescue Timeout::Error
|
|
19
40
|
false
|
|
20
41
|
end
|
|
21
42
|
|
|
43
|
+
# The public half of the throwaway keypair baked into the data image.
|
|
44
|
+
#
|
|
45
|
+
# It is deliberately published: the data container is only reachable on
|
|
46
|
+
# the local docker network for the life of one kitchen run, and shipping
|
|
47
|
+
# a fixed key avoids generating one per instance.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] an OpenSSH `authorized_keys` line
|
|
22
50
|
def insecure_ssh_public_key
|
|
23
51
|
<<~EOF
|
|
24
52
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCoJwyW7qNhw+NTuOjC4+RVpESl+JBXebXzB7JqxRgKAbymq6B39azEAiNx5NzHkWcQmOyQNhFpKFSAufegcXRS4ctS1LcElEoXe9brDAqKEBSkmnXYfZXMNIG0Enw4+5W/rZxHFCAlsUSAHYtYZEs+3CgbIWuHhZ95C8UC6nGLWHNZOjcbsYZFrnFfO0qg0ene2w8LKhxqj5X0MRSdCIn1IwyxIbl5NND5Yk1Hx8JKsJtTiNTdxssiMgmM5bvTbYQUSf8pbGrRI30VQKBgQ8/UkidZbaTfvzWXYpwcDUERSbzEYCvkUytTemZIv6uhpPxqkfjl6KEOOml/iGqquPEr test-kitchen-rsa
|
|
25
53
|
EOF
|
|
26
54
|
end
|
|
27
55
|
|
|
56
|
+
# The private half of the throwaway keypair.
|
|
57
|
+
#
|
|
58
|
+
# Written to disk 0600 by the transport just before it shells out to
|
|
59
|
+
# rsync or scp. See {#insecure_ssh_public_key} for why a fixed key is
|
|
60
|
+
# acceptable here.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] a PEM-encoded RSA private key
|
|
28
63
|
def insecure_ssh_private_key
|
|
29
64
|
<<~EOF
|
|
30
65
|
-----BEGIN RSA PRIVATE KEY-----
|
|
@@ -57,6 +92,15 @@ module Dokken
|
|
|
57
92
|
EOF
|
|
58
93
|
end
|
|
59
94
|
|
|
95
|
+
# The Dockerfile used to build the data image.
|
|
96
|
+
#
|
|
97
|
+
# The data image exists to hold the kitchen and verifier sandboxes and
|
|
98
|
+
# serve them over ssh, for the cases where the daemon cannot see the
|
|
99
|
+
# local filesystem: a remote docker host, or kitchen itself running in a
|
|
100
|
+
# container.
|
|
101
|
+
#
|
|
102
|
+
# @param registry [String, nil] a registry to pull the base image from
|
|
103
|
+
# @return [String] the Dockerfile contents
|
|
60
104
|
def data_dockerfile(registry)
|
|
61
105
|
from = "almalinux:9"
|
|
62
106
|
if registry
|
|
@@ -88,6 +132,10 @@ module Dokken
|
|
|
88
132
|
EOF
|
|
89
133
|
end
|
|
90
134
|
|
|
135
|
+
# Build and tag the data image, unless it is already present.
|
|
136
|
+
#
|
|
137
|
+
# @param registry [String, nil] a registry to pull the base image from
|
|
138
|
+
# @return [void]
|
|
91
139
|
def create_data_image(registry)
|
|
92
140
|
return if ::Docker::Image.exist?(data_image)
|
|
93
141
|
|
|
@@ -104,6 +152,9 @@ module Dokken
|
|
|
104
152
|
i.tag("repo" => repo(data_image), "tag" => tag(data_image), "force" => true)
|
|
105
153
|
end
|
|
106
154
|
|
|
155
|
+
# Work out which docker daemon to talk to when the user has not said.
|
|
156
|
+
#
|
|
157
|
+
# @return [String] a docker host URL
|
|
107
158
|
def default_docker_host
|
|
108
159
|
if ENV["DOCKER_HOST"]
|
|
109
160
|
ENV["DOCKER_HOST"]
|
|
@@ -117,15 +168,39 @@ module Dokken
|
|
|
117
168
|
end
|
|
118
169
|
end
|
|
119
170
|
|
|
171
|
+
# Fetch and cache the daemon's `/info` payload.
|
|
172
|
+
#
|
|
173
|
+
# The cache is keyed by host because the driver, provisioner and
|
|
174
|
+
# transport each resolve their own `:docker_info` default, and a
|
|
175
|
+
# kitchen.yml may point them at different daemons.
|
|
176
|
+
#
|
|
177
|
+
# @param docker_host [String] the docker host URL to query
|
|
178
|
+
# @return [Hash] the daemon's info payload
|
|
120
179
|
def docker_info(docker_host)
|
|
121
180
|
::Docker.url = docker_host
|
|
122
181
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
# Keyed by host: the driver, provisioner and transport each resolve
|
|
183
|
+
# their own :docker_info default and a kitchen.yml may point them at
|
|
184
|
+
# different daemons. A single memo would hand the second caller the
|
|
185
|
+
# first daemon's payload, which is what remote_docker_host? keys its
|
|
186
|
+
# whole local-vs-remote decision off.
|
|
187
|
+
@docker_info ||= {}
|
|
188
|
+
@docker_info[docker_host] ||= ::Docker.info
|
|
189
|
+
rescue Excon::Error::Socket => e
|
|
190
|
+
# Deliberately a UserError rather than `exit!`. This runs from inside a
|
|
191
|
+
# `default_config` block, so an `exit!` here takes the whole kitchen
|
|
192
|
+
# process down mid-resolve: no error banner, no `.kitchen/logs`, and
|
|
193
|
+
# every other instance in the run abandoned without being destroyed.
|
|
194
|
+
# A UserError is the failure mode Test Kitchen already knows how to
|
|
195
|
+
# report, and it leaves the memo empty so a later caller can retry.
|
|
196
|
+
raise Kitchen::UserError,
|
|
197
|
+
"kitchen-dokken could not connect to the docker host at #{docker_host}. " \
|
|
198
|
+
"Is docker running? (#{e.class}: #{e.message})"
|
|
127
199
|
end
|
|
128
200
|
|
|
201
|
+
# Create the kitchen and verifier sandbox directories on the host.
|
|
202
|
+
#
|
|
203
|
+
# @return [void]
|
|
129
204
|
def dokken_create_sandbox
|
|
130
205
|
info("Creating kitchen sandbox at #{dokken_kitchen_sandbox}")
|
|
131
206
|
FileUtils.mkdir_p(dokken_kitchen_sandbox, mode: 0o755)
|
|
@@ -134,22 +209,25 @@ module Dokken
|
|
|
134
209
|
FileUtils.mkdir_p(dokken_verifier_sandbox, mode: 0o755)
|
|
135
210
|
end
|
|
136
211
|
|
|
212
|
+
# Remove the kitchen and verifier sandbox directories from the host.
|
|
213
|
+
#
|
|
214
|
+
# `rm_rf` already tolerates a path that is not there, so a sandbox that
|
|
215
|
+
# was never created -- or that a previous destroy already removed -- is a
|
|
216
|
+
# no-op rather than an error. The Errno::ENOENT rescues that used to wrap
|
|
217
|
+
# these calls could never fire.
|
|
218
|
+
#
|
|
219
|
+
# @return [void]
|
|
137
220
|
def dokken_delete_sandbox
|
|
138
221
|
info("Deleting kitchen sandbox at #{dokken_kitchen_sandbox}")
|
|
139
|
-
|
|
140
|
-
FileUtils.rm_rf(dokken_kitchen_sandbox)
|
|
141
|
-
rescue Errno::ENOENT
|
|
142
|
-
debug("Cannot delete #{dokken_kitchen_sandbox}. Does not exist")
|
|
143
|
-
end
|
|
222
|
+
FileUtils.rm_rf(dokken_kitchen_sandbox)
|
|
144
223
|
|
|
145
224
|
info("Deleting verifier sandbox at #{dokken_verifier_sandbox}")
|
|
146
|
-
|
|
147
|
-
FileUtils.rm_rf(dokken_verifier_sandbox)
|
|
148
|
-
rescue Errno::ENOENT
|
|
149
|
-
debug("Cannot delete #{dokken_verifier_sandbox}. Does not exist")
|
|
150
|
-
end
|
|
225
|
+
FileUtils.rm_rf(dokken_verifier_sandbox)
|
|
151
226
|
end
|
|
152
227
|
|
|
228
|
+
# The home directory, in a form docker will accept in a bind mount spec.
|
|
229
|
+
#
|
|
230
|
+
# @return [String] an absolute path
|
|
153
231
|
def home_dir
|
|
154
232
|
# while dokken_binds avoid invalid bind mount spec "C:/Users/..." error by
|
|
155
233
|
# remote docker host virtual box shared folder on boot2docker created by docker-machine in Windows
|
|
@@ -161,23 +239,41 @@ module Dokken
|
|
|
161
239
|
Dir.home
|
|
162
240
|
end
|
|
163
241
|
|
|
242
|
+
# Where the provisioner stages files for this instance.
|
|
243
|
+
#
|
|
244
|
+
# @return [String] an absolute path on the host
|
|
164
245
|
def dokken_kitchen_sandbox
|
|
165
246
|
"#{home_dir}/.dokken/kitchen_sandbox/#{instance_name}"
|
|
166
247
|
end
|
|
167
248
|
|
|
249
|
+
# Where the verifier stages files for this instance.
|
|
250
|
+
#
|
|
251
|
+
# @return [String] an absolute path on the host
|
|
168
252
|
def dokken_verifier_sandbox
|
|
169
253
|
"#{home_dir}/.dokken/verifier_sandbox/#{instance_name}"
|
|
170
254
|
end
|
|
171
255
|
|
|
256
|
+
# A container-safe, collision-free name for this kitchen instance.
|
|
257
|
+
#
|
|
258
|
+
# The working directory is hashed into the prefix so that the same suite
|
|
259
|
+
# in two checkouts does not fight over one set of containers.
|
|
260
|
+
#
|
|
261
|
+
# @return [String] the instance name
|
|
172
262
|
def instance_name
|
|
173
263
|
prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
|
|
174
264
|
"#{prefix}-#{instance.name}".downcase
|
|
175
265
|
end
|
|
176
266
|
|
|
267
|
+
# The `ExposedPorts` value for the runner container.
|
|
268
|
+
#
|
|
269
|
+
# @return [Hash, nil] a Docker API ExposedPorts hash
|
|
177
270
|
def exposed_ports
|
|
178
271
|
coerce_exposed_ports(config[:ports])
|
|
179
272
|
end
|
|
180
273
|
|
|
274
|
+
# Network creation options for the dokken network.
|
|
275
|
+
#
|
|
276
|
+
# @return [Hash] options for `POST /networks/create`
|
|
181
277
|
def network_settings
|
|
182
278
|
if self[:ipv6]
|
|
183
279
|
{
|
|
@@ -193,10 +289,17 @@ module Dokken
|
|
|
193
289
|
end
|
|
194
290
|
end
|
|
195
291
|
|
|
292
|
+
# The `PortBindings` value for the runner container.
|
|
293
|
+
#
|
|
294
|
+
# @return [Hash, nil] a Docker API PortBindings hash
|
|
196
295
|
def port_bindings
|
|
197
296
|
coerce_port_bindings(config[:ports])
|
|
198
297
|
end
|
|
199
298
|
|
|
299
|
+
# Normalise a `ports:` setting into a Docker API ExposedPorts hash.
|
|
300
|
+
#
|
|
301
|
+
# @param v [Hash, Array<String>, String, nil] the configured ports
|
|
302
|
+
# @return [Hash, nil] an ExposedPorts hash, or nil to omit the key
|
|
200
303
|
def coerce_exposed_ports(v)
|
|
201
304
|
case v
|
|
202
305
|
when Hash, nil
|
|
@@ -208,6 +311,10 @@ module Dokken
|
|
|
208
311
|
end
|
|
209
312
|
end
|
|
210
313
|
|
|
314
|
+
# Normalise a `ports:` setting into a Docker API PortBindings hash.
|
|
315
|
+
#
|
|
316
|
+
# @param v [Hash, Array<String>, String, nil] the configured ports
|
|
317
|
+
# @return [Hash, nil] a PortBindings hash, or nil to omit the key
|
|
211
318
|
def coerce_port_bindings(v)
|
|
212
319
|
case v
|
|
213
320
|
when Hash, nil
|
|
@@ -225,6 +332,15 @@ module Dokken
|
|
|
225
332
|
end
|
|
226
333
|
end
|
|
227
334
|
|
|
335
|
+
# Parse one docker-style port spec into its component bindings.
|
|
336
|
+
#
|
|
337
|
+
# Accepts `container`, `host:container` and `host_ip:host:container`,
|
|
338
|
+
# each optionally suffixed with `/protocol` and each allowing an
|
|
339
|
+
# inclusive `low-high` container port range.
|
|
340
|
+
#
|
|
341
|
+
# @param v [String] a port specification
|
|
342
|
+
# @return [Array<Hash>] one entry per container port
|
|
343
|
+
# @raise [Kitchen::UserError] if a port range is inverted
|
|
228
344
|
def parse_port(v)
|
|
229
345
|
parts = v.split(":")
|
|
230
346
|
case parts.length
|
|
@@ -240,14 +356,28 @@ module Dokken
|
|
|
240
356
|
host_ip = ""
|
|
241
357
|
host_port = ""
|
|
242
358
|
container_port = parts[0]
|
|
359
|
+
else
|
|
360
|
+
# Without this the case fell through leaving container_port nil and
|
|
361
|
+
# the user got `undefined method 'split' for nil` from the line below.
|
|
362
|
+
# An IPv6 host address is how this gets hit in the wild:
|
|
363
|
+
# "[::1]:8500:8500" splits on ":" into five parts, not three.
|
|
364
|
+
raise Kitchen::UserError,
|
|
365
|
+
"Invalid port spec #{v.inspect}: expected container, host:container " \
|
|
366
|
+
"or host_ip:host:container. An IPv6 host address cannot be used here."
|
|
243
367
|
end
|
|
368
|
+
# `split` drops trailing empty fields, so several plausible typos leave
|
|
369
|
+
# nil where a port should be and only fail further down with an error
|
|
370
|
+
# naming a type instead of a port: "/" splits to [], "8080-" to
|
|
371
|
+
# ["8080"], "-" to []. Each of those used to surface as
|
|
372
|
+
# `undefined method 'include?' for nil` or `comparison of Integer with
|
|
373
|
+
# nil failed` in front of a user who simply mistyped kitchen.yml.
|
|
244
374
|
port_range, protocol = container_port.split("/")
|
|
245
|
-
if port_range.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
Chef::Log.fatal("FATAL: Invalid port range! #{container_port}") if port_range[0] > port_range[1]
|
|
249
|
-
port_range = (port_range[0]..port_range[1]).to_a
|
|
375
|
+
if port_range.nil? || port_range.empty?
|
|
376
|
+
raise Kitchen::UserError,
|
|
377
|
+
"Invalid port spec #{v.inspect}: no container port"
|
|
250
378
|
end
|
|
379
|
+
|
|
380
|
+
port_range = expand_port_range(port_range, v) if port_range.include?("-")
|
|
251
381
|
# qualify the port-binding protocol even when it is implicitly tcp #427.
|
|
252
382
|
protocol = "tcp" if protocol.nil?
|
|
253
383
|
Array(port_range).map do |port|
|
|
@@ -259,18 +389,67 @@ module Dokken
|
|
|
259
389
|
end
|
|
260
390
|
end
|
|
261
391
|
|
|
392
|
+
# Expand an inclusive `low-high` container port range.
|
|
393
|
+
#
|
|
394
|
+
# @param range [String] the range, e.g. "8080-8082"
|
|
395
|
+
# @param spec [String] the whole port spec, for the error message
|
|
396
|
+
# @return [Array<Integer>] every port in the range
|
|
397
|
+
# @raise [Kitchen::UserError] if either end is missing or not a number,
|
|
398
|
+
# or if the range is inverted
|
|
399
|
+
def expand_port_range(range, spec)
|
|
400
|
+
low, high = range.split("-")
|
|
401
|
+
|
|
402
|
+
unless numeric_port?(low) && numeric_port?(high)
|
|
403
|
+
raise Kitchen::UserError,
|
|
404
|
+
"Invalid port range #{range.inspect} in port spec #{spec.inspect}: " \
|
|
405
|
+
"expected two port numbers separated by a dash, as in 8080-8082"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
low = low.to_i
|
|
409
|
+
high = high.to_i
|
|
410
|
+
|
|
411
|
+
if low > high
|
|
412
|
+
raise Kitchen::UserError,
|
|
413
|
+
"Invalid port range #{range.inspect} in port spec #{spec.inspect}: " \
|
|
414
|
+
"the low port must not be greater than the high port"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
(low..high).to_a
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# @param value [String, nil] a candidate port number
|
|
421
|
+
# @return [Boolean] whether it is a bare, non-empty run of digits
|
|
422
|
+
def numeric_port?(value)
|
|
423
|
+
!value.nil? && value.match?(/\A\d+\z/)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Whether the daemon is somewhere that cannot see the local filesystem.
|
|
427
|
+
#
|
|
428
|
+
# Docker Desktop and Boot2Docker are reached over tcp but share the
|
|
429
|
+
# host's files, so they count as local no matter what the URL says.
|
|
430
|
+
#
|
|
431
|
+
# @return [Boolean] true when the sandbox must be shipped over ssh
|
|
262
432
|
def remote_docker_host?
|
|
263
|
-
|
|
264
|
-
|
|
433
|
+
# Podman and some rootless daemons omit OperatingSystem entirely, so
|
|
434
|
+
# coerce before matching rather than calling #include? on nil.
|
|
435
|
+
operating_system = config[:docker_info].to_h["OperatingSystem"].to_s
|
|
436
|
+
return false if operating_system.include?("Docker Desktop")
|
|
437
|
+
return false if operating_system.include?("Boot2Docker")
|
|
265
438
|
return true if /^tcp:/.match?(config[:docker_host_url])
|
|
266
439
|
|
|
267
440
|
false
|
|
268
441
|
end
|
|
269
442
|
|
|
443
|
+
# Whether kitchen itself is running inside a container.
|
|
444
|
+
#
|
|
445
|
+
# @return [Boolean] true when running in Docker
|
|
270
446
|
def running_inside_docker?
|
|
271
447
|
File.file?("/.dockerenv")
|
|
272
448
|
end
|
|
273
449
|
|
|
450
|
+
# Whether kitchen is running inside Docker Desktop specifically.
|
|
451
|
+
#
|
|
452
|
+
# @return [Boolean] true when `host.docker.internal` resolves
|
|
274
453
|
def running_inside_docker_desktop?
|
|
275
454
|
Resolv.getaddress "host.docker.internal."
|
|
276
455
|
true
|
|
@@ -278,14 +457,23 @@ module Dokken
|
|
|
278
457
|
false
|
|
279
458
|
end
|
|
280
459
|
|
|
460
|
+
# Where the verifier stages files for this instance.
|
|
461
|
+
#
|
|
462
|
+
# @return [String] an absolute path on the host
|
|
281
463
|
def sandbox_path
|
|
282
464
|
"#{Dir.home}/.dokken/verifier_sandbox/#{instance_name}"
|
|
283
465
|
end
|
|
284
466
|
|
|
467
|
+
# The entries staged in the verifier sandbox, for upload.
|
|
468
|
+
#
|
|
469
|
+
# @return [Array<String>] absolute paths
|
|
285
470
|
def sandbox_dirs
|
|
286
471
|
Dir.glob(File.join(sandbox_path, "*"))
|
|
287
472
|
end
|
|
288
473
|
|
|
474
|
+
# Create the verifier sandbox directory if it is missing.
|
|
475
|
+
#
|
|
476
|
+
# @return [void]
|
|
289
477
|
def create_sandbox
|
|
290
478
|
info("Creating kitchen sandbox in #{sandbox_path}")
|
|
291
479
|
unless ::Dir.exist?(sandbox_path)
|
|
@@ -293,25 +481,45 @@ module Dokken
|
|
|
293
481
|
end
|
|
294
482
|
end
|
|
295
483
|
|
|
484
|
+
# The path the kitchen sandbox is mounted at inside the container.
|
|
485
|
+
#
|
|
486
|
+
# @return [String] an absolute path inside the container
|
|
296
487
|
def resolved_root_path
|
|
297
488
|
instance.provisioner[:root_path] || "/opt/kitchen"
|
|
298
489
|
end
|
|
299
490
|
end
|
|
300
491
|
end
|
|
301
492
|
|
|
493
|
+
# Redirect the stock Test Kitchen sandboxes into ~/.dokken.
|
|
494
|
+
#
|
|
495
|
+
# Every dokken container bind-mounts (or rsyncs) these directories, so they
|
|
496
|
+
# have to live at a stable, per-instance path that the docker daemon can see
|
|
497
|
+
# -- not in the random tmpdir the base classes would otherwise use.
|
|
302
498
|
module Kitchen
|
|
303
499
|
module Provisioner
|
|
500
|
+
# @see Dokken::Helpers
|
|
304
501
|
class Base
|
|
502
|
+
# Create the kitchen sandbox under ~/.dokken.
|
|
503
|
+
#
|
|
504
|
+
# @return [void]
|
|
305
505
|
def create_sandbox
|
|
306
506
|
info("Creating kitchen sandbox in #{sandbox_path}")
|
|
307
507
|
FileUtils.mkdir_p(sandbox_path, mode: 0o755)
|
|
308
508
|
end
|
|
309
509
|
|
|
510
|
+
# Where the provisioner stages files for this instance.
|
|
511
|
+
#
|
|
310
512
|
# this MUST be named 'sandbox_path' because ruby.
|
|
513
|
+
#
|
|
514
|
+
# @return [String] an absolute path on the host
|
|
311
515
|
def sandbox_path
|
|
312
516
|
"#{Dir.home}/.dokken/kitchen_sandbox/#{instance_name}"
|
|
313
517
|
end
|
|
314
518
|
|
|
519
|
+
# A container-safe, collision-free name for this kitchen instance.
|
|
520
|
+
#
|
|
521
|
+
# @return [String] the instance name
|
|
522
|
+
# @see Dokken::Helpers#instance_name
|
|
315
523
|
def instance_name
|
|
316
524
|
prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
|
|
317
525
|
"#{prefix}-#{instance.name}".downcase
|
|
@@ -320,9 +528,16 @@ module Kitchen
|
|
|
320
528
|
end
|
|
321
529
|
end
|
|
322
530
|
|
|
531
|
+
# Redirect the stock verifier sandbox into ~/.dokken, and teach the verifier
|
|
532
|
+
# to upload it when the daemon cannot read the host filesystem.
|
|
323
533
|
module Kitchen
|
|
534
|
+
# @see Kitchen::Verifier::Base
|
|
324
535
|
module Verifier
|
|
536
|
+
# @see Dokken::Helpers
|
|
325
537
|
class Base
|
|
538
|
+
# Create the verifier sandbox under ~/.dokken.
|
|
539
|
+
#
|
|
540
|
+
# @return [void]
|
|
326
541
|
def create_sandbox
|
|
327
542
|
info("Creating kitchen sandbox in #{sandbox_path}")
|
|
328
543
|
unless ::Dir.exist?(sandbox_path)
|
|
@@ -330,15 +545,30 @@ module Kitchen
|
|
|
330
545
|
end
|
|
331
546
|
end
|
|
332
547
|
|
|
548
|
+
# Where the verifier stages files for this instance.
|
|
549
|
+
#
|
|
550
|
+
# @return [String] an absolute path on the host
|
|
333
551
|
def sandbox_path
|
|
334
552
|
"#{Dir.home}/.dokken/verifier_sandbox/#{instance_name}"
|
|
335
553
|
end
|
|
336
554
|
|
|
555
|
+
# A container-safe, collision-free name for this kitchen instance.
|
|
556
|
+
#
|
|
557
|
+
# @return [String] the instance name
|
|
558
|
+
# @see Dokken::Helpers#instance_name
|
|
337
559
|
def instance_name
|
|
338
560
|
prefix = (Digest::SHA2.hexdigest FileUtils.pwd)[0, 10]
|
|
339
561
|
"#{prefix}-#{instance.name}".downcase
|
|
340
562
|
end
|
|
341
563
|
|
|
564
|
+
# Run the verifier against the instance.
|
|
565
|
+
#
|
|
566
|
+
# Files are only uploaded when the driver built a data container, which
|
|
567
|
+
# it does exactly when the daemon cannot read the host filesystem.
|
|
568
|
+
#
|
|
569
|
+
# @param state [Hash] mutable instance state
|
|
570
|
+
# @return [void]
|
|
571
|
+
# @raise [Kitchen::ActionFailed] if the transport fails
|
|
342
572
|
def call(state)
|
|
343
573
|
create_sandbox
|
|
344
574
|
instance.transport.connection(state) do |conn|
|
|
@@ -22,7 +22,15 @@ require_relative "../helpers"
|
|
|
22
22
|
include Dokken::Helpers
|
|
23
23
|
|
|
24
24
|
module Kitchen
|
|
25
|
+
# @see Kitchen::Provisioner::Dokken
|
|
25
26
|
module Provisioner
|
|
27
|
+
# Provisioner for the dokken driver.
|
|
28
|
+
#
|
|
29
|
+
# Unlike its ChefInfra parent this provisioner never installs chef: the
|
|
30
|
+
# driver mounts /opt/chef into the runner from a volume container built
|
|
31
|
+
# from the requested image, so all that is left to do here is stage the
|
|
32
|
+
# sandbox and run the client.
|
|
33
|
+
#
|
|
26
34
|
# @author Sean OMeara <sean@sean.io>
|
|
27
35
|
class Dokken < Kitchen::Provisioner::ChefInfra
|
|
28
36
|
kitchen_provisioner_api_version 2
|
|
@@ -63,6 +71,10 @@ module Kitchen
|
|
|
63
71
|
default_config :clean_dokken_sandbox, true
|
|
64
72
|
|
|
65
73
|
# (see Base#call)
|
|
74
|
+
#
|
|
75
|
+
# @param state [Hash] mutable instance state
|
|
76
|
+
# @return [void]
|
|
77
|
+
# @raise [Kitchen::ActionFailed] if the transport fails
|
|
66
78
|
def call(state)
|
|
67
79
|
create_sandbox
|
|
68
80
|
write_run_command(run_command)
|
|
@@ -98,6 +110,9 @@ module Kitchen
|
|
|
98
110
|
# super in that case so the parent's own check_license runs. We test
|
|
99
111
|
# for both because a partial drop-in shim may define
|
|
100
112
|
# license_acceptance_id without pulling in license-acceptance.
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
# @raise [LicenseAcceptance::LicenseNotAcceptedError] if declined
|
|
101
116
|
def check_license
|
|
102
117
|
return super unless respond_to?(:license_acceptance_id, true) && defined?(LicenseAcceptance)
|
|
103
118
|
|
|
@@ -119,11 +134,17 @@ module Kitchen
|
|
|
119
134
|
config[:chef_license] ||= acceptor.acceptance_value
|
|
120
135
|
end
|
|
121
136
|
|
|
137
|
+
# Normalise the chef command-line settings before they are assembled.
|
|
138
|
+
#
|
|
139
|
+
# @return [void]
|
|
122
140
|
def validate_config
|
|
123
141
|
# check if we have an space for the user provided options
|
|
124
142
|
# or add it if not to avoid issues
|
|
143
|
+
# Assign rather than String#prepend: config values can be frozen
|
|
144
|
+
# literals, and mutating them in place edits the user's kitchen.yml
|
|
145
|
+
# data for the rest of the run.
|
|
125
146
|
unless config[:chef_options].start_with? " "
|
|
126
|
-
config[:chef_options]
|
|
147
|
+
config[:chef_options] = " #{config[:chef_options]}"
|
|
127
148
|
end
|
|
128
149
|
|
|
129
150
|
# strip spaces from all other options
|
|
@@ -139,29 +160,57 @@ module Kitchen
|
|
|
139
160
|
|
|
140
161
|
private
|
|
141
162
|
|
|
163
|
+
# The chef-client invocation staged into the sandbox as `run_command`.
|
|
164
|
+
#
|
|
142
165
|
# patching Kitchen::Provisioner::ChefInfra#run_command
|
|
166
|
+
#
|
|
167
|
+
# @return [String] a shell command line
|
|
168
|
+
# @api private
|
|
143
169
|
def run_command
|
|
144
170
|
validate_config
|
|
145
|
-
|
|
171
|
+
|
|
172
|
+
# Build into a fresh String: appending straight onto
|
|
173
|
+
# config[:chef_binary] rewrote the configured binary path, so asking
|
|
174
|
+
# for the command twice returned the arguments twice over.
|
|
175
|
+
#
|
|
176
|
+
# :profile_ruby and :slow_resource_report are deliberately not handled
|
|
177
|
+
# here -- ChefInfra#chef_args already appends both, and adding them a
|
|
178
|
+
# second time produced a duplicated (and unseparated) flag.
|
|
179
|
+
cmd = +""
|
|
180
|
+
cmd << config[:chef_binary]
|
|
146
181
|
cmd << config[:chef_options].to_s
|
|
147
182
|
cmd << " -l #{config[:chef_log_level]}"
|
|
148
183
|
cmd << " -F #{config[:chef_output_format]}"
|
|
149
184
|
cmd << " -c #{File.join(config[:root_path], "client.rb")}"
|
|
150
185
|
cmd << " -j #{File.join(config[:root_path], "dna.json")}"
|
|
151
|
-
cmd << "--profile-ruby" if config[:profile_ruby]
|
|
152
|
-
cmd << "--slow-report" if config[:slow_resource_report]
|
|
153
186
|
|
|
154
187
|
chef_cmd(cmd)
|
|
155
188
|
end
|
|
156
189
|
|
|
190
|
+
# Stage the converge command in the kitchen sandbox.
|
|
191
|
+
#
|
|
192
|
+
# Writing a script and running `sh run_command` keeps the command line
|
|
193
|
+
# short enough for `docker exec`, which the full chef invocation is not.
|
|
194
|
+
#
|
|
195
|
+
# @param command [String] the shell command line to stage
|
|
196
|
+
# @return [void]
|
|
197
|
+
# @api private
|
|
157
198
|
def write_run_command(command)
|
|
158
199
|
File.write("#{dokken_kitchen_sandbox}/run_command", command, mode: "wb")
|
|
159
200
|
end
|
|
160
201
|
|
|
202
|
+
# The container the converge runs in.
|
|
203
|
+
#
|
|
204
|
+
# @return [String] the container name
|
|
205
|
+
# @api private
|
|
161
206
|
def runner_container_name
|
|
162
207
|
instance.name.to_s
|
|
163
208
|
end
|
|
164
209
|
|
|
210
|
+
# Empty the sandbox after a converge, keeping the directory itself.
|
|
211
|
+
#
|
|
212
|
+
# @return [void]
|
|
213
|
+
# @api private
|
|
165
214
|
def cleanup_dokken_sandbox
|
|
166
215
|
return if sandbox_path.nil?
|
|
167
216
|
|