kitchen-docker 3.3.4 → 3.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be40fecdd59251c4d7b1a20de831bd939fd7018ce19d19a2a2f205ac2464f4fb
4
- data.tar.gz: 04b4579a2569e8be9f943d77eca1d4fd9f5e72e2aefc2ad3009855bf1e565935
3
+ metadata.gz: 2d994f876f76b4950f4f85521fba2ed15fe2ed54d06b9993d464a578f2fd9c69
4
+ data.tar.gz: d942290a8f115d5b0937421b747ec3f413e180fd55cfc8af245524b8337b27d5
5
5
  SHA512:
6
- metadata.gz: d14d1fcddd8c823cc51231351311d2130bb9e9dd8355bb09e0551635a982a38b8abc52fca32cb03b5813d484bc256a688574ad8da6ef44a3d20525bdeac8fb21
7
- data.tar.gz: 93f7faf2b089f727c6c78808b6c3e4d3fc9234e22a4defa21a0b463b61992324d8ecd26fb5bb9bfbc7da415d00234c67815d7bc20e889c563da50324c76c1040
6
+ metadata.gz: 7db4fee864798aa758a3e253f433db4e120d7247e8b5125e34764cf393f4836c9885eb3df2eff767501a6910c2f895b4efab467b8a24f7e1ad6120cc58eeede2
7
+ data.tar.gz: f80f60d52168ea008378af1cb66530136bf1e53b6474b75877a9275ff336f0ae632f754833f3e3db7aadd2c0f3910d64269fd39be0bb3a90baf5367d6708f115
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "3.3.4"
2
+ ".": "3.4.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ Future CHANGELOG notes will be in GitHub release notes
6
6
 
7
7
  * Docs: document the last four options and split contributor docs ([#463](https://github.com/test-kitchen/kitchen-docker/pull/463)) ([4818bce](https://github.com/test-kitchen/kitchen-docker/commit/4818bce))
8
8
 
9
+ ## [3.4.1](https://github.com/test-kitchen/kitchen-docker/compare/v3.4.0...v3.4.1) (2026-08-30)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * quote container paths interpolated into docker exec command lines ([#496](https://github.com/test-kitchen/kitchen-docker/issues/496)) ([ae3a754](https://github.com/test-kitchen/kitchen-docker/commit/ae3a754046cdb7c6d9fdb8622a6cef818643ad56))
15
+ * read the image id from a quiet build ([#489](https://github.com/test-kitchen/kitchen-docker/issues/489)) ([ddc58b5](https://github.com/test-kitchen/kitchen-docker/commit/ddc58b50a16b7236ed047071228f5641092060a1))
16
+ * report this gem's own version to kitchen diagnose ([#490](https://github.com/test-kitchen/kitchen-docker/issues/490)) ([e28171d](https://github.com/test-kitchen/kitchen-docker/commit/e28171d268ab0a4950c0d553fcee12688663e626))
17
+
18
+ ## [3.4.0](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.5...v3.4.0) (2026-08-23)
19
+
20
+
21
+ ### Features
22
+
23
+ * implement the driver's package, doctor, and status hooks ([#491](https://github.com/test-kitchen/kitchen-docker/issues/491)) ([1366895](https://github.com/test-kitchen/kitchen-docker/commit/13668957b5b802f91c30aa7e34fc1b9f761e3b8d))
24
+
25
+ ## [3.3.5](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.4...v3.3.5) (2026-08-23)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * report when docker cp silently writes nothing ([#487](https://github.com/test-kitchen/kitchen-docker/issues/487)) ([1bc62ba](https://github.com/test-kitchen/kitchen-docker/commit/1bc62ba615d25d402a97775dbf6ac252af5cbdd8))
31
+ * require test-kitchen 3.0 or newer ([#492](https://github.com/test-kitchen/kitchen-docker/issues/492)) ([0c64225](https://github.com/test-kitchen/kitchen-docker/commit/0c64225dd22aca8a2a32e6f61a6d648bcd8c0b2b))
32
+ * run every docker command through sudo when use_sudo is set ([#485](https://github.com/test-kitchen/kitchen-docker/issues/485)) ([2481aad](https://github.com/test-kitchen/kitchen-docker/commit/2481aadfc6415e5b7ed597e1095d0fe8db89afbb))
33
+ * stop printing the image inspect JSON on destroy ([#488](https://github.com/test-kitchen/kitchen-docker/issues/488)) ([0011558](https://github.com/test-kitchen/kitchen-docker/commit/00115588236900d9ef3275a73892f9d3bdb4e3c8))
34
+
9
35
  ## [3.3.4](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.3...v3.3.4) (2026-08-23)
10
36
 
11
37
 
data/README.md CHANGED
@@ -28,6 +28,7 @@ machine to boot.
28
28
  * [Driver configuration](#driver-configuration)
29
29
  * [Transport configuration](#transport-configuration)
30
30
  * [Logging into a container](#logging-into-a-container)
31
+ * [Other commands](#other-commands)
31
32
  * [Examples](#examples)
32
33
  * [Using with Chef](#using-with-chef)
33
34
  * [Troubleshooting](#troubleshooting)
@@ -204,9 +205,10 @@ platforms:
204
205
  | `dockerfile` | *(none)* | Path to your own Dockerfile, used instead of the generated one. Rendered as [ERB](#using-a-custom-dockerfile). |
205
206
  | `build_context` | `true` locally, `false` for a remote daemon | Send the working directory to the daemon as build context. Required for `ADD` and `COPY`; slow against a remote daemon. |
206
207
  | `build_options` | *(none)* | Extra flags for `docker build`, as a string or a map. |
207
- | `build_tempdir` | working directory | Where the generated Dockerfile is written, relative to `build_context`. |
208
+ | `build_tempdir` | working directory | Directory the generated Dockerfile is written to. A relative path is resolved against the working directory, and the path must be inside the build context, since `docker build -f` is given it relative to the working directory. |
208
209
  | `use_cache` | `true` | Use Docker's build cache. `false` adds `--no-cache`. |
209
210
  | `remove_images` | `false` | Remove the built image on `kitchen destroy`. |
211
+ | `package_name` | the instance name, tagged `latest` | Image `kitchen package` commits the container to. |
210
212
  | `docker_platform` | *(none)* | Target architecture, passed as `--platform` to both build and run — e.g. `linux/arm64`. |
211
213
 
212
214
  ### Provisioning the image
@@ -235,7 +237,9 @@ platforms:
235
237
  | `tty` | `false` | Pass `-t`, allocating a pseudo-TTY. |
236
238
  | `env_variables` | *(none)* | Environment variables set in the container, as a map. |
237
239
  | `wait_for_transport` | `true` | Wait for the transport to answer before converging. Set `false` for containers that do not stay up. |
238
- | `detach` | `false` | Run provisioner commands with `docker exec -d`, returning immediately instead of waiting for them. The container itself is always started detached, regardless of this setting, and `kitchen login` ignores it so the shell stays usable. |
240
+
241
+ The container itself is always started detached — see [`detach`](#transport-configuration)
242
+ under the transport for how provisioner commands are run.
239
243
 
240
244
  ### Networking
241
245
 
@@ -284,6 +288,7 @@ Each of these accepts a single value or a list.
284
288
  | `binary` | `docker` | Docker CLI to invoke — e.g. `docker.io`, or an absolute path. |
285
289
  | `socket` | `$DOCKER_HOST`, else `unix:///var/run/docker.sock` (`npipe:////./pipe/docker_engine` on Windows) | Daemon to talk to. A `tcp://` value also supplies the host used for SSH to the container. |
286
290
  | `use_sudo` | `false` | Run every `docker` command through `sudo`. |
291
+ | `sudo_command` | `sudo -E` | The command `use_sudo` prefixes, for hosts that use something else (`doas`, say). |
287
292
  | `tls` | `false` | Use TLS when connecting. |
288
293
  | `tls_verify` | `false` | Verify the daemon's certificate. |
289
294
  | `tls_cacert` | *(none)* | Path to the CA certificate. |
@@ -311,14 +316,17 @@ These options go under `transport:`, not `driver:`.
311
316
  | `working_dir` | *(none)* | Working directory inside the container (`-w`). |
312
317
  | `temp_dir` | `/tmp`, or `$env:TEMP` on Windows | Directory used to stage uploaded files. |
313
318
  | `env_variables` | *(none)* | Environment variables for each command. |
319
+ | `detach` | `false` | Run provisioner commands with `docker exec -d`, returning immediately instead of waiting for them to finish. `kitchen login` ignores it, so the shell stays usable. |
314
320
  | `privileged` | `false` | Run commands with `--privileged`. |
315
321
  | `interactive` | `false` | Pass `-i`. |
316
322
  | `tty` | `false` | Pass `-t`. |
323
+ | `use_sudo` | `false` | Run every `docker` command through `sudo`. |
324
+ | `sudo_command` | `sudo -E` | The command `use_sudo` prefixes. |
317
325
  | `tls`, `tls_verify`, `tls_cacert`, `tls_cert`, `tls_key` | as for the driver | TLS settings for the daemon connection. |
318
326
 
319
327
  The driver and transport each read their own copy of `binary`, `socket`,
320
- `username`, and the TLS settings. If you point one at a non-default daemon,
321
- point the other at it too.
328
+ `username`, `use_sudo`, and the TLS settings. If you point one at a non-default
329
+ daemon, or need `sudo` to reach it, configure the other the same way.
322
330
 
323
331
  ## Logging into a container
324
332
 
@@ -334,6 +342,68 @@ starts `powershell`. The transport's `username`, `working_dir`,
334
342
  `env_variables`, and `privileged` settings are honoured, so the shell matches
335
343
  the environment the provisioner ran in.
336
344
 
345
+ ## Other commands
346
+
347
+ ### `kitchen package`
348
+
349
+ Commits the container to a Docker image, so a converged instance can be kept
350
+ and reused:
351
+
352
+ ```sh
353
+ kitchen package default-ubuntu-2404
354
+ ```
355
+
356
+ ```text
357
+ -----> Packaging remote instance
358
+ [Docker] Packaged default-ubuntu-2404 as default-ubuntu-2404:latest (sha256:1f51c590...)
359
+ ```
360
+
361
+ The image is named after the instance. Set `package_name` for something else:
362
+
363
+ ```yaml
364
+ driver:
365
+ name: docker
366
+ package_name: myapp/under-test:candidate
367
+ ```
368
+
369
+ Run `docker save` against the result if you want a tarball.
370
+
371
+ ### `kitchen doctor`
372
+
373
+ Checks that the daemon is reachable and that the configuration points at
374
+ things that exist:
375
+
376
+ ```sh
377
+ kitchen doctor default-ubuntu-2404
378
+ ```
379
+
380
+ ```text
381
+ -----> The doctor is in
382
+ Docker daemon at unix:///var/run/docker.sock is reachable, running 29.7.2.
383
+ ```
384
+
385
+ It reports every problem it finds rather than stopping at the first, and exits
386
+ non-zero when there is one: a daemon it cannot reach, a `tls_cacert`,
387
+ `tls_cert`, `tls_key`, or `dockerfile` naming a path that is not there, or a
388
+ state file naming a container the daemon no longer has.
389
+
390
+ ### `kitchen list --live`
391
+
392
+ Asks Docker what state each container is actually in, rather than reporting
393
+ only the last action Test Kitchen took:
394
+
395
+ ```sh
396
+ kitchen list --live
397
+ ```
398
+
399
+ ```text
400
+ Instance Driver Provisioner Verifier Transport Last Action Last Error Live Status
401
+ default-ubuntu-2404 Docker Shell Dummy Docker Created <None> running
402
+ ```
403
+
404
+ `running`, `stopped`, `gone` (the state file names a container the daemon does
405
+ not have), or `not created`.
406
+
337
407
  ## Examples
338
408
 
339
409
  ### Testing a systemd service
@@ -534,7 +604,8 @@ bootstrap. Set it to a supported family, or supply your own
534
604
  to `false` against a remote daemon.
535
605
 
536
606
  **Permission denied talking to the daemon.** Either add your user to the
537
- `docker` group, or set `use_sudo: true`.
607
+ `docker` group, or set `use_sudo: true` under **both** `driver:` and
608
+ `transport:` -- the transport runs its own `docker exec` and `docker cp`.
538
609
 
539
610
  **Anything else.** Run with `-l debug`:
540
611
 
@@ -16,5 +16,5 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency "test-kitchen", ">= 1.0.0", "< 5.0"
19
+ spec.add_dependency "test-kitchen", ">= 3.0", "< 5.0"
20
20
  end
@@ -84,7 +84,10 @@ module Kitchen
84
84
  debug("Executing temp script #{remote_path}/#{filename} on container")
85
85
  remote_path = replace_env_variables(@config, remote_path)
86
86
 
87
- container_exec(@config, "/bin/bash #{remote_path}/#{filename}")
87
+ # Escaped because the exec command line is assembled as one string:
88
+ # a temp_dir with a space in it was split before docker saw it, and
89
+ # bash was handed the first half of the directory as the script.
90
+ container_exec(@config, "/bin/bash #{Shellwords.escape("#{remote_path}/#{filename}")}")
88
91
  rescue => e
89
92
  raise "Failed to execute command on Linux container. #{e}"
90
93
  ensure
@@ -69,7 +69,11 @@ module Kitchen
69
69
  # Replace any environment variables used in the path and execute script file
70
70
  debug("Executing temp script #{remote_path}\\#{filename} on container")
71
71
  remote_path = replace_env_variables(@config, remote_path)
72
- cmd = build_powershell_command("-File #{remote_path}\\#{filename}")
72
+ # Quoted because PowerShell's -File takes exactly one argument. An
73
+ # unquoted path with a space in it -- which $env:TEMP has whenever the
74
+ # user's name does -- left PowerShell looking for a script named
75
+ # after the first word of the directory.
76
+ cmd = build_powershell_command(%{-File "#{remote_path}\\#{filename}"})
73
77
 
74
78
  container_exec(@config, cmd)
75
79
  rescue => e
@@ -18,6 +18,6 @@ module Kitchen
18
18
  # Everything belonging to the kitchen-docker plugin.
19
19
  module Docker
20
20
  # The version of the kitchen-docker gem.
21
- DOCKER_VERSION = "3.3.4".freeze
21
+ DOCKER_VERSION = "3.4.1".freeze
22
22
  end
23
23
  end
@@ -64,11 +64,39 @@ module Kitchen
64
64
  docker << " --tlscacert=#{shell_escape(config[:tls_cacert])}" if config[:tls_cacert]
65
65
  docker << " --tlscert=#{shell_escape(config[:tls_cert])}" if config[:tls_cert]
66
66
  docker << " --tlskey=#{shell_escape(config[:tls_key])}" if config[:tls_key]
67
+ options = docker_sudo_opts(options)
67
68
  logger.debug("docker_command: #{docker} #{cmd} shell_opts: #{docker_shell_opts(options)}")
68
69
  run_command("#{docker} #{cmd}", docker_shell_opts(options))
69
70
  end
70
71
  # rubocop:enable Metrics/AbcSize
71
72
 
73
+ # Adds the sudo options {#run_command} reads, when +use_sudo+ is set.
74
+ #
75
+ # Sudo is a property of the call rather than of the configuration as far
76
+ # as the shell-out layer is concerned: it reads +:use_sudo+ from the
77
+ # options hash it is handed and knows nothing about +config+. So a
78
+ # docker command only runs through sudo if these are passed to it.
79
+ #
80
+ # Without this, +use_sudo+ reached exactly one command -- the
81
+ # `docker` probe in +verify_dependencies+ -- and every build, run,
82
+ # exec, cp, and rm still ran as the invoking user. On a host where the
83
+ # daemon socket needs root, that made the documented answer to
84
+ # "permission denied while trying to connect to the Docker daemon
85
+ # socket" do nothing at all.
86
+ #
87
+ # A copy is returned rather than the hash being edited in place, so a
88
+ # caller that reuses its options hash does not accumulate sudo.
89
+ #
90
+ # @param options [Hash] shell-out options
91
+ # @return [Hash] those options, with sudo added when configured
92
+ def docker_sudo_opts(options = {})
93
+ return options unless config[:use_sudo]
94
+
95
+ options = options.merge(use_sudo: true)
96
+ options[:sudo_command] = config[:sudo_command] if config[:sudo_command]
97
+ options
98
+ end
99
+
72
100
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
73
101
 
74
102
  # Runs a shell command, returning stderr as well as stdout.
@@ -79,7 +107,7 @@ module Kitchen
79
107
  # @param cmd [String] the command to run
80
108
  # @param options [Hash] shell-out options
81
109
  # @return [String] combined stdout and stderr
82
- # @raise [Kitchen::ShellCommandFailed] if the command exits non-zero
110
+ # @raise [Kitchen::ShellOut::ShellCommandFailed] if the command exits non-zero
83
111
  def run_command(cmd, options = {})
84
112
  if options.fetch(:use_sudo, false)
85
113
  cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
@@ -33,6 +33,11 @@ module Kitchen
33
33
  include Configurable
34
34
  include Kitchen::Docker::Helpers::CliHelper
35
35
 
36
+ # Printed by the probe in {#file_on_container?} when the file is there.
37
+ # A marker is echoed rather than the exit status being read, because a
38
+ # non-zero exit from `docker exec` is raised rather than returned.
39
+ COPIED_MARKER = "kitchen_docker_copied".freeze
40
+
36
41
  # Pulls the container id out of `docker run` output.
37
42
  #
38
43
  # Docker prints ids in short (12) or full (64) hex form, on a line of
@@ -150,6 +155,17 @@ module Kitchen
150
155
 
151
156
  # Creates a directory inside the container, on Linux or Windows.
152
157
  #
158
+ # The path is escaped for the shell, because it comes from the
159
+ # transport's +temp_dir+ and a `docker exec` command line is assembled
160
+ # as one string. A directory with a space in it was torn in two before
161
+ # docker ever saw it, and `mkdir -p` obligingly created both halves --
162
+ # neither of them the directory that was asked for. Every upload that
163
+ # followed then went to a path that did not exist.
164
+ #
165
+ # The PowerShell branch already quotes the path itself, and its
166
+ # argument is reassembled by PowerShell rather than split by a shell,
167
+ # so it is left as it is.
168
+ #
153
169
  # @param state [Hash] instance state naming the container
154
170
  # @param path [String] the directory to create; environment variable
155
171
  # references are expanded first
@@ -157,7 +173,7 @@ module Kitchen
157
173
  # @raise [RuntimeError] if the directory cannot be created
158
174
  def create_dir_on_container(state, path)
159
175
  path = replace_env_variables(state, path)
160
- cmd = "mkdir -p #{path}"
176
+ cmd = "mkdir -p #{Shellwords.escape(path)}"
161
177
 
162
178
  if state[:platform].include?("windows")
163
179
  psh = "-Command if(-not (Test-Path '#{path}')) { New-Item -Path '#{path}' -Force }"
@@ -172,23 +188,84 @@ module Kitchen
172
188
 
173
189
  # Copies a local file into the container.
174
190
  #
191
+ # The copy is checked afterwards, because `docker cp` cannot write into
192
+ # a mount and does not say so. A destination under a tmpfs or a volume
193
+ # is written to the container's own filesystem layer, which the mount
194
+ # then hides, and docker exits 0 with no output -- so the copy looks
195
+ # like it worked and the file is simply not there.
196
+ #
197
+ # Left unchecked, that surfaces later and somewhere else. Running with
198
+ # `tmpfs: /tmp`, which is how the Docker documentation suggests running
199
+ # systemd, the first sign is the next command failing with
200
+ # `/bin/bash: /tmp/docker-<uuid>.sh: No such file or directory` (#387),
201
+ # which names neither the copy nor the mount.
202
+ #
175
203
  # @param state [Hash] instance state naming the container
176
204
  # @param local_file [String] source path
177
205
  # @param remote_file [String] destination path inside the container
178
206
  # @return [String] the command's combined output
179
- # @raise [RuntimeError] if the copy fails
207
+ # @raise [RuntimeError] if the copy fails, or if it silently wrote
208
+ # nothing
180
209
  def copy_file_to_container(state, local_file, remote_file)
181
210
  debug("Copying local file #{local_file} to #{remote_file} on container")
182
211
 
183
212
  remote_file = replace_env_variables(state, remote_file)
184
213
 
185
- remote_file = "#{state[:container_id]}:#{remote_file}"
186
- cmd = build_copy_command(local_file, remote_file)
187
- docker_command(cmd)
214
+ cmd = build_copy_command(local_file, "#{state[:container_id]}:#{remote_file}")
215
+ output = docker_command(cmd)
216
+ verify_file_copied(state, local_file, remote_file)
217
+ output
188
218
  rescue => e
189
219
  raise "Failed to copy file #{local_file} to container. #{e}"
190
220
  end
191
221
 
222
+ # Checks that a copied file arrived, and says why if it did not.
223
+ #
224
+ # Only Linux containers are checked. tmpfs mounts are a Linux container
225
+ # feature, and `docker cp` against a Windows container is a different
226
+ # code path in Docker that this cannot be tried against, so those keep
227
+ # the behaviour they have always had.
228
+ #
229
+ # @param state [Hash] instance state naming the container
230
+ # @param local_file [String] the source that was copied
231
+ # @param remote_file [String] the destination it was copied to
232
+ # @return [void]
233
+ # @raise [Kitchen::ActionFailed] if the file is not there
234
+ def verify_file_copied(state, local_file, remote_file)
235
+ return if state[:platform].to_s.include?("windows")
236
+ return if file_on_container?(state, remote_file, ::File.basename(local_file))
237
+
238
+ raise ActionFailed,
239
+ "docker reported no error copying it to #{remote_file}, but the file is " \
240
+ "not there. `docker cp` cannot write into a mount -- if #{remote_file} is " \
241
+ "a tmpfs or a volume, set the transport's temp_dir and the provisioner's " \
242
+ "root_path to a path that is not."
243
+ end
244
+
245
+ # Whether a `docker cp` destination now holds the file that was copied.
246
+ #
247
+ # `docker cp SRC CONTAINER:DEST` copies into DEST when DEST is a
248
+ # directory and to DEST otherwise. Which of those happened is only known
249
+ # inside the container, so the choice is made there, in the one command,
250
+ # rather than by asking twice from here.
251
+ #
252
+ # The two paths are passed as arguments to `sh` rather than interpolated
253
+ # into the script, so that nothing in either is read as shell syntax.
254
+ #
255
+ # @param state [Hash] instance state naming the container
256
+ # @param remote_file [String] the destination that was copied to
257
+ # @param basename [String] the source's file name
258
+ # @return [Boolean] whether the file is there
259
+ def file_on_container?(state, remote_file, basename)
260
+ script = 'p="$1"; if [ -d "$p" ]; then p="$p/$2"; fi; ' \
261
+ "if [ -e \"$p\" ]; then echo #{COPIED_MARKER}; fi"
262
+ probe = "/bin/sh -c #{Shellwords.escape(script)} sh " \
263
+ "#{Shellwords.escape(remote_file)} #{Shellwords.escape(basename)}"
264
+
265
+ output = docker_command(build_exec_command(state, probe), suppress_output: !logger.debug?)
266
+ output.include?(COPIED_MARKER)
267
+ end
268
+
192
269
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
193
270
 
194
271
  # Reads the container's environment.
@@ -27,6 +27,9 @@ module Kitchen
27
27
  include Kitchen::Docker::Helpers::CliHelper
28
28
  include Kitchen::Docker::Helpers::ContainerHelper
29
29
 
30
+ # An id on a line of its own, which is all `docker build -q` prints.
31
+ QUIET_BUILD_IMAGE_ID = /\A(sha256:[[:xdigit:]]{64})\z/
32
+
30
33
  # Pulls the built image's id out of `docker build` output.
31
34
  #
32
35
  # Scanned in reverse, and against several patterns, because the wording has
@@ -37,6 +40,13 @@ module Kitchen
37
40
  # @raise [Kitchen::ActionFailed] if no id could be found
38
41
  def parse_image_id(output)
39
42
  output.split("\n").reverse_each do |line|
43
+ line = line.strip
44
+ # `docker build -q` prints the id and nothing else -- none of the
45
+ # wording below appears -- so a build with `build_options: -q` had
46
+ # no line any of these matched and failed with "Could not parse
47
+ # Docker build output for image ID" (#225).
48
+ return Regexp.last_match(1) if line.match(QUIET_BUILD_IMAGE_ID)
49
+
40
50
  if line =~ /writing image (sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i
41
51
  img_id = line[/writing image (sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i, 1]
42
52
  return img_id
@@ -126,10 +136,28 @@ module Kitchen
126
136
  parse_image_id(output)
127
137
  end
128
138
 
139
+ # Whether the image named in state is present locally.
140
+ #
141
+ # The inspect is silenced, as every other predicate that shells out to
142
+ # docker is. Left speaking, `kitchen destroy` on an instance with
143
+ # +remove_images+ set printed the image's entire `docker inspect` JSON
144
+ # -- config, every layer digest, metadata -- into the middle of the
145
+ # destroy output, between the container being removed and the image
146
+ # being removed. Nothing read it: only whether the command succeeded is
147
+ # used.
148
+ #
149
+ # It is still printed under `-l debug`, where the rest of the driver's
150
+ # docker traffic is.
151
+ #
129
152
  # @param state [Hash] instance state naming the image
130
153
  # @return [Boolean] whether the image is present locally
131
154
  def image_exists?(state)
132
- state[:image_id] && !!docker_command("inspect --type=image #{state[:image_id]}") rescue false
155
+ return false unless state[:image_id]
156
+
157
+ !!docker_command("inspect --type=image #{state[:image_id]}",
158
+ suppress_output: !logger.debug?)
159
+ rescue
160
+ false
133
161
  end
134
162
  end
135
163
  end
@@ -17,11 +17,13 @@ require "kitchen"
17
17
  require "json" unless defined?(JSON)
18
18
  require "securerandom" unless defined?(SecureRandom)
19
19
  require "net/ssh" unless defined?(Net::SSH)
20
+ require "time" unless defined?(Time)
20
21
 
21
22
  require "kitchen/driver/base"
22
23
 
23
24
  require_relative "../docker/container/linux"
24
25
  require_relative "../docker/container/windows"
26
+ require_relative "../docker/docker_version"
25
27
  require_relative "../docker/helpers/cli_helper"
26
28
  require_relative "../docker/helpers/container_helper"
27
29
 
@@ -36,6 +38,13 @@ module Kitchen
36
38
  include Kitchen::Docker::Helpers::ContainerHelper
37
39
  include ShellOut
38
40
 
41
+ # Reported by `kitchen diagnose`, which is what a bug report is asked to
42
+ # include. The version is this gem's own: the transport used to report
43
+ # Kitchen::VERSION, which is Test Kitchen's, so a diagnose said the
44
+ # plugin was at whatever version Test Kitchen happened to be.
45
+ kitchen_driver_api_version 2
46
+ plugin_version Kitchen::Docker::DOCKER_VERSION
47
+
39
48
  default_config :binary, "docker"
40
49
  default_config :build_options, nil
41
50
  default_config :build_tempdir, Dir.pwd
@@ -52,6 +61,7 @@ module Kitchen
52
61
  default_config :remove_images, false
53
62
  default_config :run_options, nil
54
63
  default_config :security_opt, nil
64
+ default_config :sudo_command, nil
55
65
  default_config :tls, false
56
66
  default_config :tls_cacert, nil
57
67
  default_config :tls_cert, nil
@@ -79,6 +89,13 @@ module Kitchen
79
89
  ].join("-").downcase
80
90
  end
81
91
 
92
+ # The image `kitchen package` commits to. Derived from the instance name,
93
+ # which is already lowercase and dash-separated, so it is a valid Docker
94
+ # repository name as it stands.
95
+ default_config :package_name do |driver|
96
+ "#{driver.instance.name.downcase.gsub(/[^a-z0-9_.-]/, "-")}:latest"
97
+ end
98
+
82
99
  default_config :platform, &:default_platform
83
100
 
84
101
  default_config :run_command do |driver|
@@ -140,6 +157,82 @@ module Kitchen
140
157
  container.destroy(state)
141
158
  end
142
159
 
160
+ # Commits the container to a Docker image.
161
+ #
162
+ # `kitchen package` asks a driver to turn a converged instance into
163
+ # something reusable. For Docker that is an image: `docker commit` on the
164
+ # running container, which is the artifact every other docker tool
165
+ # already takes. Run `docker save` against the result for a tarball.
166
+ #
167
+ # @param state [Hash] instance state naming the container
168
+ # @return [void]
169
+ # @raise [Kitchen::ActionFailed] if the instance has not been created
170
+ def package(state)
171
+ unless state[:container_id]
172
+ raise ActionFailed, "Cannot package #{instance.name}: it has not been created."
173
+ end
174
+
175
+ # Asked here rather than left to `docker commit`, which reports a
176
+ # container that is gone as a bare "Error response from daemon: No such
177
+ # container: <64 hex characters>" with nothing about the instance or
178
+ # what to do next.
179
+ unless container_exists?(state)
180
+ raise ActionFailed, "Cannot package #{instance.name}: the state file names container " \
181
+ "#{state[:container_id]}, which the daemon does not have. " \
182
+ "Run `kitchen destroy` to clear it."
183
+ end
184
+
185
+ name = config[:package_name]
186
+ info("[Docker] Committing container #{state[:container_id]} to #{name}")
187
+ output = docker_command("commit #{shell_escape(state[:container_id])} #{shell_escape(name)}",
188
+ suppress_output: !logger.debug?)
189
+ image_id = output.lines.map(&:strip).find { |line| line.match?(/\Asha256:[[:xdigit:]]{64}\z/) }
190
+ info("[Docker] Packaged #{instance.name} as #{name}#{" (#{image_id})" if image_id}")
191
+ end
192
+
193
+ # Checks the configuration and the daemon it points at.
194
+ #
195
+ # Run by `kitchen doctor`. A true return is how Test Kitchen decides to
196
+ # exit non-zero, so every check runs and the results are OR-ed together
197
+ # rather than returning at the first problem -- somebody running `doctor`
198
+ # wants the whole list, not the first item on it.
199
+ #
200
+ # @param state [Hash] instance state
201
+ # @return [Boolean] whether a problem was found
202
+ def doctor(state)
203
+ [
204
+ doctor_daemon,
205
+ doctor_files,
206
+ doctor_container(state),
207
+ ].any?
208
+ end
209
+
210
+ # Reports whether the container backing this instance is up.
211
+ #
212
+ # Read by `kitchen list --live`, which showed "unknown" for every
213
+ # instance: {Kitchen::Driver::Base} cannot know, and this driver never
214
+ # said. Docker can answer directly, and these are the same two questions
215
+ # create and destroy already ask.
216
+ #
217
+ # @param state [Hash] instance state naming the container
218
+ # @return [Hash] normalized status data
219
+ def status(state)
220
+ common = { source: "driver", checked_at: Time.now.utc.iso8601, resource_id: state[:container_id] }
221
+
222
+ if !state[:container_id]
223
+ common.merge(live: false, state: "not created",
224
+ message: "No container is recorded in the state file")
225
+ elsif !container_exists?(state)
226
+ common.merge(live: false, state: "gone",
227
+ message: "The state file names a container the daemon does not have")
228
+ elsif container_running?(state)
229
+ common.merge(live: true, state: "running")
230
+ else
231
+ common.merge(live: false, state: "stopped",
232
+ message: "The container exists but is not running")
233
+ end
234
+ end
235
+
143
236
  # Waits for the transport to accept a connection, unless disabled.
144
237
  #
145
238
  # @param state [Hash] instance state describing how to connect
@@ -171,6 +264,44 @@ module Kitchen
171
264
 
172
265
  protected
173
266
 
267
+ # @return [Boolean] whether the daemon could not be reached
268
+ def doctor_daemon
269
+ version = docker_command("version --format '{{.Server.Version}}'", suppress_output: true).strip
270
+ info("Docker daemon at #{config[:socket]} is reachable, running #{version}.")
271
+ false
272
+ rescue => e
273
+ error("Cannot reach the Docker daemon at #{config[:socket]}. #{e}")
274
+ true
275
+ end
276
+
277
+ # Checks paths the configuration names.
278
+ #
279
+ # A missing TLS file or Dockerfile is worth catching here because docker
280
+ # reports it far from the cause -- a missing client certificate surfaces
281
+ # as a connection error rather than as a missing file.
282
+ #
283
+ # @return [Boolean] whether any named path is missing
284
+ def doctor_files
285
+ %i{tls_cacert tls_cert tls_key dockerfile}.map do |key|
286
+ path = config[key]
287
+ next false if path.nil? || ::File.exist?(::File.expand_path(path))
288
+
289
+ error("#{key} is set to #{path}, which does not exist.")
290
+ true
291
+ end.any?
292
+ end
293
+
294
+ # @param state [Hash] instance state naming the container
295
+ # @return [Boolean] whether state names a container that is gone
296
+ def doctor_container(state)
297
+ return false unless state[:container_id]
298
+ return false if container_exists?(state)
299
+
300
+ error("The state file names container #{state[:container_id]}, which the daemon does " \
301
+ "not have. Run `kitchen destroy` to clear it.")
302
+ true
303
+ end
304
+
174
305
  # The container implementation for this platform.
175
306
  #
176
307
  # @return [Kitchen::Docker::Container] a Windows or Linux container