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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +26 -0
- data/README.md +76 -5
- data/kitchen-docker.gemspec +1 -1
- data/lib/kitchen/docker/container/linux.rb +4 -1
- data/lib/kitchen/docker/container/windows.rb +5 -1
- data/lib/kitchen/docker/docker_version.rb +1 -1
- data/lib/kitchen/docker/helpers/cli_helper.rb +29 -1
- data/lib/kitchen/docker/helpers/container_helper.rb +82 -5
- data/lib/kitchen/docker/helpers/image_helper.rb +29 -1
- data/lib/kitchen/driver/docker.rb +131 -0
- data/lib/kitchen/transport/docker.rb +10 -2
- data/spec/cli_helper_spec.rb +49 -0
- data/spec/container_helper_spec.rb +156 -4
- data/spec/docker_spec.rb +171 -0
- data/spec/image_helper_spec.rb +69 -0
- data/spec/linux_container_spec.rb +27 -0
- data/spec/support/docker_output.rb +9 -0
- data/spec/transport_docker_spec.rb +20 -0
- data/spec/windows_container_spec.rb +54 -0
- metadata +4 -4
|
@@ -15,6 +15,7 @@ require "kitchen"
|
|
|
15
15
|
|
|
16
16
|
require_relative "../docker/container/linux"
|
|
17
17
|
require_relative "../docker/container/windows"
|
|
18
|
+
require_relative "../docker/docker_version"
|
|
18
19
|
|
|
19
20
|
require_relative "../docker/helpers/inspec_helper"
|
|
20
21
|
|
|
@@ -31,19 +32,24 @@ module Kitchen
|
|
|
31
32
|
# Raised when a docker command against the container fails.
|
|
32
33
|
class DockerFailed < TransportFailed; end
|
|
33
34
|
|
|
34
|
-
#
|
|
35
|
-
|
|
35
|
+
# Reported by `kitchen diagnose`. plugin_version was Kitchen::VERSION,
|
|
36
|
+
# which is Test Kitchen's version rather than this gem's, so a diagnose
|
|
37
|
+
# reported the transport as 4.1.1 while kitchen-docker was at 3.3.4.
|
|
38
|
+
kitchen_transport_api_version 1
|
|
39
|
+
plugin_version Kitchen::Docker::DOCKER_VERSION
|
|
36
40
|
|
|
37
41
|
default_config :binary, "docker"
|
|
38
42
|
default_config :env_variables, nil
|
|
39
43
|
default_config :interactive, false
|
|
40
44
|
default_config :privileged, false
|
|
45
|
+
default_config :sudo_command, nil
|
|
41
46
|
default_config :tls, false
|
|
42
47
|
default_config :tls_cacert, nil
|
|
43
48
|
default_config :tls_cert, nil
|
|
44
49
|
default_config :tls_key, nil
|
|
45
50
|
default_config :tls_verify, false
|
|
46
51
|
default_config :tty, false
|
|
52
|
+
default_config :use_sudo, false
|
|
47
53
|
default_config :working_dir, nil
|
|
48
54
|
|
|
49
55
|
default_config :socket do |transport|
|
|
@@ -77,6 +83,8 @@ module Kitchen
|
|
|
77
83
|
# than from Test Kitchen's configuration.
|
|
78
84
|
#
|
|
79
85
|
# @param state [Hash] instance state naming the container
|
|
86
|
+
# @param block [Proc, nil] forwarded to the connection's constructor, which
|
|
87
|
+
# yields the new connection to it; the connection is not closed afterwards
|
|
80
88
|
# @yieldparam connection [Connection] if a block is given
|
|
81
89
|
# @return [Connection]
|
|
82
90
|
def connection(state, &block)
|
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)
|
|
@@ -333,12 +333,164 @@ describe Kitchen::Docker::Helpers::ContainerHelper do
|
|
|
333
333
|
end
|
|
334
334
|
end
|
|
335
335
|
|
|
336
|
+
describe "#create_dir_on_container" do
|
|
337
|
+
def dir_maker(state)
|
|
338
|
+
helper.tap do |h|
|
|
339
|
+
@commands = []
|
|
340
|
+
allow(h).to receive(:replace_env_variables) { |_s, path| path }
|
|
341
|
+
allow(h).to receive(:docker_command) { |cmd, _opts = {}| @commands << cmd; "" }
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it "creates the directory inside the container" do
|
|
346
|
+
dir_maker(nil).create_dir_on_container({ container_id: "abc", platform: "ubuntu-24.04" }, "/tmp")
|
|
347
|
+
expect(argv(@commands.first)).to include_consecutive("mkdir", "-p", "/tmp")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it "expands an environment variable reference before creating it" do
|
|
351
|
+
h = helper
|
|
352
|
+
allow(h).to receive(:container_env_variables).and_return("TEMP" => "/var/tmp")
|
|
353
|
+
allow(h).to receive(:docker_command) { |cmd, _opts = {}| @cmd = cmd; "" }
|
|
354
|
+
h.create_dir_on_container({ container_id: "abc", platform: "ubuntu-24.04" }, "$TEMP/kitchen")
|
|
355
|
+
expect(argv(@cmd)).to include_consecutive("mkdir", "-p", "/var/tmp/kitchen")
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# A temp_dir with a space in it reached `mkdir -p` unquoted, so the shell
|
|
359
|
+
# tore it in two and mkdir made two directories -- neither of them the one
|
|
360
|
+
# asked for. Every later upload then landed somewhere that did not exist.
|
|
361
|
+
it "keeps a path containing a space as one argument" do
|
|
362
|
+
dir_maker(nil).create_dir_on_container({ container_id: "abc", platform: "ubuntu-24.04" },
|
|
363
|
+
"/var/tmp/kitchen docker")
|
|
364
|
+
expect(argv(@commands.first)).to include_consecutive("mkdir", "-p", "/var/tmp/kitchen docker")
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it "uses PowerShell on a Windows container" do
|
|
368
|
+
dir_maker(nil).create_dir_on_container({ container_id: "abc", platform: "windows-2022" }, 'C:\\Temp')
|
|
369
|
+
expect(@commands.first).to include("powershell").and include("New-Item")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
it "says which directory it could not create" do
|
|
373
|
+
h = helper
|
|
374
|
+
allow(h).to receive(:replace_env_variables) { |_s, path| path }
|
|
375
|
+
allow(h).to receive(:docker_command).and_raise("boom")
|
|
376
|
+
expect { h.create_dir_on_container({ container_id: "abc", platform: "ubuntu-24.04" }, "/tmp/kitchen") }
|
|
377
|
+
.to raise_error(RuntimeError, %r{Failed to create directory /tmp/kitchen})
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
describe "#container_exec" do
|
|
382
|
+
it "runs the command through docker exec" do
|
|
383
|
+
h = helper
|
|
384
|
+
allow(h).to receive(:docker_command) { |cmd, _opts = {}| @cmd = cmd; "output" }
|
|
385
|
+
expect(h.container_exec({ container_id: "abc" }, "echo hi")).to eq "output"
|
|
386
|
+
expect(argv(@cmd)).to include_consecutive("exec", "abc", "echo", "hi")
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it "names the container operation when the command fails" do
|
|
390
|
+
h = helper
|
|
391
|
+
allow(h).to receive(:docker_command).and_raise("boom")
|
|
392
|
+
expect { h.container_exec({ container_id: "abc" }, "echo hi") }
|
|
393
|
+
.to raise_error(RuntimeError, /Failed to execute command on Docker container/)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
describe "#run_container" do
|
|
398
|
+
it "returns the id docker printed for the container it started" do
|
|
399
|
+
h = helper(instance_name: "kitchen-test")
|
|
400
|
+
allow(h).to receive(:docker_command).and_return(DockerOutput::RUN_CLEAN)
|
|
401
|
+
expect(h.run_container({ image_id: "sha256:abc" }, 22)).to eq DockerOutput::RUN_CONTAINER_ID
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it "publishes the port it was given" do
|
|
405
|
+
h = helper
|
|
406
|
+
allow(h).to receive(:docker_command) { |cmd, _opts = {}| @cmd = cmd; DockerOutput::RUN_CLEAN }
|
|
407
|
+
h.run_container({ image_id: "sha256:abc" }, 22)
|
|
408
|
+
expect(argv(@cmd)).to include_consecutive("-p", "22")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it "publishes no port when there is none, as for a Windows container" do
|
|
412
|
+
h = helper
|
|
413
|
+
allow(h).to receive(:docker_command) { |cmd, _opts = {}| @cmd = cmd; DockerOutput::RUN_CLEAN }
|
|
414
|
+
h.run_container(image_id: "sha256:abc")
|
|
415
|
+
expect(argv(@cmd)).not_to include("-p")
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
336
419
|
describe "#copy_file_to_container" do
|
|
420
|
+
let(:state) { { container_id: "abc", platform: "ubuntu-24.04" } }
|
|
421
|
+
|
|
422
|
+
# Records every docker subcommand, and answers the copy check with
|
|
423
|
+
# `landed`. `docker cp` itself prints nothing on success, so the probe is
|
|
424
|
+
# the only call whose output matters.
|
|
425
|
+
def copier(landed:)
|
|
426
|
+
helper.tap do |h|
|
|
427
|
+
@commands = []
|
|
428
|
+
allow(h).to receive(:replace_env_variables) { |_s, path| path }
|
|
429
|
+
allow(h).to receive(:docker_command) do |cmd, _opts = {}|
|
|
430
|
+
@commands << cmd
|
|
431
|
+
cmd.include?(described_class::COPIED_MARKER) && landed ? "#{described_class::COPIED_MARKER}\n" : ""
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
337
436
|
it "addresses the destination as container:path" do
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
437
|
+
copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb")
|
|
438
|
+
expect(@commands.first).to eq "cp /local/f.rb abc:/tmp/f.rb"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
it "says nothing when the file arrived" do
|
|
442
|
+
expect { copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
443
|
+
.not_to raise_error
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# From #387. `docker cp` writes to the container's filesystem layer, which
|
|
447
|
+
# a tmpfs or volume mounted over the destination then hides -- and it exits
|
|
448
|
+
# 0, so nothing about the copy says it did not happen. Left unchecked the
|
|
449
|
+
# first sign is the next command failing with "No such file or directory",
|
|
450
|
+
# which names neither the copy nor the mount.
|
|
451
|
+
context "when docker exits 0 but wrote nothing, as it does into a mount" do
|
|
452
|
+
it "fails at the copy rather than somewhere later" do
|
|
453
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
454
|
+
.to raise_error(RuntimeError, %r{Failed to copy file /local/f\.rb})
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "names the mount as the cause" do
|
|
458
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
459
|
+
.to raise_error(RuntimeError, /cannot write into a mount/)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
it "names the settings that move the destination off the mount" do
|
|
463
|
+
expect { copier(landed: false).copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb") }
|
|
464
|
+
.to raise_error(RuntimeError, /temp_dir.*root_path/m)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "checks the path docker copies into, not the directory it was given" do
|
|
469
|
+
copier(landed: true).copy_file_to_container(state, "/local/f.rb", "/tmp")
|
|
470
|
+
# The probe is handed both parts and picks between them inside the
|
|
471
|
+
# container, since only there is it known whether /tmp is a directory.
|
|
472
|
+
expect(@commands.last).to include("/tmp f.rb")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
it "does not print the probe's marker to the console" do
|
|
476
|
+
h = copier(landed: true)
|
|
477
|
+
allow(h).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
478
|
+
opts = []
|
|
479
|
+
allow(h).to receive(:docker_command) { |_cmd, o = {}| opts << o; "" }
|
|
480
|
+
begin
|
|
481
|
+
h.copy_file_to_container(state, "/local/f.rb", "/tmp/f.rb")
|
|
482
|
+
rescue RuntimeError
|
|
483
|
+
nil
|
|
484
|
+
end
|
|
485
|
+
expect(opts.last).to eq(suppress_output: true)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Windows containers have no tmpfs, and `docker cp` against them is a
|
|
489
|
+
# different code path in Docker, so they keep the behaviour they had.
|
|
490
|
+
it "does not probe a Windows container" do
|
|
491
|
+
h = copier(landed: false)
|
|
492
|
+
h.copy_file_to_container({ container_id: "abc", platform: "windows-2022" }, "C:\\f.rb", "C:\\Temp")
|
|
493
|
+
expect(@commands.length).to eq 1
|
|
342
494
|
end
|
|
343
495
|
end
|
|
344
496
|
end
|
data/spec/docker_spec.rb
CHANGED
|
@@ -17,6 +17,24 @@
|
|
|
17
17
|
require "spec_helper"
|
|
18
18
|
|
|
19
19
|
describe Kitchen::Driver::Docker do
|
|
20
|
+
# `kitchen diagnose` is what bug reports are asked to include, so what it
|
|
21
|
+
# says about the plugin has to be true. The transport reported
|
|
22
|
+
# Kitchen::VERSION, which is Test Kitchen's version, and the driver reported
|
|
23
|
+
# nothing at all.
|
|
24
|
+
describe "plugin metadata" do
|
|
25
|
+
it "reports this gem's version, not Test Kitchen's" do
|
|
26
|
+
expect(described_class.diagnose[:version]).to eq Kitchen::Docker::DOCKER_VERSION
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "does not report Test Kitchen's version" do
|
|
30
|
+
expect(described_class.diagnose[:version]).not_to eq Kitchen::VERSION
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "declares the driver API version it is written against" do
|
|
34
|
+
expect(described_class.diagnose[:api_version]).to eq 2
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
20
38
|
describe "#config_to_options" do
|
|
21
39
|
let(:config) {}
|
|
22
40
|
subject { described_class.new.send(:config_to_options, config) }
|
|
@@ -62,6 +80,159 @@ describe Kitchen::Driver::Docker do
|
|
|
62
80
|
end # /context with a hash of strings with spaces
|
|
63
81
|
end # /describe #config_to_options
|
|
64
82
|
|
|
83
|
+
# `kitchen package`, `kitchen doctor`, and `kitchen list --live` each ask the
|
|
84
|
+
# driver a question. Driver::Base answers all three with a shrug, and this
|
|
85
|
+
# driver used to inherit that: package produced nothing, doctor said nothing,
|
|
86
|
+
# and every instance listed as "unknown". Docker can answer all three.
|
|
87
|
+
def driver(config = {}, instance_name: "default-ubuntu-2404")
|
|
88
|
+
described_class.new(config).tap do |d|
|
|
89
|
+
allow(d).to receive(:instance).and_return(instance_double("Kitchen::Instance", name: instance_name))
|
|
90
|
+
allow(d).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
91
|
+
%i{info error debug banner}.each { |level| allow(d).to receive(level) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "#status" do
|
|
96
|
+
let(:state) { { container_id: "abc123abc123" } }
|
|
97
|
+
|
|
98
|
+
def status_of(exists:, running:, state: { container_id: "abc123abc123" })
|
|
99
|
+
d = driver
|
|
100
|
+
allow(d).to receive(:container_exists?).and_return(exists)
|
|
101
|
+
allow(d).to receive(:container_running?).and_return(running)
|
|
102
|
+
d.status(state)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "reports a running container as live" do
|
|
106
|
+
expect(status_of(exists: true, running: true))
|
|
107
|
+
.to include(live: true, state: "running", source: "driver")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "distinguishes a stopped container from a missing one" do
|
|
111
|
+
expect(status_of(exists: true, running: false)).to include(live: false, state: "stopped")
|
|
112
|
+
expect(status_of(exists: false, running: false)).to include(live: false, state: "gone")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "reports an instance with no container as not created" do
|
|
116
|
+
expect(status_of(exists: false, running: false, state: {}))
|
|
117
|
+
.to include(live: false, state: "not created")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "names the container so `kitchen list --live` can show it" do
|
|
121
|
+
expect(status_of(exists: true, running: true)[:resource_id]).to eq "abc123abc123"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "does not ask docker about an instance that has no container" do
|
|
125
|
+
d = driver
|
|
126
|
+
expect(d).not_to receive(:container_exists?)
|
|
127
|
+
d.status({})
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "stamps when it looked" do
|
|
131
|
+
expect(status_of(exists: true, running: true)[:checked_at])
|
|
132
|
+
.to match(/\A\d{4}-\d{2}-\d{2}T[\d:]+Z\z/)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "#package" do
|
|
137
|
+
let(:state) { { container_id: "abc123abc123" } }
|
|
138
|
+
let(:digest) { "sha256:#{"a" * 64}" }
|
|
139
|
+
|
|
140
|
+
it "commits the container to the configured image name" do
|
|
141
|
+
d = driver({ package_name: "myapp:v1" })
|
|
142
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
143
|
+
expect(d).to receive(:docker_command)
|
|
144
|
+
.with("commit abc123abc123 myapp:v1", hash_including(:suppress_output))
|
|
145
|
+
.and_return("#{digest}\n")
|
|
146
|
+
d.package(state)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "names the image after the instance by default" do
|
|
150
|
+
d = driver
|
|
151
|
+
expect(d.send(:config)[:package_name]).to eq "default-ubuntu-2404:latest"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "escapes a package name that would otherwise split" do
|
|
155
|
+
d = driver({ package_name: "my app:v1" })
|
|
156
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
157
|
+
expect(d).to receive(:docker_command)
|
|
158
|
+
.with(%q{commit abc123abc123 my\ app:v1}, hash_including(:suppress_output))
|
|
159
|
+
.and_return("#{digest}\n")
|
|
160
|
+
d.package(state)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "refuses to package an instance that was never created" do
|
|
164
|
+
expect { driver.package({}) }
|
|
165
|
+
.to raise_error(Kitchen::ActionFailed, /has not been created/)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "does not run docker for an instance that was never created" do
|
|
169
|
+
d = driver
|
|
170
|
+
expect(d).not_to receive(:docker_command)
|
|
171
|
+
expect { d.package({}) }.to raise_error(Kitchen::ActionFailed)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# `docker commit` on a container that is gone says only "Error response
|
|
175
|
+
# from daemon: No such container: <64 hex characters>", which names neither
|
|
176
|
+
# the instance nor what to do about it.
|
|
177
|
+
it "names the instance when the container is gone" do
|
|
178
|
+
d = driver
|
|
179
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
180
|
+
expect { d.package(state) }
|
|
181
|
+
.to raise_error(Kitchen::ActionFailed, /default-ubuntu-2404.*kitchen destroy/m)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe "#doctor" do
|
|
186
|
+
let(:state) { {} }
|
|
187
|
+
|
|
188
|
+
def doctor_with(config = {}, daemon: "29.7.2", state: {})
|
|
189
|
+
d = driver(config)
|
|
190
|
+
allow(d).to receive(:container_exists?).and_return(true)
|
|
191
|
+
allow(d).to receive(:docker_command) do
|
|
192
|
+
raise Kitchen::ShellOut::ShellCommandFailed, "cannot connect" if daemon.nil?
|
|
193
|
+
|
|
194
|
+
"#{daemon}\n"
|
|
195
|
+
end
|
|
196
|
+
d.doctor(state)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "reports no problem when the daemon answers" do
|
|
200
|
+
expect(doctor_with).to be false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "reports a problem when the daemon cannot be reached" do
|
|
204
|
+
expect(doctor_with(daemon: nil)).to be true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "reports a TLS file that is not there" do
|
|
208
|
+
expect(doctor_with({ tls_cert: "/nope/cert.pem" })).to be true
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "reports a dockerfile that is not there" do
|
|
212
|
+
expect(doctor_with({ dockerfile: "/nope/Dockerfile" })).to be true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "accepts paths that do exist" do
|
|
216
|
+
expect(doctor_with({ dockerfile: __FILE__ })).to be false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "reports a state file naming a container the daemon does not have" do
|
|
220
|
+
d = driver
|
|
221
|
+
allow(d).to receive(:docker_command).and_return("29.7.2\n")
|
|
222
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
223
|
+
expect(d.doctor(container_id: "abc123abc123")).to be true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "keeps checking after the first problem, so the whole list is reported" do
|
|
227
|
+
# `kitchen doctor` exists to tell you everything that is wrong at once.
|
|
228
|
+
d = driver({ tls_cert: "/nope/cert.pem", dockerfile: "/nope/Dockerfile" })
|
|
229
|
+
allow(d).to receive(:docker_command).and_raise(Kitchen::ShellOut::ShellCommandFailed, "nope")
|
|
230
|
+
allow(d).to receive(:container_exists?).and_return(false)
|
|
231
|
+
expect(d).to receive(:error).at_least(4).times
|
|
232
|
+
d.doctor(container_id: "abc123abc123")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
65
236
|
describe "socket default config logic" do
|
|
66
237
|
def resolve_socket
|
|
67
238
|
socket = "unix:///var/run/docker.sock"
|
data/spec/image_helper_spec.rb
CHANGED
|
@@ -16,6 +16,30 @@ require "spec_helper"
|
|
|
16
16
|
|
|
17
17
|
describe Kitchen::Docker::Helpers::ImageHelper do
|
|
18
18
|
describe "#parse_image_id" do
|
|
19
|
+
# From #225. `docker build -q` prints the id on a line of its own and
|
|
20
|
+
# nothing else -- no "writing image", no "naming to", no "successfully
|
|
21
|
+
# built" -- so every pattern the parser had missed it, and a build with
|
|
22
|
+
# `build_options: -q` failed with "Could not parse Docker build output for
|
|
23
|
+
# image ID" rather than producing an instance.
|
|
24
|
+
context "with a quiet build" do
|
|
25
|
+
it "reads the id docker printed on its own" do
|
|
26
|
+
expect(helper.parse_image_id(DockerOutput::BUILD_QUIET))
|
|
27
|
+
.to eq DockerOutput::BUILD_QUIET_IMAGE_ID
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "reads it when docker also wrote a warning to stderr" do
|
|
31
|
+
expect(helper.parse_image_id("WARNING: something happened\n#{DockerOutput::BUILD_QUIET}"))
|
|
32
|
+
.to eq DockerOutput::BUILD_QUIET_IMAGE_ID
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "does not mistake a digest that is part of a longer line for the id" do
|
|
36
|
+
# Ordinary build output is full of "... sha256:... done" lines. Only a
|
|
37
|
+
# line that is nothing but a digest is the quiet form.
|
|
38
|
+
expect { helper.parse_image_id("#6 exporting manifest sha256:#{"a" * 64} done\n") }
|
|
39
|
+
.to raise_error(Kitchen::ActionFailed)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
19
43
|
# Docker has changed how it reports the built image's id several times, and
|
|
20
44
|
# each change has broken this parser. Every format the driver claims to
|
|
21
45
|
# support gets a case here, against output copied from a real build.
|
|
@@ -178,4 +202,49 @@ describe Kitchen::Docker::Helpers::ImageHelper do
|
|
|
178
202
|
h.remove_image(state)
|
|
179
203
|
end
|
|
180
204
|
end
|
|
205
|
+
|
|
206
|
+
describe "#image_exists?" do
|
|
207
|
+
let(:state) { { image_id: "sha256:abc" } }
|
|
208
|
+
|
|
209
|
+
def asking(&answer)
|
|
210
|
+
helper.tap do |h|
|
|
211
|
+
@opts = nil
|
|
212
|
+
allow(h).to receive(:logger).and_return(double(debug?: false, debug: nil))
|
|
213
|
+
allow(h).to receive(:docker_command) { |_cmd, opts = {}| @opts = opts; answer.call }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "is true when docker knows the image" do
|
|
218
|
+
expect(asking { "[{}]" }.image_exists?(state)).to be true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "is false when docker does not" do
|
|
222
|
+
expect(asking { raise Kitchen::ShellOut::ShellCommandFailed, "no such image" }
|
|
223
|
+
.image_exists?(state)).to be false
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "does not ask about an image that state does not name" do
|
|
227
|
+
h = helper
|
|
228
|
+
expect(h).not_to receive(:docker_command)
|
|
229
|
+
expect(h.image_exists?({})).to be false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# `kitchen destroy` with remove_images set printed the image's whole
|
|
233
|
+
# inspect JSON -- config, every layer digest, metadata -- between removing
|
|
234
|
+
# the container and removing the image. Only whether the command succeeded
|
|
235
|
+
# is used here, as in every other predicate in this file, all of which
|
|
236
|
+
# already silence their output.
|
|
237
|
+
it "does not print the inspect output" do
|
|
238
|
+
asking { "[{}]" }.image_exists?(state)
|
|
239
|
+
expect(@opts).to eq(suppress_output: true)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "still prints it under -l debug" do
|
|
243
|
+
h = helper
|
|
244
|
+
allow(h).to receive(:logger).and_return(double(debug?: true, debug: nil))
|
|
245
|
+
allow(h).to receive(:docker_command) { |_cmd, opts = {}| @opts = opts; "[{}]" }
|
|
246
|
+
h.image_exists?(state)
|
|
247
|
+
expect(@opts).to eq(suppress_output: false)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
181
250
|
end
|
|
@@ -211,6 +211,33 @@ describe Kitchen::Docker::Container::Linux do
|
|
|
211
211
|
c.execute("echo hi")
|
|
212
212
|
expect(uploaded).to eq "echo hi"
|
|
213
213
|
end
|
|
214
|
+
|
|
215
|
+
# The staged script is run by path, and that path is built from temp_dir,
|
|
216
|
+
# which the user sets.
|
|
217
|
+
def ran_in(temp_dir)
|
|
218
|
+
c = container(temp_dir: temp_dir)
|
|
219
|
+
allow(c).to receive(:create_dir_on_container)
|
|
220
|
+
allow(c).to receive(:replace_env_variables) { |_cfg, path| path }
|
|
221
|
+
allow(c).to receive(:upload)
|
|
222
|
+
ran = nil
|
|
223
|
+
allow(c).to receive(:container_exec) { |_cfg, cmd| ran = cmd }
|
|
224
|
+
c.execute("echo hi")
|
|
225
|
+
ran
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "runs the staged script with bash" do
|
|
229
|
+
ran = ran_in("/tmp")
|
|
230
|
+
expect(argv(ran).first).to eq "/bin/bash"
|
|
231
|
+
expect(argv(ran).last).to match(%r{\A/tmp/docker-[0-9a-f-]+\.sh\z})
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Interpolated unquoted, a temp_dir with a space in it reached `docker
|
|
235
|
+
# exec` as two arguments, and bash was handed the first half of the
|
|
236
|
+
# directory as the script to run.
|
|
237
|
+
it "keeps a temp_dir containing a space as one argument" do
|
|
238
|
+
expect(argv(ran_in("/var/tmp/kitchen docker")).last)
|
|
239
|
+
.to match(%r{\A/var/tmp/kitchen docker/docker-[0-9a-f-]+\.sh\z})
|
|
240
|
+
end
|
|
214
241
|
end
|
|
215
242
|
|
|
216
243
|
describe "#generate_keys" do
|
|
@@ -81,6 +81,15 @@ module DockerOutput
|
|
|
81
81
|
|
|
82
82
|
BUILD_LEGACY_IMAGE_ID = "1a2b3c4d5e6f".freeze
|
|
83
83
|
|
|
84
|
+
# `docker build -q` on Docker 29.7.2. Quiet mode prints the id and nothing
|
|
85
|
+
# else -- no step lines, no exporting lines, none of the wording the other
|
|
86
|
+
# fixtures here are built around. Reachable through the driver as
|
|
87
|
+
# `build_options: -q`.
|
|
88
|
+
BUILD_QUIET = "sha256:ab86ce908a36ffb7de411a72550e416a4d8c268570a0f7313c284c79344d6c0f\n".freeze
|
|
89
|
+
|
|
90
|
+
BUILD_QUIET_IMAGE_ID =
|
|
91
|
+
"sha256:ab86ce908a36ffb7de411a72550e416a4d8c268570a0f7313c284c79344d6c0f".freeze
|
|
92
|
+
|
|
84
93
|
# `docker run -d`, the ordinary case: the id and nothing else.
|
|
85
94
|
RUN_CONTAINER_ID = "b89e1e8b07664a1ee0bd09decb833146eaf9cc810a152950e3576a56745944db".freeze
|
|
86
95
|
RUN_CLEAN = "#{RUN_CONTAINER_ID}\n".freeze
|
|
@@ -15,6 +15,26 @@
|
|
|
15
15
|
require "spec_helper"
|
|
16
16
|
require "kitchen/transport/docker"
|
|
17
17
|
|
|
18
|
+
describe Kitchen::Transport::Docker do
|
|
19
|
+
# `kitchen diagnose` is what bug reports are asked to include, so what it
|
|
20
|
+
# says about the plugin has to be true.
|
|
21
|
+
describe "plugin metadata" do
|
|
22
|
+
it "reports this gem's version, not Test Kitchen's" do
|
|
23
|
+
expect(described_class.diagnose[:version]).to eq Kitchen::Docker::DOCKER_VERSION
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "does not report Test Kitchen's version" do
|
|
27
|
+
# It did: `plugin_version Kitchen::VERSION` made a diagnose say the
|
|
28
|
+
# transport was at Test Kitchen's version rather than this gem's.
|
|
29
|
+
expect(described_class.diagnose[:version]).not_to eq Kitchen::VERSION
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "declares the transport API version it is written against" do
|
|
33
|
+
expect(described_class.diagnose[:api_version]).to eq 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
18
38
|
describe Kitchen::Transport::Docker::Connection do
|
|
19
39
|
let(:options) do
|
|
20
40
|
{
|
|
@@ -64,6 +64,60 @@ describe Kitchen::Docker::Container::Windows do
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
describe "#execute" do
|
|
68
|
+
def staged(config = {})
|
|
69
|
+
c = container({ temp_dir: 'C:\\Temp' }.merge(config))
|
|
70
|
+
allow(c).to receive(:create_dir_on_container)
|
|
71
|
+
allow(c).to receive(:replace_env_variables) { |_cfg, path| path }
|
|
72
|
+
allow(c).to receive(:upload)
|
|
73
|
+
allow(c).to receive(:container_exec) { |_cfg, cmd| @ran = cmd }
|
|
74
|
+
c
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
around do |example|
|
|
78
|
+
Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "runs the staged script with powershell" do
|
|
82
|
+
staged.execute("Write-Host hi")
|
|
83
|
+
expect(@ran).to start_with "powershell -ExecutionPolicy Bypass -NoLogo "
|
|
84
|
+
expect(@ran).to match(/-File .*docker-[0-9a-f-]+\.ps1/)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "stages the script under the configured temp_dir" do
|
|
88
|
+
staged.execute("Write-Host hi")
|
|
89
|
+
expect(@ran).to include 'C:\\Temp\\docker-'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "accepts a temp_dir written with forward slashes" do
|
|
93
|
+
# Kitchen configuration is YAML that is often shared with Linux suites,
|
|
94
|
+
# so the separator is normalised rather than rejected.
|
|
95
|
+
staged(temp_dir: "C:/Temp").execute("Write-Host hi")
|
|
96
|
+
expect(@ran).to include 'C:\\Temp\\docker-'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A Windows temp_dir routinely contains a space -- $env:TEMP under a user
|
|
100
|
+
# whose name has one does. PowerShell's -File takes exactly one argument,
|
|
101
|
+
# so an unquoted path left it looking for a script named after the first
|
|
102
|
+
# word.
|
|
103
|
+
it "quotes a script path containing a space" do
|
|
104
|
+
staged(temp_dir: 'C:\\Users\\Foo Bar\\Temp').execute("Write-Host hi")
|
|
105
|
+
expect(@ran).to include('-File "C:\\Users\\Foo Bar\\Temp\\docker-').and end_with('.ps1"')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "removes the staged script once the command has run" do
|
|
109
|
+
staged.execute("Write-Host hi")
|
|
110
|
+
expect(Dir.glob(".kitchen/temp/docker-*.ps1")).to be_empty
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "removes the staged script when the command fails" do
|
|
114
|
+
c = staged
|
|
115
|
+
allow(c).to receive(:container_exec).and_raise("command exploded")
|
|
116
|
+
expect { c.execute("Write-Host hi") }.to raise_error(/Failed to execute command/)
|
|
117
|
+
expect(Dir.glob(".kitchen/temp/docker-*.ps1")).to be_empty
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
67
121
|
describe "the contract it shares with the Linux container" do
|
|
68
122
|
# The two classes share almost no code but must agree on the state they
|
|
69
123
|
# populate, because the driver and transport read the same keys whichever
|