kitchen-docker 3.3.4 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +17 -0
- data/README.md +71 -3
- data/kitchen-docker.gemspec +1 -1
- data/lib/kitchen/docker/docker_version.rb +1 -1
- data/lib/kitchen/docker/helpers/cli_helper.rb +28 -0
- data/lib/kitchen/docker/helpers/container_helper.rb +70 -4
- data/lib/kitchen/docker/helpers/image_helper.rb +19 -1
- data/lib/kitchen/driver/docker.rb +123 -0
- data/lib/kitchen/transport/docker.rb +2 -0
- data/spec/cli_helper_spec.rb +49 -0
- data/spec/container_helper_spec.rb +73 -4
- data/spec/docker_spec.rb +153 -0
- data/spec/image_helper_spec.rb +45 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cacd3efc6d10490456e00f5d077a3a1eb8f96bb5aef71c6c93b8d2044823a1f
|
|
4
|
+
data.tar.gz: f6c3337ecdc3da7c134da8506472e7237b85a9784dd63fb5b46fa996303539b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec737baa038bf371481c82599263ed09cf98921f687da8cb45e52f1becd13d75417fb545d72338806c533dee99d4b1a39d4c47618e8c15709487937557c30c75
|
|
7
|
+
data.tar.gz: e8434376fb8dd607f224de53137d18ee4f9c644c1205b7341cbe3bcc43c7c07ad2aa575fb0ba5cdc7350ee721d6b0fcf6cb781f73421b2251cbfede68f154a0d
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ 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.0](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.5...v3.4.0) (2026-08-23)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* 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))
|
|
15
|
+
|
|
16
|
+
## [3.3.5](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.4...v3.3.5) (2026-08-23)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* 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))
|
|
22
|
+
* 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))
|
|
23
|
+
* 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))
|
|
24
|
+
* 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))
|
|
25
|
+
|
|
9
26
|
## [3.3.4](https://github.com/test-kitchen/kitchen-docker/compare/v3.3.3...v3.3.4) (2026-08-23)
|
|
10
27
|
|
|
11
28
|
|
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)
|
|
@@ -207,6 +208,7 @@ platforms:
|
|
|
207
208
|
| `build_tempdir` | working directory | Where the generated Dockerfile is written, relative to `build_context`. |
|
|
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
|
|
@@ -284,6 +286,7 @@ Each of these accepts a single value or a list.
|
|
|
284
286
|
| `binary` | `docker` | Docker CLI to invoke — e.g. `docker.io`, or an absolute path. |
|
|
285
287
|
| `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
288
|
| `use_sudo` | `false` | Run every `docker` command through `sudo`. |
|
|
289
|
+
| `sudo_command` | `sudo -E` | The command `use_sudo` prefixes, for hosts that use something else (`doas`, say). |
|
|
287
290
|
| `tls` | `false` | Use TLS when connecting. |
|
|
288
291
|
| `tls_verify` | `false` | Verify the daemon's certificate. |
|
|
289
292
|
| `tls_cacert` | *(none)* | Path to the CA certificate. |
|
|
@@ -314,11 +317,13 @@ These options go under `transport:`, not `driver:`.
|
|
|
314
317
|
| `privileged` | `false` | Run commands with `--privileged`. |
|
|
315
318
|
| `interactive` | `false` | Pass `-i`. |
|
|
316
319
|
| `tty` | `false` | Pass `-t`. |
|
|
320
|
+
| `use_sudo` | `false` | Run every `docker` command through `sudo`. |
|
|
321
|
+
| `sudo_command` | `sudo -E` | The command `use_sudo` prefixes. |
|
|
317
322
|
| `tls`, `tls_verify`, `tls_cacert`, `tls_cert`, `tls_key` | as for the driver | TLS settings for the daemon connection. |
|
|
318
323
|
|
|
319
324
|
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
|
|
321
|
-
|
|
325
|
+
`username`, `use_sudo`, and the TLS settings. If you point one at a non-default
|
|
326
|
+
daemon, or need `sudo` to reach it, configure the other the same way.
|
|
322
327
|
|
|
323
328
|
## Logging into a container
|
|
324
329
|
|
|
@@ -334,6 +339,68 @@ starts `powershell`. The transport's `username`, `working_dir`,
|
|
|
334
339
|
`env_variables`, and `privileged` settings are honoured, so the shell matches
|
|
335
340
|
the environment the provisioner ran in.
|
|
336
341
|
|
|
342
|
+
## Other commands
|
|
343
|
+
|
|
344
|
+
### `kitchen package`
|
|
345
|
+
|
|
346
|
+
Commits the container to a Docker image, so a converged instance can be kept
|
|
347
|
+
and reused:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
kitchen package default-ubuntu-2404
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
-----> Packaging remote instance
|
|
355
|
+
[Docker] Packaged default-ubuntu-2404 as default-ubuntu-2404:latest (sha256:1f51c590...)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The image is named after the instance. Set `package_name` for something else:
|
|
359
|
+
|
|
360
|
+
```yaml
|
|
361
|
+
driver:
|
|
362
|
+
name: docker
|
|
363
|
+
package_name: myapp/under-test:candidate
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Run `docker save` against the result if you want a tarball.
|
|
367
|
+
|
|
368
|
+
### `kitchen doctor`
|
|
369
|
+
|
|
370
|
+
Checks that the daemon is reachable and that the configuration points at
|
|
371
|
+
things that exist:
|
|
372
|
+
|
|
373
|
+
```sh
|
|
374
|
+
kitchen doctor default-ubuntu-2404
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```text
|
|
378
|
+
-----> The doctor is in
|
|
379
|
+
Docker daemon at unix:///var/run/docker.sock is reachable, running 29.7.2.
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
It reports every problem it finds rather than stopping at the first, and exits
|
|
383
|
+
non-zero when there is one: a daemon it cannot reach, a `tls_cacert`,
|
|
384
|
+
`tls_cert`, `tls_key`, or `dockerfile` naming a path that is not there, or a
|
|
385
|
+
state file naming a container the daemon no longer has.
|
|
386
|
+
|
|
387
|
+
### `kitchen list --live`
|
|
388
|
+
|
|
389
|
+
Asks Docker what state each container is actually in, rather than reporting
|
|
390
|
+
only the last action Test Kitchen took:
|
|
391
|
+
|
|
392
|
+
```sh
|
|
393
|
+
kitchen list --live
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```text
|
|
397
|
+
Instance Driver Provisioner Verifier Transport Last Action Last Error Live Status
|
|
398
|
+
default-ubuntu-2404 Docker Shell Dummy Docker Created <None> running
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`running`, `stopped`, `gone` (the state file names a container the daemon does
|
|
402
|
+
not have), or `not created`.
|
|
403
|
+
|
|
337
404
|
## Examples
|
|
338
405
|
|
|
339
406
|
### Testing a systemd service
|
|
@@ -534,7 +601,8 @@ bootstrap. Set it to a supported family, or supply your own
|
|
|
534
601
|
to `false` against a remote daemon.
|
|
535
602
|
|
|
536
603
|
**Permission denied talking to the daemon.** Either add your user to the
|
|
537
|
-
`docker` group, or set `use_sudo: true
|
|
604
|
+
`docker` group, or set `use_sudo: true` under **both** `driver:` and
|
|
605
|
+
`transport:` -- the transport runs its own `docker exec` and `docker cp`.
|
|
538
606
|
|
|
539
607
|
**Anything else.** Run with `-l debug`:
|
|
540
608
|
|
data/kitchen-docker.gemspec
CHANGED
|
@@ -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.
|
|
@@ -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
|
|
@@ -172,23 +177,84 @@ module Kitchen
|
|
|
172
177
|
|
|
173
178
|
# Copies a local file into the container.
|
|
174
179
|
#
|
|
180
|
+
# The copy is checked afterwards, because `docker cp` cannot write into
|
|
181
|
+
# a mount and does not say so. A destination under a tmpfs or a volume
|
|
182
|
+
# is written to the container's own filesystem layer, which the mount
|
|
183
|
+
# then hides, and docker exits 0 with no output -- so the copy looks
|
|
184
|
+
# like it worked and the file is simply not there.
|
|
185
|
+
#
|
|
186
|
+
# Left unchecked, that surfaces later and somewhere else. Running with
|
|
187
|
+
# `tmpfs: /tmp`, which is how the Docker documentation suggests running
|
|
188
|
+
# systemd, the first sign is the next command failing with
|
|
189
|
+
# `/bin/bash: /tmp/docker-<uuid>.sh: No such file or directory` (#387),
|
|
190
|
+
# which names neither the copy nor the mount.
|
|
191
|
+
#
|
|
175
192
|
# @param state [Hash] instance state naming the container
|
|
176
193
|
# @param local_file [String] source path
|
|
177
194
|
# @param remote_file [String] destination path inside the container
|
|
178
195
|
# @return [String] the command's combined output
|
|
179
|
-
# @raise [RuntimeError] if the copy fails
|
|
196
|
+
# @raise [RuntimeError] if the copy fails, or if it silently wrote
|
|
197
|
+
# nothing
|
|
180
198
|
def copy_file_to_container(state, local_file, remote_file)
|
|
181
199
|
debug("Copying local file #{local_file} to #{remote_file} on container")
|
|
182
200
|
|
|
183
201
|
remote_file = replace_env_variables(state, remote_file)
|
|
184
202
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
cmd = build_copy_command(local_file, "#{state[:container_id]}:#{remote_file}")
|
|
204
|
+
output = docker_command(cmd)
|
|
205
|
+
verify_file_copied(state, local_file, remote_file)
|
|
206
|
+
output
|
|
188
207
|
rescue => e
|
|
189
208
|
raise "Failed to copy file #{local_file} to container. #{e}"
|
|
190
209
|
end
|
|
191
210
|
|
|
211
|
+
# Checks that a copied file arrived, and says why if it did not.
|
|
212
|
+
#
|
|
213
|
+
# Only Linux containers are checked. tmpfs mounts are a Linux container
|
|
214
|
+
# feature, and `docker cp` against a Windows container is a different
|
|
215
|
+
# code path in Docker that this cannot be tried against, so those keep
|
|
216
|
+
# the behaviour they have always had.
|
|
217
|
+
#
|
|
218
|
+
# @param state [Hash] instance state naming the container
|
|
219
|
+
# @param local_file [String] the source that was copied
|
|
220
|
+
# @param remote_file [String] the destination it was copied to
|
|
221
|
+
# @return [void]
|
|
222
|
+
# @raise [Kitchen::ActionFailed] if the file is not there
|
|
223
|
+
def verify_file_copied(state, local_file, remote_file)
|
|
224
|
+
return if state[:platform].to_s.include?("windows")
|
|
225
|
+
return if file_on_container?(state, remote_file, ::File.basename(local_file))
|
|
226
|
+
|
|
227
|
+
raise ActionFailed,
|
|
228
|
+
"docker reported no error copying it to #{remote_file}, but the file is " \
|
|
229
|
+
"not there. `docker cp` cannot write into a mount -- if #{remote_file} is " \
|
|
230
|
+
"a tmpfs or a volume, set the transport's temp_dir and the provisioner's " \
|
|
231
|
+
"root_path to a path that is not."
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Whether a `docker cp` destination now holds the file that was copied.
|
|
235
|
+
#
|
|
236
|
+
# `docker cp SRC CONTAINER:DEST` copies into DEST when DEST is a
|
|
237
|
+
# directory and to DEST otherwise. Which of those happened is only known
|
|
238
|
+
# inside the container, so the choice is made there, in the one command,
|
|
239
|
+
# rather than by asking twice from here.
|
|
240
|
+
#
|
|
241
|
+
# The two paths are passed as arguments to `sh` rather than interpolated
|
|
242
|
+
# into the script, so that nothing in either is read as shell syntax.
|
|
243
|
+
#
|
|
244
|
+
# @param state [Hash] instance state naming the container
|
|
245
|
+
# @param remote_file [String] the destination that was copied to
|
|
246
|
+
# @param basename [String] the source's file name
|
|
247
|
+
# @return [Boolean] whether the file is there
|
|
248
|
+
def file_on_container?(state, remote_file, basename)
|
|
249
|
+
script = 'p="$1"; if [ -d "$p" ]; then p="$p/$2"; fi; ' \
|
|
250
|
+
"if [ -e \"$p\" ]; then echo #{COPIED_MARKER}; fi"
|
|
251
|
+
probe = "/bin/sh -c #{Shellwords.escape(script)} sh " \
|
|
252
|
+
"#{Shellwords.escape(remote_file)} #{Shellwords.escape(basename)}"
|
|
253
|
+
|
|
254
|
+
output = docker_command(build_exec_command(state, probe), suppress_output: !logger.debug?)
|
|
255
|
+
output.include?(COPIED_MARKER)
|
|
256
|
+
end
|
|
257
|
+
|
|
192
258
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
193
259
|
|
|
194
260
|
# Reads the container's environment.
|
|
@@ -126,10 +126,28 @@ module Kitchen
|
|
|
126
126
|
parse_image_id(output)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Whether the image named in state is present locally.
|
|
130
|
+
#
|
|
131
|
+
# The inspect is silenced, as every other predicate that shells out to
|
|
132
|
+
# docker is. Left speaking, `kitchen destroy` on an instance with
|
|
133
|
+
# +remove_images+ set printed the image's entire `docker inspect` JSON
|
|
134
|
+
# -- config, every layer digest, metadata -- into the middle of the
|
|
135
|
+
# destroy output, between the container being removed and the image
|
|
136
|
+
# being removed. Nothing read it: only whether the command succeeded is
|
|
137
|
+
# used.
|
|
138
|
+
#
|
|
139
|
+
# It is still printed under `-l debug`, where the rest of the driver's
|
|
140
|
+
# docker traffic is.
|
|
141
|
+
#
|
|
129
142
|
# @param state [Hash] instance state naming the image
|
|
130
143
|
# @return [Boolean] whether the image is present locally
|
|
131
144
|
def image_exists?(state)
|
|
132
|
-
|
|
145
|
+
return false unless state[:image_id]
|
|
146
|
+
|
|
147
|
+
!!docker_command("inspect --type=image #{state[:image_id]}",
|
|
148
|
+
suppress_output: !logger.debug?)
|
|
149
|
+
rescue
|
|
150
|
+
false
|
|
133
151
|
end
|
|
134
152
|
end
|
|
135
153
|
end
|
|
@@ -17,6 +17,7 @@ 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
|
|
|
@@ -52,6 +53,7 @@ module Kitchen
|
|
|
52
53
|
default_config :remove_images, false
|
|
53
54
|
default_config :run_options, nil
|
|
54
55
|
default_config :security_opt, nil
|
|
56
|
+
default_config :sudo_command, nil
|
|
55
57
|
default_config :tls, false
|
|
56
58
|
default_config :tls_cacert, nil
|
|
57
59
|
default_config :tls_cert, nil
|
|
@@ -79,6 +81,13 @@ module Kitchen
|
|
|
79
81
|
].join("-").downcase
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
# The image `kitchen package` commits to. Derived from the instance name,
|
|
85
|
+
# which is already lowercase and dash-separated, so it is a valid Docker
|
|
86
|
+
# repository name as it stands.
|
|
87
|
+
default_config :package_name do |driver|
|
|
88
|
+
"#{driver.instance.name.downcase.gsub(/[^a-z0-9_.-]/, "-")}:latest"
|
|
89
|
+
end
|
|
90
|
+
|
|
82
91
|
default_config :platform, &:default_platform
|
|
83
92
|
|
|
84
93
|
default_config :run_command do |driver|
|
|
@@ -140,6 +149,82 @@ module Kitchen
|
|
|
140
149
|
container.destroy(state)
|
|
141
150
|
end
|
|
142
151
|
|
|
152
|
+
# Commits the container to a Docker image.
|
|
153
|
+
#
|
|
154
|
+
# `kitchen package` asks a driver to turn a converged instance into
|
|
155
|
+
# something reusable. For Docker that is an image: `docker commit` on the
|
|
156
|
+
# running container, which is the artifact every other docker tool
|
|
157
|
+
# already takes. Run `docker save` against the result for a tarball.
|
|
158
|
+
#
|
|
159
|
+
# @param state [Hash] instance state naming the container
|
|
160
|
+
# @return [void]
|
|
161
|
+
# @raise [Kitchen::ActionFailed] if the instance has not been created
|
|
162
|
+
def package(state)
|
|
163
|
+
unless state[:container_id]
|
|
164
|
+
raise ActionFailed, "Cannot package #{instance.name}: it has not been created."
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Asked here rather than left to `docker commit`, which reports a
|
|
168
|
+
# container that is gone as a bare "Error response from daemon: No such
|
|
169
|
+
# container: <64 hex characters>" with nothing about the instance or
|
|
170
|
+
# what to do next.
|
|
171
|
+
unless container_exists?(state)
|
|
172
|
+
raise ActionFailed, "Cannot package #{instance.name}: the state file names container " \
|
|
173
|
+
"#{state[:container_id]}, which the daemon does not have. " \
|
|
174
|
+
"Run `kitchen destroy` to clear it."
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
name = config[:package_name]
|
|
178
|
+
info("[Docker] Committing container #{state[:container_id]} to #{name}")
|
|
179
|
+
output = docker_command("commit #{shell_escape(state[:container_id])} #{shell_escape(name)}",
|
|
180
|
+
suppress_output: !logger.debug?)
|
|
181
|
+
image_id = output.lines.map(&:strip).find { |line| line.match?(/\Asha256:[[:xdigit:]]{64}\z/) }
|
|
182
|
+
info("[Docker] Packaged #{instance.name} as #{name}#{" (#{image_id})" if image_id}")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Checks the configuration and the daemon it points at.
|
|
186
|
+
#
|
|
187
|
+
# Run by `kitchen doctor`. A true return is how Test Kitchen decides to
|
|
188
|
+
# exit non-zero, so every check runs and the results are OR-ed together
|
|
189
|
+
# rather than returning at the first problem -- somebody running `doctor`
|
|
190
|
+
# wants the whole list, not the first item on it.
|
|
191
|
+
#
|
|
192
|
+
# @param state [Hash] instance state
|
|
193
|
+
# @return [Boolean] whether a problem was found
|
|
194
|
+
def doctor(state)
|
|
195
|
+
[
|
|
196
|
+
doctor_daemon,
|
|
197
|
+
doctor_files,
|
|
198
|
+
doctor_container(state),
|
|
199
|
+
].any?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Reports whether the container backing this instance is up.
|
|
203
|
+
#
|
|
204
|
+
# Read by `kitchen list --live`, which showed "unknown" for every
|
|
205
|
+
# instance: {Kitchen::Driver::Base} cannot know, and this driver never
|
|
206
|
+
# said. Docker can answer directly, and these are the same two questions
|
|
207
|
+
# create and destroy already ask.
|
|
208
|
+
#
|
|
209
|
+
# @param state [Hash] instance state naming the container
|
|
210
|
+
# @return [Hash] normalized status data
|
|
211
|
+
def status(state)
|
|
212
|
+
common = { source: "driver", checked_at: Time.now.utc.iso8601, resource_id: state[:container_id] }
|
|
213
|
+
|
|
214
|
+
if !state[:container_id]
|
|
215
|
+
common.merge(live: false, state: "not created",
|
|
216
|
+
message: "No container is recorded in the state file")
|
|
217
|
+
elsif !container_exists?(state)
|
|
218
|
+
common.merge(live: false, state: "gone",
|
|
219
|
+
message: "The state file names a container the daemon does not have")
|
|
220
|
+
elsif container_running?(state)
|
|
221
|
+
common.merge(live: true, state: "running")
|
|
222
|
+
else
|
|
223
|
+
common.merge(live: false, state: "stopped",
|
|
224
|
+
message: "The container exists but is not running")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
143
228
|
# Waits for the transport to accept a connection, unless disabled.
|
|
144
229
|
#
|
|
145
230
|
# @param state [Hash] instance state describing how to connect
|
|
@@ -171,6 +256,44 @@ module Kitchen
|
|
|
171
256
|
|
|
172
257
|
protected
|
|
173
258
|
|
|
259
|
+
# @return [Boolean] whether the daemon could not be reached
|
|
260
|
+
def doctor_daemon
|
|
261
|
+
version = docker_command("version --format '{{.Server.Version}}'", suppress_output: true).strip
|
|
262
|
+
info("Docker daemon at #{config[:socket]} is reachable, running #{version}.")
|
|
263
|
+
false
|
|
264
|
+
rescue => e
|
|
265
|
+
error("Cannot reach the Docker daemon at #{config[:socket]}. #{e}")
|
|
266
|
+
true
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Checks paths the configuration names.
|
|
270
|
+
#
|
|
271
|
+
# A missing TLS file or Dockerfile is worth catching here because docker
|
|
272
|
+
# reports it far from the cause -- a missing client certificate surfaces
|
|
273
|
+
# as a connection error rather than as a missing file.
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean] whether any named path is missing
|
|
276
|
+
def doctor_files
|
|
277
|
+
%i{tls_cacert tls_cert tls_key dockerfile}.map do |key|
|
|
278
|
+
path = config[key]
|
|
279
|
+
next false if path.nil? || ::File.exist?(::File.expand_path(path))
|
|
280
|
+
|
|
281
|
+
error("#{key} is set to #{path}, which does not exist.")
|
|
282
|
+
true
|
|
283
|
+
end.any?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# @param state [Hash] instance state naming the container
|
|
287
|
+
# @return [Boolean] whether state names a container that is gone
|
|
288
|
+
def doctor_container(state)
|
|
289
|
+
return false unless state[:container_id]
|
|
290
|
+
return false if container_exists?(state)
|
|
291
|
+
|
|
292
|
+
error("The state file names container #{state[:container_id]}, which the daemon does " \
|
|
293
|
+
"not have. Run `kitchen destroy` to clear it.")
|
|
294
|
+
true
|
|
295
|
+
end
|
|
296
|
+
|
|
174
297
|
# The container implementation for this platform.
|
|
175
298
|
#
|
|
176
299
|
# @return [Kitchen::Docker::Container] a Windows or Linux container
|
|
@@ -38,12 +38,14 @@ module Kitchen
|
|
|
38
38
|
default_config :env_variables, nil
|
|
39
39
|
default_config :interactive, false
|
|
40
40
|
default_config :privileged, false
|
|
41
|
+
default_config :sudo_command, nil
|
|
41
42
|
default_config :tls, false
|
|
42
43
|
default_config :tls_cacert, nil
|
|
43
44
|
default_config :tls_cert, nil
|
|
44
45
|
default_config :tls_key, nil
|
|
45
46
|
default_config :tls_verify, false
|
|
46
47
|
default_config :tty, false
|
|
48
|
+
default_config :use_sudo, false
|
|
47
49
|
default_config :working_dir, nil
|
|
48
50
|
|
|
49
51
|
default_config :socket do |transport|
|
data/spec/cli_helper_spec.rb
CHANGED
|
@@ -298,6 +298,55 @@ describe Kitchen::Docker::Helpers::CliHelper do
|
|
|
298
298
|
end
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
+
describe "#docker_command" do
|
|
302
|
+
# `binary` is set to `echo` and `sudo_command` to `echo SUDO`, so the
|
|
303
|
+
# assembled command line is observable as output without needing real
|
|
304
|
+
# sudo -- or a real docker -- on the machine running the specs.
|
|
305
|
+
def echoed(config = {})
|
|
306
|
+
helper({ binary: "echo", socket: nil, sudo_command: "echo SUDO" }.merge(config))
|
|
307
|
+
.docker_command("ps -a")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "runs the command as the invoking user by default" do
|
|
311
|
+
expect(echoed).to eq "ps -a\n"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Cases from the README's "Permission denied talking to the daemon"
|
|
315
|
+
# advice. `use_sudo` reached only the `verify_dependencies` probe, so
|
|
316
|
+
# every command that actually touches the daemon still ran unprivileged
|
|
317
|
+
# and the documented fix did nothing.
|
|
318
|
+
it "runs the command through sudo when use_sudo is set" do
|
|
319
|
+
expect(echoed(use_sudo: true)).to eq "SUDO echo ps -a\n"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "leaves the sudo command at its default when none is configured" do
|
|
323
|
+
expect(helper(binary: "echo", socket: nil, use_sudo: true).docker_sudo_opts({}))
|
|
324
|
+
.to eq(use_sudo: true)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
describe "#docker_sudo_opts" do
|
|
329
|
+
it "adds nothing when use_sudo is unset" do
|
|
330
|
+
expect(helper.docker_sudo_opts(suppress_output: true)).to eq(suppress_output: true)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "adds the sudo options the shell-out layer reads" do
|
|
334
|
+
expect(helper(use_sudo: true, sudo_command: "doas").docker_sudo_opts({}))
|
|
335
|
+
.to eq(use_sudo: true, sudo_command: "doas")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "keeps the options it was given" do
|
|
339
|
+
expect(helper(use_sudo: true).docker_sudo_opts(suppress_output: true))
|
|
340
|
+
.to eq(suppress_output: true, use_sudo: true)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "does not mutate the options it was given" do
|
|
344
|
+
options = {}
|
|
345
|
+
helper(use_sudo: true).docker_sudo_opts(options)
|
|
346
|
+
expect(options).to eq({})
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
301
350
|
describe "#docker_shell_opts" do
|
|
302
351
|
it "translates suppress_output into silencing the live stream" do
|
|
303
352
|
expect(helper.docker_shell_opts(suppress_output: true)).to eq(live_stream: nil)
|
|
@@ -334,11 +334,80 @@ describe Kitchen::Docker::Helpers::ContainerHelper do
|
|
|
334
334
|
end
|
|
335
335
|
|
|
336
336
|
describe "#copy_file_to_container" do
|
|
337
|
+
let(:state) { { container_id: "abc", platform: "ubuntu-24.04" } }
|
|
338
|
+
|
|
339
|
+
# Records every docker subcommand, and answers the copy check with
|
|
340
|
+
# `landed`. `docker cp` itself prints nothing on success, so the probe is
|
|
341
|
+
# the only call whose output matters.
|
|
342
|
+
def copier(landed:)
|
|
343
|
+
helper.tap do |h|
|
|
344
|
+
@commands = []
|
|
345
|
+
allow(h).to receive(:replace_env_variables) { |_s, path| path }
|
|
346
|
+
allow(h).to receive(:docker_command) do |cmd, _opts = {}|
|
|
347
|
+
@commands << cmd
|
|
348
|
+
cmd.include?(described_class::COPIED_MARKER) && landed ? "#{described_class::COPIED_MARKER}\n" : ""
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
337
353
|
it "addresses the destination as container:path" do
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb")
|
|
355
|
+
expect(@commands.first).to eq "cp /local/f.rb abc:/tmp/f.rb"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
it "says nothing when the file arrived" do
|
|
359
|
+
expect { copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
360
|
+
.not_to raise_error
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# From #387. `docker cp` writes to the container's filesystem layer, which
|
|
364
|
+
# a tmpfs or volume mounted over the destination then hides -- and it exits
|
|
365
|
+
# 0, so nothing about the copy says it did not happen. Left unchecked the
|
|
366
|
+
# first sign is the next command failing with "No such file or directory",
|
|
367
|
+
# which names neither the copy nor the mount.
|
|
368
|
+
context "when docker exits 0 but wrote nothing, as it does into a mount" do
|
|
369
|
+
it "fails at the copy rather than somewhere later" do
|
|
370
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
371
|
+
.to raise_error(RuntimeError, %r{Failed to copy file /local/f\.rb})
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "names the mount as the cause" do
|
|
375
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
376
|
+
.to raise_error(RuntimeError, /cannot write into a mount/)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it "names the settings that move the destination off the mount" do
|
|
380
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
381
|
+
.to raise_error(RuntimeError, /temp_dir.*root_path/m)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it "checks the path docker copies into, not the directory it was given" do
|
|
386
|
+
copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp")
|
|
387
|
+
# The probe is handed both parts and picks between them inside the
|
|
388
|
+
# container, since only there is it known whether /tmp is a directory.
|
|
389
|
+
expect(@commands.last).to include("/tmp f.rb")
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it "does not print the probe's marker to the console" do
|
|
393
|
+
h = copier(landed: true)
|
|
394
|
+
allow(h).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
395
|
+
opts = []
|
|
396
|
+
allow(h).to receive(:docker_command) { |_cmd, o = {}| opts << o; "" }
|
|
397
|
+
begin
|
|
398
|
+
h.copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb")
|
|
399
|
+
rescue RuntimeError
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
expect(opts.last).to eq(suppress_output: true)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Windows containers have no tmpfs, and `docker cp` against them is a
|
|
406
|
+
# different code path in Docker, so they keep the behaviour they had.
|
|
407
|
+
it "does not probe a Windows container" do
|
|
408
|
+
h = copier(landed: false)
|
|
409
|
+
h.copy_file_to_container({ container_id: "abc", platform: "windows-2022" }, "C:\\f.rb", "C:\\Temp")
|
|
410
|
+
expect(@commands.length).to eq 1
|
|
342
411
|
end
|
|
343
412
|
end
|
|
344
413
|
end
|
data/spec/docker_spec.rb
CHANGED
|
@@ -62,6 +62,159 @@ describe Kitchen::Driver::Docker do
|
|
|
62
62
|
end # /context with a hash of strings with spaces
|
|
63
63
|
end # /describe #config_to_options
|
|
64
64
|
|
|
65
|
+
# `kitchen package`, `kitchen doctor`, and `kitchen list --live` each ask the
|
|
66
|
+
# driver a question. Driver::Base answers all three with a shrug, and this
|
|
67
|
+
# driver used to inherit that: package produced nothing, doctor said nothing,
|
|
68
|
+
# and every instance listed as "unknown". Docker can answer all three.
|
|
69
|
+
def driver(config = {}, instance_name: "default-ubuntu-2404")
|
|
70
|
+
described_class.new(config).tap do |d|
|
|
71
|
+
allow(d).to receive(:instance).and_return(instance_double("Kitchen::Instance", name: instance_name))
|
|
72
|
+
allow(d).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
73
|
+
%i{info error debug banner}.each { |level| allow(d).to receive(level) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe "#status" do
|
|
78
|
+
let(:state) { { container_id: "abc123abc123" } }
|
|
79
|
+
|
|
80
|
+
def status_of(exists:, running:, state: { container_id: "abc123abc123" })
|
|
81
|
+
d = driver
|
|
82
|
+
allow(d).to receive(:container_exists?).and_return(exists)
|
|
83
|
+
allow(d).to receive(:container_running?).and_return(running)
|
|
84
|
+
d.status(state)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "reports a running container as live" do
|
|
88
|
+
expect(status_of(exists: true, running: true))
|
|
89
|
+
.to include(live: true, state: "running", source: "driver")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "distinguishes a stopped container from a missing one" do
|
|
93
|
+
expect(status_of(exists: true, running: false)).to include(live: false, state: "stopped")
|
|
94
|
+
expect(status_of(exists: false, running: false)).to include(live: false, state: "gone")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "reports an instance with no container as not created" do
|
|
98
|
+
expect(status_of(exists: false, running: false, state: {}))
|
|
99
|
+
.to include(live: false, state: "not created")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "names the container so `kitchen list --live` can show it" do
|
|
103
|
+
expect(status_of(exists: true, running: true)[:resource_id]).to eq "abc123abc123"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "does not ask docker about an instance that has no container" do
|
|
107
|
+
d = driver
|
|
108
|
+
expect(d).not_to receive(:container_exists?)
|
|
109
|
+
d.status({})
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "stamps when it looked" do
|
|
113
|
+
expect(status_of(exists: true, running: true)[:checked_at])
|
|
114
|
+
.to match(/\A\d{4}-\d{2}-\d{2}T[\d:]+Z\z/)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "#package" do
|
|
119
|
+
let(:state) { { container_id: "abc123abc123" } }
|
|
120
|
+
let(:digest) { "sha256:#{"a" * 64}" }
|
|
121
|
+
|
|
122
|
+
it "commits the container to the configured image name" do
|
|
123
|
+
d = driver({ package_name: "myapp:v1" })
|
|
124
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
125
|
+
expect(d).to receive(:docker_command)
|
|
126
|
+
.with("commit abc123abc123 myapp:v1", hash_including(:suppress_output))
|
|
127
|
+
.and_return("#{digest}\n")
|
|
128
|
+
d.package(state)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "names the image after the instance by default" do
|
|
132
|
+
d = driver
|
|
133
|
+
expect(d.send(:config)[:package_name]).to eq "default-ubuntu-2404:latest"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "escapes a package name that would otherwise split" do
|
|
137
|
+
d = driver({ package_name: "my app:v1" })
|
|
138
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
139
|
+
expect(d).to receive(:docker_command)
|
|
140
|
+
.with(%q{commit abc123abc123 my\ app:v1}, hash_including(:suppress_output))
|
|
141
|
+
.and_return("#{digest}\n")
|
|
142
|
+
d.package(state)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "refuses to package an instance that was never created" do
|
|
146
|
+
expect { driver.package({}) }
|
|
147
|
+
.to raise_error(Kitchen::ActionFailed, /has not been created/)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "does not run docker for an instance that was never created" do
|
|
151
|
+
d = driver
|
|
152
|
+
expect(d).not_to receive(:docker_command)
|
|
153
|
+
expect { d.package({}) }.to raise_error(Kitchen::ActionFailed)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# `docker commit` on a container that is gone says only "Error response
|
|
157
|
+
# from daemon: No such container: <64 hex characters>", which names neither
|
|
158
|
+
# the instance nor what to do about it.
|
|
159
|
+
it "names the instance when the container is gone" do
|
|
160
|
+
d = driver
|
|
161
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
162
|
+
expect { d.package(state) }
|
|
163
|
+
.to raise_error(Kitchen::ActionFailed, /default-ubuntu-2404.*kitchen destroy/m)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe "#doctor" do
|
|
168
|
+
let(:state) { {} }
|
|
169
|
+
|
|
170
|
+
def doctor_with(config = {}, daemon: "29.7.2", state: {})
|
|
171
|
+
d = driver(config)
|
|
172
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
173
|
+
allow(d).to receive(:docker_command) do
|
|
174
|
+
raise Kitchen::ShellOut::ShellCommandFailed, "cannot connect" if daemon.nil?
|
|
175
|
+
|
|
176
|
+
"#{daemon}\n"
|
|
177
|
+
end
|
|
178
|
+
d.doctor(state)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "reports no problem when the daemon answers" do
|
|
182
|
+
expect(doctor_with).to be false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "reports a problem when the daemon cannot be reached" do
|
|
186
|
+
expect(doctor_with(daemon: nil)).to be true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "reports a TLS file that is not there" do
|
|
190
|
+
expect(doctor_with({ tls_cert: "/nope/cert.pem" })).to be true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "reports a dockerfile that is not there" do
|
|
194
|
+
expect(doctor_with({ dockerfile: "/nope/Dockerfile" })).to be true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "accepts paths that do exist" do
|
|
198
|
+
expect(doctor_with({ dockerfile: __FILE__ })).to be false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "reports a state file naming a container the daemon does not have" do
|
|
202
|
+
d = driver
|
|
203
|
+
allow(d).to receive(:docker_command).and_return("29.7.2\n")
|
|
204
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
205
|
+
expect(d.doctor(container_id: "abc123abc123")).to be true
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "keeps checking after the first problem, so the whole list is reported" do
|
|
209
|
+
# `kitchen doctor` exists to tell you everything that is wrong at once.
|
|
210
|
+
d = driver({ tls_cert: "/nope/cert.pem", dockerfile: "/nope/Dockerfile" })
|
|
211
|
+
allow(d).to receive(:docker_command).and_raise(Kitchen::ShellOut::ShellCommandFailed, "nope")
|
|
212
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
213
|
+
expect(d).to receive(:error).at_least(4).times
|
|
214
|
+
d.doctor(container_id: "abc123abc123")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
65
218
|
describe "socket default config logic" do
|
|
66
219
|
def resolve_socket
|
|
67
220
|
socket = "unix:///var/run/docker.sock"
|
data/spec/image_helper_spec.rb
CHANGED
|
@@ -178,4 +178,49 @@ describe Kitchen::Docker::Helpers::ImageHelper do
|
|
|
178
178
|
h.remove_image(state)
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
|
+
|
|
182
|
+
describe "#image_exists?" do
|
|
183
|
+
let(:state) { { image_id: "sha256:abc" } }
|
|
184
|
+
|
|
185
|
+
def asking(&answer)
|
|
186
|
+
helper.tap do |h|
|
|
187
|
+
@opts = nil
|
|
188
|
+
allow(h).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
189
|
+
allow(h).to receive(:docker_command) { |_cmd, opts = {}| @opts = opts; answer.call }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "is true when docker knows the image" do
|
|
194
|
+
expect(asking { "[{}]" }.image_exists?(state)).to be true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "is false when docker does not" do
|
|
198
|
+
expect(asking { raise Kitchen::ShellOut::ShellCommandFailed, "no such image" }
|
|
199
|
+
.image_exists?(state)).to be false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "does not ask about an image that state does not name" do
|
|
203
|
+
h = helper
|
|
204
|
+
expect(h).not_to receive(:docker_command)
|
|
205
|
+
expect(h.image_exists?({})).to be false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# `kitchen destroy` with remove_images set printed the image's whole
|
|
209
|
+
# inspect JSON -- config, every layer digest, metadata -- between removing
|
|
210
|
+
# the container and removing the image. Only whether the command succeeded
|
|
211
|
+
# is used here, as in every other predicate in this file, all of which
|
|
212
|
+
# already silence their output.
|
|
213
|
+
it "does not print the inspect output" do
|
|
214
|
+
asking { "[{}]" }.image_exists?(state)
|
|
215
|
+
expect(@opts).to eq(suppress_output: true)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "still prints it under -l debug" do
|
|
219
|
+
h = helper
|
|
220
|
+
allow(h).to receive(:logger).and_return(double(debug?: true, debug: nil))
|
|
221
|
+
allow(h).to receive(:docker_command) { |_cmd, opts = {}| @opts = opts; "[{}]" }
|
|
222
|
+
h.image_exists?(state)
|
|
223
|
+
expect(@opts).to eq(suppress_output: false)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
181
226
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-docker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Porter
|
|
@@ -16,7 +16,7 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: '3.0'
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
22
|
version: '5.0'
|
|
@@ -26,7 +26,7 @@ dependencies:
|
|
|
26
26
|
requirements:
|
|
27
27
|
- - ">="
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version:
|
|
29
|
+
version: '3.0'
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '5.0'
|