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.
@@ -18,6 +18,6 @@
18
18
  module Kitchen
19
19
  module Driver
20
20
  # Version string for Dokken Kitchen driver
21
- DOKKEN_VERSION = "2.23.2".freeze
21
+ DOKKEN_VERSION = "2.23.4".freeze
22
22
  end
23
23
  end
@@ -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
- begin
10
- Timeout.timeout(1) do
11
- s = TCPSocket.new(ip, port)
12
- s.close
13
- return true
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
- @docker_info ||= ::Docker.info
124
- rescue Excon::Error::Socket
125
- puts "kitchen-dokken could not connect to the docker host at #{default_docker_host}. Is docker running?"
126
- exit!
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
- begin
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
- begin
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.include?("-")
246
- port_range = container_port.split("-")
247
- port_range.map!(&:to_i)
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
- return false if config[:docker_info]["OperatingSystem"].include?("Docker Desktop")
264
- return false if config[:docker_info]["OperatingSystem"].include?("Boot2Docker")
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].prepend(" ")
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
- cmd = config[:chef_binary]
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