kitchen-docker 3.3.0 → 3.3.3

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.
@@ -0,0 +1,292 @@
1
+ #
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ #
14
+
15
+ require "spec_helper"
16
+
17
+ describe Kitchen::Docker::Helpers::ContainerHelper do
18
+ describe "#parse_container_id" do
19
+ it "accepts a full-length id" do
20
+ expect(helper.parse_container_id(DockerOutput::RUN_CLEAN))
21
+ .to eq DockerOutput::RUN_CONTAINER_ID
22
+ end
23
+
24
+ it "accepts a short id" do
25
+ expect(helper.parse_container_id("abc123abc123\n")).to eq "abc123abc123"
26
+ end
27
+
28
+ it "fails loudly when the output holds no id" do
29
+ expect { helper.parse_container_id("Error response from daemon: no such image\n") }
30
+ .to raise_error(Kitchen::ActionFailed, /Could not parse Docker run output/)
31
+ end
32
+
33
+ it "fails loudly on empty output" do
34
+ expect { helper.parse_container_id("") }
35
+ .to raise_error(Kitchen::ActionFailed, /Could not parse Docker run output/)
36
+ end
37
+
38
+ # Cases from #452. Rootless Docker emits this on every `docker run`, which
39
+ # makes it the most common way to reach this code path.
40
+ context "when docker wrote a warning to stderr" do
41
+ let(:warning) { "WARNING: IPv4 forwarding is disabled. Networking will not work." }
42
+
43
+ it "finds an id the warning follows" do
44
+ expect(helper.parse_container_id("#{DockerOutput::RUN_CONTAINER_ID}\n#{warning}\n"))
45
+ .to eq DockerOutput::RUN_CONTAINER_ID
46
+ end
47
+
48
+ it "finds an id the warning precedes" do
49
+ expect(helper.parse_container_id("#{warning}\n#{DockerOutput::RUN_CONTAINER_ID}\n"))
50
+ .to eq DockerOutput::RUN_CONTAINER_ID
51
+ end
52
+
53
+ it "finds a short id" do
54
+ expect(helper.parse_container_id("abcdef123456\n#{warning}\n")).to eq "abcdef123456"
55
+ end
56
+
57
+ it "still fails when the warning is all there is" do
58
+ expect { helper.parse_container_id("#{warning}\nno container id here") }
59
+ .to raise_error(Kitchen::ActionFailed, /Could not parse Docker run output/)
60
+ end
61
+ end
62
+
63
+ it "takes the id docker printed, not a later hex line" do
64
+ # run_command concatenates stdout before stderr, so the id always comes
65
+ # first. Scanning backwards would prefer a bare hex line that a warning
66
+ # happened to leave on stderr.
67
+ expect(helper.parse_container_id("#{DockerOutput::RUN_CONTAINER_ID}\ndeadbeefcafe\n"))
68
+ .to eq DockerOutput::RUN_CONTAINER_ID
69
+ end
70
+
71
+ it "does not mistake a hex-looking word inside a message for an id" do
72
+ expect { helper.parse_container_id("WARNING: layer abcdef123456 was skipped\n") }
73
+ .to raise_error(Kitchen::ActionFailed, /Could not parse Docker run output/)
74
+ end
75
+
76
+ it "tolerates surrounding whitespace on the id line" do
77
+ expect(helper.parse_container_id(" #{DockerOutput::RUN_CONTAINER_ID} \n"))
78
+ .to eq DockerOutput::RUN_CONTAINER_ID
79
+ end
80
+
81
+ # run_command returns stdout + stderr, and the parser requires the whole
82
+ # combined string to be exactly the id. Anything docker writes to stderr on
83
+ # a successful `run` therefore fails container creation, with an error that
84
+ # blames parsing rather than naming the warning.
85
+ it "finds the id when docker pulled the image and logged progress to stderr" do
86
+ expect(helper.parse_container_id(DockerOutput::RUN_WITH_PULL_ON_STDERR))
87
+ .to eq DockerOutput::RUN_CONTAINER_ID
88
+ end
89
+
90
+ it "finds the id when the kernel lacks swap accounting" do
91
+ expect(helper.parse_container_id(DockerOutput::RUN_WITH_SWAP_WARNING))
92
+ .to eq DockerOutput::RUN_CONTAINER_ID
93
+ end
94
+ end
95
+
96
+ describe "#remote_socket? and #socket_uri" do
97
+ {
98
+ "unix:///var/run/docker.sock" => false,
99
+ "npipe:////./pipe/docker_engine" => false,
100
+ "tcp://docker.example.com:2376" => true,
101
+ "tcp://127.0.0.1:2375" => true,
102
+ }.each do |socket, remote|
103
+ it "treats #{socket} as #{remote ? "remote" : "local"}" do
104
+ expect(helper(socket: socket).remote_socket?).to eq remote
105
+ end
106
+ end
107
+
108
+ it "reports a socket-less configuration as local" do
109
+ expect(helper(socket: nil).remote_socket?).to be false
110
+ end
111
+
112
+ it "exposes the socket as a URI" do
113
+ expect(helper(socket: "tcp://docker.example.com:2376").socket_uri.port).to eq 2376
114
+ end
115
+ end
116
+
117
+ describe "#dockerfile_proxy_config" do
118
+ it "is empty when no proxy is configured" do
119
+ expect(helper.dockerfile_proxy_config).to eq ""
120
+ end
121
+
122
+ # Tools inside the image disagree about the spelling, so both cases have to
123
+ # be set. Dropping either half breaks builds behind a proxy in a way that
124
+ # only shows up on somebody else's network.
125
+ {
126
+ http_proxy: %w{http_proxy HTTP_PROXY},
127
+ https_proxy: %w{https_proxy HTTPS_PROXY},
128
+ no_proxy: %w{no_proxy NO_PROXY},
129
+ }.each do |option, spellings|
130
+ it "sets #{spellings.join(" and ")} for #{option}" do
131
+ lines = helper(option => "http://proxy:8080").dockerfile_proxy_config.lines.map(&:strip)
132
+ spellings.each { |name| expect(lines).to include "ENV #{name}=http://proxy:8080" }
133
+ end
134
+ end
135
+
136
+ it "emits only the proxies that are configured" do
137
+ config = helper(http_proxy: "http://proxy:8080").dockerfile_proxy_config
138
+ expect(config).not_to match(/no_proxy|https_proxy/i)
139
+ end
140
+ end
141
+
142
+ describe "#container_env_variables" do
143
+ def with_printenv(output)
144
+ h = helper
145
+ allow(h).to receive(:docker_command).and_return(output)
146
+ h.container_env_variables(platform: "ubuntu-24.04")
147
+ end
148
+
149
+ it "reads the container's environment" do
150
+ expect(with_printenv("HOME=/root\nUSER=kitchen\n"))
151
+ .to eq("HOME" => "/root", "USER" => "kitchen")
152
+ end
153
+
154
+ # printenv output is `NAME=VALUE`, and values routinely contain `=`:
155
+ # LS_COLORS and any JAVA_OPTS-style variable do. Splitting on every `=`
156
+ # rather than the first silently truncates them, and the truncated value
157
+ # then flows into replace_env_variables and into upload paths.
158
+ it "keeps a value containing an equals sign intact" do
159
+ expect(with_printenv("LS_COLORS=rs=0:di=01;34\n"))
160
+ .to eq("LS_COLORS" => "rs=0:di=01;34")
161
+ end
162
+
163
+ it "keeps a value that is itself a key=value list" do
164
+ expect(with_printenv("JAVA_OPTS=-Dfoo=bar -Dbaz=qux\n"))
165
+ .to eq("JAVA_OPTS" => "-Dfoo=bar -Dbaz=qux")
166
+ end
167
+
168
+ it "keeps a variable with an empty value" do
169
+ expect(with_printenv("EMPTY=\n")).to eq("EMPTY" => "")
170
+ end
171
+
172
+ it "ignores a continuation line of a multi-line value" do
173
+ # printenv prints an embedded newline literally, so the second line has no
174
+ # name. Recording it would put a nil-valued entry under a bogus key.
175
+ expect(with_printenv("CERT=-----BEGIN-----\nMIIBIjANBg\n"))
176
+ .to eq("CERT" => "-----BEGIN-----")
177
+ end
178
+
179
+ it "parses the JSON a Windows container returns" do
180
+ h = helper
181
+ allow(h).to receive(:docker_command).and_return('{"TEMP":"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp"}')
182
+ expect(h.container_env_variables(platform: "windows-2022"))
183
+ .to eq("TEMP" => 'C:\Users\ADMINI~1\AppData\Local\Temp')
184
+ end
185
+ end
186
+
187
+ describe "#replace_env_variables" do
188
+ let(:linux_state) { { platform: "ubuntu-24.04", container_id: "abc" } }
189
+ let(:windows_state) { { platform: "windows-2022", container_id: "abc" } }
190
+
191
+ def helper_returning(vars)
192
+ h = helper
193
+ allow(h).to receive(:container_env_variables).and_return(vars)
194
+ h
195
+ end
196
+
197
+ it "expands a POSIX $VAR reference" do
198
+ h = helper_returning("TEMP" => "/var/tmp")
199
+ expect(h.replace_env_variables(linux_state, "$TEMP/kitchen")).to eq "/var/tmp/kitchen"
200
+ end
201
+
202
+ it "expands a PowerShell $env:VAR reference" do
203
+ h = helper_returning("TEMP" => 'C:\Temp')
204
+ expect(h.replace_env_variables(windows_state, '$env:TEMP\kitchen')).to eq 'C:\Temp\kitchen'
205
+ end
206
+
207
+ it "leaves a path with no variable reference alone" do
208
+ expect(helper.replace_env_variables(linux_state, "/tmp/kitchen")).to eq "/tmp/kitchen"
209
+ end
210
+
211
+ it "substitutes an empty string when the variable is not set in the container" do
212
+ # Documents current behaviour rather than endorsing it: an unset variable
213
+ # silently yields a path like "/kitchen" instead of failing, and the
214
+ # upload then lands somewhere unexpected.
215
+ h = helper_returning({})
216
+ expect(h.replace_env_variables(linux_state, "$TEMP/kitchen")).to eq "/kitchen"
217
+ end
218
+ end
219
+
220
+ describe "#container_exists? and #container_running?" do
221
+ # `docker inspect` answers for a container in any state; `docker top` only
222
+ # for a running one. The two predicates have to disagree on a stopped
223
+ # container, or destroy cannot clean it up.
224
+ def helper_answering(inspect_ok:, running: "false")
225
+ h = helper
226
+ allow(h).to receive(:docker_command) do |cmd, _opts = {}|
227
+ raise Kitchen::ShellOut::ShellCommandFailed, "No such container" unless inspect_ok
228
+
229
+ cmd.include?("{{.State.Running}}") ? "#{running}\n" : "[{...}]"
230
+ end
231
+ h
232
+ end
233
+
234
+ let(:state) { { container_id: "abc123abc123" } }
235
+
236
+ it "reports a running container as existing and running" do
237
+ h = helper_answering(inspect_ok: true, running: "true")
238
+ expect(h.container_exists?(state)).to be true
239
+ expect(h.container_running?(state)).to be true
240
+ end
241
+
242
+ it "reports a stopped container as existing but not running" do
243
+ h = helper_answering(inspect_ok: true, running: "false")
244
+ expect(h.container_exists?(state)).to be true
245
+ expect(h.container_running?(state)).to be false
246
+ end
247
+
248
+ it "reports a container docker does not know as neither" do
249
+ h = helper_answering(inspect_ok: false)
250
+ expect(h.container_exists?(state)).to be false
251
+ expect(h.container_running?(state)).to be false
252
+ end
253
+
254
+ it "reports no container when state names none" do
255
+ h = helper
256
+ expect(h).not_to receive(:docker_command)
257
+ expect(h.container_exists?({})).to be false
258
+ expect(h.container_running?({})).to be false
259
+ end
260
+
261
+ it "asks docker about a container, not about its processes" do
262
+ # `docker top` was the original implementation and is the bug: it fails
263
+ # on a stopped container, which read as the container not existing.
264
+ h = helper
265
+ seen = []
266
+ allow(h).to receive(:docker_command) { |cmd, _opts = {}| seen << cmd; "[{...}]" }
267
+ h.container_exists?(state)
268
+ expect(seen.join).to include("inspect --type=container")
269
+ expect(seen.join).not_to include("top ")
270
+ end
271
+ end
272
+
273
+ describe "#remove_container" do
274
+ it "stops the container before removing it" do
275
+ # `docker rm` refuses to remove a running container, so the order matters.
276
+ h = helper
277
+ calls = []
278
+ allow(h).to receive(:docker_command) { |cmd| calls << cmd }
279
+ h.remove_container(container_id: "abc")
280
+ expect(calls).to eq ["stop -t 0 abc", "rm abc"]
281
+ end
282
+ end
283
+
284
+ describe "#copy_file_to_container" do
285
+ it "addresses the destination as container:path" do
286
+ h = helper
287
+ allow(h).to receive(:replace_env_variables) { |_state, path| path }
288
+ expect(h).to receive(:docker_command).with("cp /local/f.rb abc:/tmp/f.rb")
289
+ h.copy_file_to_container({ container_id: "abc", platform: "ubuntu-24.04" }, "/local/f.rb", "/tmp/f.rb")
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,115 @@
1
+ #
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ #
14
+
15
+ require "spec_helper"
16
+
17
+ describe Kitchen::Docker::Container do
18
+ subject(:container) { described_class.new(config) }
19
+
20
+ let(:config) { { username: "kitchen", remove_images: false } }
21
+ let(:state) { { container_id: "abc123abc123" } }
22
+
23
+ # A container that exists but has stopped is the case these two guard. It
24
+ # happens whenever the host reboots, the container's PID 1 exits, or someone
25
+ # runs `docker stop` -- so it is not an exotic state to be in.
26
+ def stub_container(exists:, running:)
27
+ allow(container).to receive(:container_exists?).and_return(exists)
28
+ allow(container).to receive(:container_running?).and_return(running)
29
+ end
30
+
31
+ describe "#destroy" do
32
+ it "removes a running container" do
33
+ stub_container(exists: true, running: true)
34
+ expect(container).to receive(:remove_container).with(state)
35
+ container.destroy(state)
36
+ end
37
+
38
+ it "removes a container that exists but has stopped" do
39
+ # Test Kitchen deletes the state file once destroy returns, so a stopped
40
+ # container skipped here is orphaned for good: nothing records its id any
41
+ # more, and `kitchen list` reports the instance as not created.
42
+ stub_container(exists: true, running: false)
43
+ expect(container).to receive(:remove_container).with(state)
44
+ container.destroy(state)
45
+ end
46
+
47
+ it "removes nothing when the container is already gone" do
48
+ stub_container(exists: false, running: false)
49
+ expect(container).not_to receive(:remove_container)
50
+ container.destroy(state)
51
+ end
52
+
53
+ context "with remove_images set" do
54
+ let(:config) { { username: "kitchen", remove_images: true } }
55
+ let(:state) { { container_id: "abc123abc123", image_id: "sha256:abc" } }
56
+
57
+ it "removes the image after the container" do
58
+ stub_container(exists: true, running: true)
59
+ allow(container).to receive(:image_exists?).and_return(true)
60
+ order = []
61
+ allow(container).to receive(:remove_container) { order << :container }
62
+ allow(container).to receive(:remove_image) { order << :image }
63
+
64
+ container.destroy(state)
65
+
66
+ expect(order).to eq %i{container image}
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#create" do
72
+ it "accepts a running container it has seen before" do
73
+ stub_container(exists: true, running: true)
74
+ expect { container.create(state) }.not_to raise_error
75
+ end
76
+
77
+ it "records the login user" do
78
+ stub_container(exists: true, running: true)
79
+ new_state = state.dup
80
+ container.create(new_state)
81
+ expect(new_state[:username]).to eq "kitchen"
82
+ end
83
+
84
+ it "refuses a container that exists but is not running, and says how to clear it" do
85
+ # Reporting this as "the container does not exist" left no way forward:
86
+ # the container is there, so the message has to point at destroy.
87
+ stub_container(exists: true, running: false)
88
+ expect { container.create(state) }
89
+ .to raise_error(Kitchen::ActionFailed, /is not running.*kitchen destroy/m)
90
+ end
91
+
92
+ it "refuses a container that state names but docker does not have" do
93
+ stub_container(exists: false, running: false)
94
+ expect { container.create(state) }
95
+ .to raise_error(Kitchen::ActionFailed, /does not exist/)
96
+ end
97
+
98
+ it "is happy to start from nothing" do
99
+ stub_container(exists: false, running: false)
100
+ expect { container.create({}) }.not_to raise_error
101
+ end
102
+
103
+ it "asks docker about the container only as often as it needs to" do
104
+ # The previous implementation called container_exists? twice on the path
105
+ # that raises, which is two `docker inspect` round trips for one answer.
106
+ stub_container(exists: false, running: false)
107
+ expect(container).to receive(:container_exists?).once.and_return(false)
108
+ begin
109
+ container.create(state)
110
+ rescue Kitchen::ActionFailed
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
@@ -13,97 +13,133 @@
13
13
  #
14
14
 
15
15
  require "spec_helper"
16
- require "kitchen/docker/helpers/dockerfile_helper"
17
16
 
18
17
  describe Kitchen::Docker::Helpers::DockerfileHelper do
19
- let(:helper_class) do
20
- Class.new do
21
- include Kitchen::Docker::Helpers::DockerfileHelper
22
- attr_accessor :config
18
+ # Every platform name the driver accepts, and the method it must route to.
19
+ # Adding a distribution means adding a `when` branch and a method, and it is
20
+ # easy to add one and forget the other -- the result being a platform that
21
+ # raises "Unknown platform" or, worse, quietly bootstraps with the wrong
22
+ # package manager.
23
+ PLATFORM_ROUTES = {
24
+ "arch" => :arch_platform,
25
+ "debian" => :debian_platform,
26
+ "ubuntu" => :debian_platform,
27
+ "fedora" => :fedora_platform,
28
+ "gentoo" => :gentoo_platform,
29
+ "gentoo-paludis" => :gentoo_paludis_platform,
30
+ "opensuse" => :opensuse_platform,
31
+ "opensuse/leap" => :opensuse_platform,
32
+ "opensuse/tumbleweed" => :opensuse_platform,
33
+ "sles" => :opensuse_platform,
34
+ "rhel" => :rhel_platform,
35
+ "centos" => :rhel_platform,
36
+ "oraclelinux" => :rhel_platform,
37
+ "amazonlinux" => :amazonlinux_platform,
38
+ "centosstream" => :centosstream_platform,
39
+ "almalinux" => :almalinux_platform,
40
+ "rockylinux" => :rockylinux_platform,
41
+ "photon" => :photonos_platform,
42
+ }.freeze
23
43
 
24
- def initialize(config = {})
25
- @config = config
44
+ describe "#dockerfile_platform" do
45
+ PLATFORM_ROUTES.each do |platform, method|
46
+ it "routes #{platform} to ##{method}" do
47
+ # Compared by output rather than by stubbing the method, so this also
48
+ # proves the branch is wired to a method that actually exists.
49
+ h = helper(platform: platform, disable_upstart: true)
50
+ expect(h.dockerfile_platform).to eq h.public_send(method)
26
51
  end
27
52
  end
53
+
54
+ it "names the platform it could not handle" do
55
+ # The error is what a user sees after a typo in kitchen.yml, so it has to
56
+ # contain the value they typed.
57
+ expect { helper(platform: "ubunut").dockerfile_platform }
58
+ .to raise_error(Kitchen::ActionFailed, /Unknown platform 'ubunut'/)
59
+ end
28
60
  end
29
61
 
30
- let(:helper) { helper_class.new(platform:) }
62
+ # Test Kitchen reaches a Linux container over SSH as a sudo-capable user, so
63
+ # an image that lacks either an SSH server or sudo is useless no matter which
64
+ # distribution it is. These hold for every supported platform.
65
+ describe "invariants every platform's setup must satisfy" do
66
+ PLATFORM_ROUTES.values.uniq.each do |method|
67
+ context "##{method}" do
68
+ subject(:fragment) { helper(platform: "ubuntu", disable_upstart: true).public_send(method) }
69
+
70
+ it "installs sudo" do
71
+ expect(fragment).to match(/\bsudo\b/)
72
+ end
73
+
74
+ it "installs an SSH server" do
75
+ expect(fragment).to match(/openssh|\bssh\b/)
76
+ end
77
+
78
+ it "contains only Dockerfile instructions" do
79
+ # Catches a fragment that has picked up a stray shell line, which
80
+ # would fail the build with a parse error rather than anything that
81
+ # points back here.
82
+ instructions = fragment.lines.map(&:strip).reject(&:empty?)
83
+ continued = false
84
+ instructions.each do |line|
85
+ expect(line).to match(/\A(RUN|ENV|ARG|COPY|ADD|WORKDIR|USER)\b/) unless continued
86
+ continued = line.end_with?("\\")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
31
92
 
32
93
  describe "#amazonlinux_platform" do
33
- let(:platform) { "amazonlinux" }
94
+ subject(:fragment) { helper(platform: "amazonlinux").amazonlinux_platform }
34
95
 
35
- it "includes --allowerasing flag for yum install" do
36
- result = helper.amazonlinux_platform
37
- expect(result).to include("yum install -y --allowerasing")
96
+ it "allows erasing conflicting packages, which Amazon Linux 2023 needs" do
97
+ expect(fragment).to include "--allowerasing"
38
98
  end
39
99
 
40
- it "installs required packages including curl" do
41
- result = helper.amazonlinux_platform
42
- expect(result).to include("sudo openssh-server openssh-clients which curl")
100
+ it "installs curl" do
101
+ expect(fragment).to match(/curl/)
43
102
  end
44
103
 
45
- it "sets container environment variable" do
46
- result = helper.amazonlinux_platform
47
- expect(result).to include("ENV container=docker")
104
+ it "marks the image as running in a container" do
105
+ expect(fragment).to include "ENV container=docker"
48
106
  end
49
107
 
50
- it "generates SSH host key if missing" do
51
- result = helper.amazonlinux_platform
52
- expect(result).to include("ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key")
108
+ it "generates an SSH host key only if one is missing" do
109
+ # Regenerating on every build would change the host key under a user who
110
+ # already accepted it.
111
+ expect(fragment).to include '[ -f "/etc/ssh/ssh_host_rsa_key" ] ||'
53
112
  end
54
113
  end
55
114
 
56
115
  describe "#rhel_platform" do
57
- let(:platform) { "rhel" }
116
+ subject(:fragment) { helper(platform: "rhel").rhel_platform }
58
117
 
59
- it "does not include --allowerasing flag" do
60
- result = helper.rhel_platform
61
- expect(result).not_to include("--allowerasing")
118
+ it "does not pass --allowerasing, which older yum does not accept" do
119
+ expect(fragment).not_to include "--allowerasing"
62
120
  end
63
121
 
64
- it "installs required packages including curl if needed" do
65
- result = helper.rhel_platform
66
- expect(result).to include("sudo openssh-server openssh-clients which")
67
- expect(result).to include("which curl || yum install -y curl")
122
+ it "installs curl if it is not already present" do
123
+ expect(fragment).to match(/curl/)
68
124
  end
69
125
  end
70
126
 
71
- describe "#dockerfile_platform" do
72
- context "when platform is amazonlinux" do
73
- let(:platform) { "amazonlinux" }
74
-
75
- it "calls amazonlinux_platform method" do
76
- expect(helper).to receive(:amazonlinux_platform).and_call_original
77
- result = helper.dockerfile_platform
78
- expect(result).to include("--allowerasing")
79
- end
80
- end
81
-
82
- context "when platform is rhel" do
83
- let(:platform) { "rhel" }
84
-
85
- it "calls rhel_platform method" do
86
- expect(helper).to receive(:rhel_platform).and_call_original
87
- result = helper.dockerfile_platform
88
- expect(result).not_to include("--allowerasing")
89
- end
127
+ describe "#debian_platform" do
128
+ it "neutralises initctl when disable_upstart is set" do
129
+ expect(helper(platform: "ubuntu", disable_upstart: true).debian_platform)
130
+ .to include "dpkg-divert"
90
131
  end
91
132
 
92
- context "when platform is centos" do
93
- let(:platform) { "centos" }
94
-
95
- it "calls rhel_platform method" do
96
- expect(helper).to receive(:rhel_platform).and_call_original
97
- helper.dockerfile_platform
98
- end
133
+ it "leaves initctl alone when disable_upstart is false" do
134
+ expect(helper(platform: "ubuntu", disable_upstart: false).debian_platform)
135
+ .not_to include "dpkg-divert"
99
136
  end
100
137
 
101
- context "when platform is oraclelinux" do
102
- let(:platform) { "oraclelinux" }
103
-
104
- it "calls rhel_platform method" do
105
- expect(helper).to receive(:rhel_platform).and_call_original
106
- helper.dockerfile_platform
138
+ it "keeps apt non-interactive either way" do
139
+ # A prompt during the build hangs it until Docker times out.
140
+ [true, false].each do |setting|
141
+ expect(helper(platform: "ubuntu", disable_upstart: setting).debian_platform)
142
+ .to include "ENV DEBIAN_FRONTEND=noninteractive"
107
143
  end
108
144
  end
109
145
  end
@@ -0,0 +1,60 @@
1
+ #
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ #
14
+
15
+ require "spec_helper"
16
+ require "tmpdir"
17
+
18
+ describe Kitchen::Docker::ERBContext do
19
+ def render(template, config)
20
+ ERB.new(template).result(described_class.new(config).get_binding)
21
+ end
22
+
23
+ # A user-supplied `dockerfile:` is rendered through ERB with the driver
24
+ # configuration exposed as instance variables. That is a documented part of
25
+ # the driver's interface -- test/Dockerfile depends on it -- so the exposure
26
+ # rules are worth pinning.
27
+ it "exposes each configuration key as an instance variable" do
28
+ expect(render("<%= @image %>", image: "ubuntu:24.04")).to eq "ubuntu:24.04"
29
+ end
30
+
31
+ it "exposes keys the driver has no option for" do
32
+ # kitchen.yml's dockerfile platform passes `password`, which the driver
33
+ # itself never reads; the template is the only consumer.
34
+ expect(render("<%= @password %>", password: "secret")).to eq "secret"
35
+ end
36
+
37
+ it "renders a missing key as empty rather than failing the build" do
38
+ expect(render("[<%= @nope %>]", {})).to eq "[]"
39
+ end
40
+
41
+ it "accepts string keys as well as symbols" do
42
+ expect(render("<%= @image %>", "image" => "alpine")).to eq "alpine"
43
+ end
44
+
45
+ it "allows a template to read a file, as test/Dockerfile does with the public key" do
46
+ Dir.mktmpdir do |dir|
47
+ key = File.join(dir, "k.pub")
48
+ File.write(key, "ssh-rsa AAAA test\n")
49
+ expect(render("<%= IO.read(@public_key).strip %>", public_key: key))
50
+ .to eq "ssh-rsa AAAA test"
51
+ end
52
+ end
53
+
54
+ it "keeps values verbatim, without escaping" do
55
+ # The template author decides on quoting; silently escaping here would
56
+ # corrupt Dockerfiles that already quote correctly.
57
+ expect(render("<%= @run_command %>", run_command: 'sshd -o "UseDNS=no"'))
58
+ .to eq 'sshd -o "UseDNS=no"'
59
+ end
60
+ end