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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +0 -1
- data/.yardopts +11 -0
- data/CHANGELOG.md +399 -20
- data/CONTRIBUTING.md +203 -0
- data/Gemfile +4 -0
- data/README.md +417 -533
- data/Rakefile +18 -0
- data/lib/kitchen/docker/container/linux.rb +73 -6
- data/lib/kitchen/docker/container/windows.rb +34 -3
- data/lib/kitchen/docker/container.rb +45 -1
- data/lib/kitchen/docker/docker_version.rb +4 -2
- data/lib/kitchen/docker/erb_context.rb +9 -0
- data/lib/kitchen/docker/helpers/cli_helper.rb +111 -34
- data/lib/kitchen/docker/helpers/container_helper.rb +151 -25
- data/lib/kitchen/docker/helpers/dockerfile_helper.rb +54 -0
- data/lib/kitchen/docker/helpers/file_helper.rb +24 -10
- data/lib/kitchen/docker/helpers/image_helper.rb +47 -1
- data/lib/kitchen/docker/helpers/inspec_helper.rb +1 -0
- data/lib/kitchen/driver/docker.rb +27 -0
- data/lib/kitchen/transport/docker.rb +43 -2
- data/spec/cli_helper_spec.rb +320 -0
- data/spec/container_helper_spec.rb +292 -0
- data/spec/container_spec.rb +115 -0
- data/spec/dockerfile_helper_spec.rb +97 -61
- data/spec/erb_context_spec.rb +60 -0
- data/spec/file_helper_spec.rb +110 -0
- data/spec/image_helper_spec.rb +181 -0
- data/spec/linux_container_spec.rb +252 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/argv.rb +55 -0
- data/spec/support/docker_output.rb +117 -0
- data/spec/support/harness.rb +70 -0
- data/spec/transport_docker_spec.rb +7 -0
- data/spec/windows_container_spec.rb +101 -0
- metadata +15 -2
|
@@ -24,44 +24,122 @@ require_relative "cli_helper"
|
|
|
24
24
|
|
|
25
25
|
module Kitchen
|
|
26
26
|
module Docker
|
|
27
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
27
28
|
module Helpers
|
|
28
|
-
# rubocop:disable Metrics/ModuleLength
|
|
29
|
+
# rubocop:disable Metrics/ModuleLength
|
|
30
|
+
# Operations against a running container: exec, copy, inspect, remove.
|
|
29
31
|
module ContainerHelper
|
|
30
32
|
include Configurable
|
|
31
33
|
include Kitchen::Docker::Helpers::CliHelper
|
|
32
34
|
|
|
35
|
+
# Pulls the container id out of `docker run` output.
|
|
36
|
+
#
|
|
37
|
+
# Docker prints ids in short (12) or full (64) hex form, on a line of
|
|
38
|
+
# their own. The id is looked for line by line rather than by taking the
|
|
39
|
+
# whole output, because {CliHelper#run_command} returns stdout and stderr
|
|
40
|
+
# together and some daemons write warnings to stderr on a run that
|
|
41
|
+
# otherwise succeeds. Rootless Docker emits "WARNING: IPv4 forwarding is
|
|
42
|
+
# disabled. Networking will not work." on every run; setting
|
|
43
|
+
# +run_options+ to +--net=host+ produces "WARNING: Published ports are
|
|
44
|
+
# discarded when using host network mode", since the driver always
|
|
45
|
+
# publishes port 22 for Linux containers. Treating the whole output as
|
|
46
|
+
# the id failed those runs *after* the container had been created,
|
|
47
|
+
# leaving it running and untracked.
|
|
48
|
+
#
|
|
49
|
+
# Scanning forward is deterministic: +run_command+ concatenates stdout
|
|
50
|
+
# before stderr, so the id always precedes anything a warning adds.
|
|
51
|
+
#
|
|
52
|
+
# @param output [String] the command output, stdout and stderr together
|
|
53
|
+
# @return [String] the container id
|
|
54
|
+
# @raise [Kitchen::ActionFailed] if no id could be found
|
|
33
55
|
def parse_container_id(output)
|
|
34
|
-
container_id = output.
|
|
35
|
-
|
|
36
|
-
unless [12, 64].include?(container_id.size)
|
|
37
|
-
raise ActionFailed, "Could not parse Docker run output for container ID"
|
|
56
|
+
container_id = output.to_s.lines.map(&:strip).find do |line|
|
|
57
|
+
line.match?(/\A[0-9a-f]{12}(?:[0-9a-f]{52})?\z/)
|
|
38
58
|
end
|
|
39
59
|
|
|
60
|
+
raise ActionFailed, "Could not parse Docker run output for container ID" unless container_id
|
|
61
|
+
|
|
40
62
|
container_id
|
|
41
63
|
end
|
|
42
64
|
|
|
65
|
+
# Renders the configured Dockerfile through ERB.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] the rendered Dockerfile
|
|
43
68
|
def dockerfile_template
|
|
44
69
|
template = IO.read(File.expand_path(config[:dockerfile]))
|
|
45
70
|
context = Kitchen::Docker::ERBContext.new(config.to_hash)
|
|
46
71
|
ERB.new(template).result(context.get_binding)
|
|
47
72
|
end
|
|
48
73
|
|
|
74
|
+
# @return [Boolean] whether the configured socket is a TCP one, meaning
|
|
75
|
+
# the daemon is not on this machine
|
|
49
76
|
def remote_socket?
|
|
50
77
|
config[:socket] ? socket_uri.scheme == "tcp" : false
|
|
51
78
|
end
|
|
52
79
|
|
|
80
|
+
# @return [URI] the configured Docker socket
|
|
53
81
|
def socket_uri
|
|
54
82
|
URI.parse(config[:socket])
|
|
55
83
|
end
|
|
56
84
|
|
|
85
|
+
# The path to pass to `docker build -f`.
|
|
86
|
+
#
|
|
87
|
+
# With a build context the path has to be relative to it; without one
|
|
88
|
+
# docker reads the Dockerfile from stdin and the absolute path is fine.
|
|
89
|
+
#
|
|
90
|
+
# @param file [File] the temp Dockerfile
|
|
91
|
+
# @return [String] the path to use
|
|
57
92
|
def dockerfile_path(file)
|
|
58
93
|
config[:build_context] ? Pathname.new(file.path).relative_path_from(Pathname.pwd).to_s : file.path
|
|
59
94
|
end
|
|
60
95
|
|
|
96
|
+
# Whether the container named in state is present, running or not.
|
|
97
|
+
#
|
|
98
|
+
# Asked with `docker inspect` rather than `docker top`, which answers a
|
|
99
|
+
# different question: `top` lists processes, so it fails on a container
|
|
100
|
+
# that exists but has stopped. Reading that as "does not exist" made
|
|
101
|
+
# {Kitchen::Docker::Container#destroy} skip removal and leave the
|
|
102
|
+
# container behind, while Test Kitchen deleted the state file and
|
|
103
|
+
# reported success.
|
|
104
|
+
#
|
|
105
|
+
# @param state [Hash] instance state naming the container
|
|
106
|
+
# @return [Boolean] whether the container exists in any state
|
|
61
107
|
def container_exists?(state)
|
|
62
|
-
|
|
108
|
+
return false unless state[:container_id]
|
|
109
|
+
|
|
110
|
+
!!docker_command("inspect --type=container #{state[:container_id]}",
|
|
111
|
+
suppress_output: !logger.debug?)
|
|
112
|
+
rescue
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Whether the container named in state is running.
|
|
117
|
+
#
|
|
118
|
+
# Separate from {#container_exists?} because the two callers want
|
|
119
|
+
# different questions answered: destroy removes a container in any
|
|
120
|
+
# state, while create has to tell a container it can use from one that
|
|
121
|
+
# has stopped.
|
|
122
|
+
#
|
|
123
|
+
# @param state [Hash] instance state naming the container
|
|
124
|
+
# @return [Boolean] whether the container exists and is running
|
|
125
|
+
def container_running?(state)
|
|
126
|
+
return false unless state[:container_id]
|
|
127
|
+
|
|
128
|
+
output = docker_command(
|
|
129
|
+
"inspect --type=container --format '{{.State.Running}}' #{state[:container_id]}",
|
|
130
|
+
suppress_output: !logger.debug?
|
|
131
|
+
)
|
|
132
|
+
output.strip == "true"
|
|
133
|
+
rescue
|
|
134
|
+
false
|
|
63
135
|
end
|
|
64
136
|
|
|
137
|
+
# Runs a command inside the container.
|
|
138
|
+
#
|
|
139
|
+
# @param state [Hash] instance state naming the container
|
|
140
|
+
# @param command [String] the command to run
|
|
141
|
+
# @return [String] the command's combined output
|
|
142
|
+
# @raise [RuntimeError] if the command fails
|
|
65
143
|
def container_exec(state, command)
|
|
66
144
|
cmd = build_exec_command(state, command)
|
|
67
145
|
docker_command(cmd)
|
|
@@ -69,6 +147,13 @@ module Kitchen
|
|
|
69
147
|
raise "Failed to execute command on Docker container. #{e}"
|
|
70
148
|
end
|
|
71
149
|
|
|
150
|
+
# Creates a directory inside the container, on Linux or Windows.
|
|
151
|
+
#
|
|
152
|
+
# @param state [Hash] instance state naming the container
|
|
153
|
+
# @param path [String] the directory to create; environment variable
|
|
154
|
+
# references are expanded first
|
|
155
|
+
# @return [String] the command's combined output
|
|
156
|
+
# @raise [RuntimeError] if the directory cannot be created
|
|
72
157
|
def create_dir_on_container(state, path)
|
|
73
158
|
path = replace_env_variables(state, path)
|
|
74
159
|
cmd = "mkdir -p #{path}"
|
|
@@ -84,6 +169,13 @@ module Kitchen
|
|
|
84
169
|
raise "Failed to create directory #{path} on container. #{e}"
|
|
85
170
|
end
|
|
86
171
|
|
|
172
|
+
# Copies a local file into the container.
|
|
173
|
+
#
|
|
174
|
+
# @param state [Hash] instance state naming the container
|
|
175
|
+
# @param local_file [String] source path
|
|
176
|
+
# @param remote_file [String] destination path inside the container
|
|
177
|
+
# @return [String] the command's combined output
|
|
178
|
+
# @raise [RuntimeError] if the copy fails
|
|
87
179
|
def copy_file_to_container(state, local_file, remote_file)
|
|
88
180
|
debug("Copying local file #{local_file} to #{remote_file} on container")
|
|
89
181
|
|
|
@@ -97,6 +189,11 @@ module Kitchen
|
|
|
97
189
|
end
|
|
98
190
|
|
|
99
191
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
192
|
+
|
|
193
|
+
# Reads the container's environment.
|
|
194
|
+
#
|
|
195
|
+
# @param state [Hash] instance state naming the container
|
|
196
|
+
# @return [Hash] variable names to values
|
|
100
197
|
def container_env_variables(state)
|
|
101
198
|
# Retrieves all environment variables from inside container
|
|
102
199
|
vars = {}
|
|
@@ -109,13 +206,28 @@ module Kitchen
|
|
|
109
206
|
else
|
|
110
207
|
cmd = build_exec_command(state, "printenv")
|
|
111
208
|
stdout = docker_command(cmd, suppress_output: !logger.debug?).strip
|
|
112
|
-
|
|
209
|
+
# printenv writes NAME=VALUE, and values routinely contain "=" --
|
|
210
|
+
# LS_COLORS and anything -Dkey=value shaped do. Split on the first
|
|
211
|
+
# one only, or the value is truncated at it. Lines with no "=" are
|
|
212
|
+
# continuations of a multi-line value and carry no name.
|
|
213
|
+
stdout.split("\n").each do |line|
|
|
214
|
+
name, value = line.split("=", 2)
|
|
215
|
+
vars[name] = value unless value.nil?
|
|
216
|
+
end
|
|
113
217
|
end
|
|
114
218
|
|
|
115
219
|
vars
|
|
116
220
|
end
|
|
117
221
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
118
222
|
|
|
223
|
+
# Expands a container-side environment variable reference in a path.
|
|
224
|
+
#
|
|
225
|
+
# Handles both +$env:TEMP+ and +$TEMP+ forms. The value has to be read from
|
|
226
|
+
# inside the container, since the workstation's environment is unrelated.
|
|
227
|
+
#
|
|
228
|
+
# @param state [Hash] instance state naming the container
|
|
229
|
+
# @param str [String] the string to expand
|
|
230
|
+
# @return [String] the expanded string
|
|
119
231
|
def replace_env_variables(state, str)
|
|
120
232
|
if str.include?("$env:")
|
|
121
233
|
key = str[/\$env:(.*?)(\\|$)/, 1]
|
|
@@ -130,12 +242,20 @@ module Kitchen
|
|
|
130
242
|
str
|
|
131
243
|
end
|
|
132
244
|
|
|
245
|
+
# Runs the container and returns its id.
|
|
246
|
+
#
|
|
247
|
+
# @param state [Hash] instance state naming the image
|
|
248
|
+
# @param transport_port [Integer, nil] container port to publish, if any
|
|
249
|
+
# @return [String] the new container's id
|
|
133
250
|
def run_container(state, transport_port = nil)
|
|
134
251
|
cmd = build_run_command(state[:image_id], transport_port)
|
|
135
252
|
output = docker_command(cmd)
|
|
136
253
|
parse_container_id(output)
|
|
137
254
|
end
|
|
138
255
|
|
|
256
|
+
# @param state [Hash] instance state naming the container
|
|
257
|
+
# @return [String] the container's address on the Docker network
|
|
258
|
+
# @raise [Kitchen::ActionFailed] if it cannot be determined
|
|
139
259
|
def container_ip_address(state)
|
|
140
260
|
cmd = "inspect --format '{{ .NetworkSettings.IPAddress }}'"
|
|
141
261
|
cmd << " #{state[:container_id]}"
|
|
@@ -144,35 +264,41 @@ module Kitchen
|
|
|
144
264
|
raise ActionFailed, "Error getting internal IP of Docker container"
|
|
145
265
|
end
|
|
146
266
|
|
|
267
|
+
# Stops and removes the container.
|
|
268
|
+
#
|
|
269
|
+
# @param state [Hash] instance state naming the container
|
|
270
|
+
# @return [void]
|
|
147
271
|
def remove_container(state)
|
|
148
272
|
container_id = state[:container_id]
|
|
149
273
|
docker_command("stop -t 0 #{container_id}")
|
|
150
274
|
docker_command("rm #{container_id}")
|
|
151
275
|
end
|
|
152
276
|
|
|
153
|
-
#
|
|
277
|
+
# Dockerfile ENV lines carrying the configured proxy settings.
|
|
278
|
+
#
|
|
279
|
+
# Each is emitted in both lower and upper case, because different tools
|
|
280
|
+
# inside the image read different spellings.
|
|
281
|
+
#
|
|
282
|
+
# @return [String] the ENV lines, empty when no proxy is configured
|
|
154
283
|
def dockerfile_proxy_config
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
if config[:https_proxy]
|
|
162
|
-
env_variables << "ENV https_proxy=#{config[:https_proxy]}\n"
|
|
163
|
-
env_variables << "ENV HTTPS_PROXY=#{config[:https_proxy]}\n"
|
|
164
|
-
end
|
|
284
|
+
%i{http_proxy https_proxy no_proxy}.map do |proxy_type|
|
|
285
|
+
proxy_env_vars(proxy_type)
|
|
286
|
+
end.join
|
|
287
|
+
end
|
|
165
288
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
289
|
+
# ENV lines for one proxy setting, in both spellings.
|
|
290
|
+
#
|
|
291
|
+
# @param proxy_type [Symbol] +:http_proxy+, +:https_proxy+, or
|
|
292
|
+
# +:no_proxy+
|
|
293
|
+
# @return [String] two ENV lines, or empty when that proxy is unset
|
|
294
|
+
def proxy_env_vars(proxy_type)
|
|
295
|
+
return "" unless config[proxy_type]
|
|
170
296
|
|
|
171
|
-
|
|
297
|
+
value = config[proxy_type]
|
|
298
|
+
"ENV #{proxy_type}=#{value}\nENV #{proxy_type.upcase}=#{value}\n"
|
|
172
299
|
end
|
|
173
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
174
300
|
end
|
|
175
|
-
# rubocop:enable Metrics/ModuleLength
|
|
301
|
+
# rubocop:enable Metrics/ModuleLength
|
|
176
302
|
end
|
|
177
303
|
end
|
|
178
304
|
end
|
|
@@ -16,10 +16,20 @@ require "kitchen/configurable"
|
|
|
16
16
|
|
|
17
17
|
module Kitchen
|
|
18
18
|
module Docker
|
|
19
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
19
20
|
module Helpers
|
|
21
|
+
# Per-distribution Dockerfile fragments that prepare a Linux image for SSH.
|
|
20
22
|
module DockerfileHelper
|
|
21
23
|
include Configurable
|
|
22
24
|
|
|
25
|
+
# Dockerfile lines that prepare the configured platform for SSH.
|
|
26
|
+
#
|
|
27
|
+
# Each distribution needs its own package manager invocation to install an
|
|
28
|
+
# SSH server and sudo, and its own host-key generation, which is why there
|
|
29
|
+
# is one method per family rather than a shared one.
|
|
30
|
+
#
|
|
31
|
+
# @return [String] the RUN lines for this platform
|
|
32
|
+
# @raise [Kitchen::ActionFailed] if the platform is not recognised
|
|
23
33
|
def dockerfile_platform
|
|
24
34
|
case config[:platform]
|
|
25
35
|
when "arch"
|
|
@@ -51,6 +61,9 @@ module Kitchen
|
|
|
51
61
|
end
|
|
52
62
|
end
|
|
53
63
|
|
|
64
|
+
# Dockerfile lines installing an SSH server and sudo on Arch Linux.
|
|
65
|
+
#
|
|
66
|
+
# @return [String] the RUN lines
|
|
54
67
|
def arch_platform
|
|
55
68
|
<<-CODE
|
|
56
69
|
RUN pacman --noconfirm -Sy archlinux-keyring
|
|
@@ -60,6 +73,9 @@ module Kitchen
|
|
|
60
73
|
CODE
|
|
61
74
|
end
|
|
62
75
|
|
|
76
|
+
# Dockerfile lines installing an SSH server and sudo on Debian and Ubuntu.
|
|
77
|
+
#
|
|
78
|
+
# @return [String] the RUN lines
|
|
63
79
|
def debian_platform
|
|
64
80
|
disable_upstart = <<-CODE
|
|
65
81
|
RUN [ ! -f "/sbin/initctl" ] || dpkg-divert --local --rename --add /sbin/initctl \
|
|
@@ -74,6 +90,9 @@ module Kitchen
|
|
|
74
90
|
config[:disable_upstart] ? disable_upstart + packages : packages
|
|
75
91
|
end
|
|
76
92
|
|
|
93
|
+
# Dockerfile lines installing an SSH server and sudo on Fedora.
|
|
94
|
+
#
|
|
95
|
+
# @return [String] the RUN lines
|
|
77
96
|
def fedora_platform
|
|
78
97
|
<<-CODE
|
|
79
98
|
ENV container=docker
|
|
@@ -83,6 +102,9 @@ module Kitchen
|
|
|
83
102
|
CODE
|
|
84
103
|
end
|
|
85
104
|
|
|
105
|
+
# Dockerfile lines installing an SSH server and sudo on Gentoo with Portage.
|
|
106
|
+
#
|
|
107
|
+
# @return [String] the RUN lines
|
|
86
108
|
def gentoo_platform
|
|
87
109
|
<<-CODE
|
|
88
110
|
RUN emerge-webrsync
|
|
@@ -91,6 +113,9 @@ module Kitchen
|
|
|
91
113
|
CODE
|
|
92
114
|
end
|
|
93
115
|
|
|
116
|
+
# Dockerfile lines installing an SSH server and sudo on Gentoo with Paludis.
|
|
117
|
+
#
|
|
118
|
+
# @return [String] the RUN lines
|
|
94
119
|
def gentoo_paludis_platform
|
|
95
120
|
<<-CODE
|
|
96
121
|
RUN cave sync
|
|
@@ -99,6 +124,9 @@ module Kitchen
|
|
|
99
124
|
CODE
|
|
100
125
|
end
|
|
101
126
|
|
|
127
|
+
# Dockerfile lines installing an SSH server and sudo on openSUSE and SLES.
|
|
128
|
+
#
|
|
129
|
+
# @return [String] the RUN lines
|
|
102
130
|
def opensuse_platform
|
|
103
131
|
<<-CODE
|
|
104
132
|
ENV container=docker
|
|
@@ -107,6 +135,9 @@ module Kitchen
|
|
|
107
135
|
CODE
|
|
108
136
|
end
|
|
109
137
|
|
|
138
|
+
# Dockerfile lines installing an SSH server and sudo on RHEL, CentOS, and Oracle Linux.
|
|
139
|
+
#
|
|
140
|
+
# @return [String] the RUN lines
|
|
110
141
|
def rhel_platform
|
|
111
142
|
<<-CODE
|
|
112
143
|
ENV container=docker
|
|
@@ -117,6 +148,9 @@ module Kitchen
|
|
|
117
148
|
CODE
|
|
118
149
|
end
|
|
119
150
|
|
|
151
|
+
# Dockerfile lines installing an SSH server and sudo on Amazon Linux.
|
|
152
|
+
#
|
|
153
|
+
# @return [String] the RUN lines
|
|
120
154
|
def amazonlinux_platform
|
|
121
155
|
<<-CODE
|
|
122
156
|
ENV container=docker
|
|
@@ -126,6 +160,9 @@ module Kitchen
|
|
|
126
160
|
CODE
|
|
127
161
|
end
|
|
128
162
|
|
|
163
|
+
# Dockerfile lines installing an SSH server and sudo on CentOS Stream.
|
|
164
|
+
#
|
|
165
|
+
# @return [String] the RUN lines
|
|
129
166
|
def centosstream_platform
|
|
130
167
|
<<-CODE
|
|
131
168
|
ENV container=docker
|
|
@@ -135,6 +172,9 @@ module Kitchen
|
|
|
135
172
|
CODE
|
|
136
173
|
end
|
|
137
174
|
|
|
175
|
+
# Dockerfile lines installing an SSH server and sudo on AlmaLinux.
|
|
176
|
+
#
|
|
177
|
+
# @return [String] the RUN lines
|
|
138
178
|
def almalinux_platform
|
|
139
179
|
<<-CODE
|
|
140
180
|
ENV container=docker
|
|
@@ -144,6 +184,9 @@ module Kitchen
|
|
|
144
184
|
CODE
|
|
145
185
|
end
|
|
146
186
|
|
|
187
|
+
# Dockerfile lines installing an SSH server and sudo on Rocky Linux.
|
|
188
|
+
#
|
|
189
|
+
# @return [String] the RUN lines
|
|
147
190
|
def rockylinux_platform
|
|
148
191
|
<<-CODE
|
|
149
192
|
ENV container=docker
|
|
@@ -153,6 +196,9 @@ module Kitchen
|
|
|
153
196
|
CODE
|
|
154
197
|
end
|
|
155
198
|
|
|
199
|
+
# Dockerfile lines installing an SSH server and sudo on Photon OS.
|
|
200
|
+
#
|
|
201
|
+
# @return [String] the RUN lines
|
|
156
202
|
def photonos_platform
|
|
157
203
|
<<-CODE
|
|
158
204
|
ENV container=docker
|
|
@@ -163,6 +209,14 @@ module Kitchen
|
|
|
163
209
|
CODE
|
|
164
210
|
end
|
|
165
211
|
|
|
212
|
+
# Dockerfile lines creating the login user and its SSH directory.
|
|
213
|
+
#
|
|
214
|
+
# The user gets passwordless sudo and +Defaults !requiretty+, because Test
|
|
215
|
+
# Kitchen runs commands non-interactively and sudo would otherwise refuse.
|
|
216
|
+
#
|
|
217
|
+
# @param username [String] the login user to create
|
|
218
|
+
# @param homedir [String] that user's home directory
|
|
219
|
+
# @return [String] the RUN lines
|
|
166
220
|
def dockerfile_base_linux(username, homedir)
|
|
167
221
|
<<-CODE
|
|
168
222
|
RUN if ! getent passwd #{username}; then \
|
|
@@ -15,24 +15,38 @@ require "fileutils" unless defined?(FileUtils)
|
|
|
15
15
|
|
|
16
16
|
module Kitchen
|
|
17
17
|
module Docker
|
|
18
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
18
19
|
module Helpers
|
|
20
|
+
# Local temp-file handling.
|
|
19
21
|
module FileHelper
|
|
22
|
+
# Writes a temp file, creating its parent directory if needed.
|
|
23
|
+
#
|
|
24
|
+
# Written with +File.write+, which opens, writes and closes in one call.
|
|
25
|
+
# The previous implementation assigned the open file back over the +file+
|
|
26
|
+
# parameter and closed it in an +ensure+, so when opening failed -- a
|
|
27
|
+
# read-only directory, a parent that is not a directory, a full disk --
|
|
28
|
+
# +file+ was still the path String and the ensure raised
|
|
29
|
+
# "undefined method 'close' for an instance of String", replacing the
|
|
30
|
+
# real error with a Ruby one. Its rescue did not help either: it caught
|
|
31
|
+
# +IOError+, while opening a file fails with +Errno+ classes, which are
|
|
32
|
+
# +SystemCallError+ and not +IOError+.
|
|
33
|
+
#
|
|
34
|
+
# @param file [String] path to write
|
|
35
|
+
# @param contents [String] what to write
|
|
36
|
+
# @return [void]
|
|
37
|
+
# @raise [RuntimeError] if the file cannot be written, naming the path
|
|
38
|
+
# and the underlying cause
|
|
20
39
|
def create_temp_file(file, contents)
|
|
21
40
|
debug("[Docker] Creating temp file #{file}")
|
|
22
41
|
debug("[Docker] --- Start Temp File Contents ---")
|
|
23
42
|
debug(contents)
|
|
24
43
|
debug("[Docker] --- End Temp File Contents ---")
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
rescue IOError => e
|
|
32
|
-
raise "Failed to write temp file. Error Details: #{e}"
|
|
33
|
-
ensure
|
|
34
|
-
file.close unless file.nil?
|
|
35
|
-
end
|
|
45
|
+
path = ::File.dirname(file)
|
|
46
|
+
::FileUtils.mkdir_p(path) unless ::Dir.exist?(path)
|
|
47
|
+
::File.write(file, contents)
|
|
48
|
+
rescue SystemCallError, IOError => e
|
|
49
|
+
raise "Failed to write temp file #{file}. Error Details: #{e}"
|
|
36
50
|
end
|
|
37
51
|
end
|
|
38
52
|
end
|
|
@@ -19,12 +19,22 @@ require_relative "container_helper"
|
|
|
19
19
|
|
|
20
20
|
module Kitchen
|
|
21
21
|
module Docker
|
|
22
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
22
23
|
module Helpers
|
|
24
|
+
# Building, inspecting, and removing Docker images.
|
|
23
25
|
module ImageHelper
|
|
24
26
|
include Configurable
|
|
25
27
|
include Kitchen::Docker::Helpers::CliHelper
|
|
26
28
|
include Kitchen::Docker::Helpers::ContainerHelper
|
|
27
29
|
|
|
30
|
+
# Pulls the built image's id out of `docker build` output.
|
|
31
|
+
#
|
|
32
|
+
# Scanned in reverse, and against several patterns, because the wording has
|
|
33
|
+
# changed across Docker and BuildKit versions.
|
|
34
|
+
#
|
|
35
|
+
# @param output [String] the build output
|
|
36
|
+
# @return [String] the image id
|
|
37
|
+
# @raise [Kitchen::ActionFailed] if no id could be found
|
|
28
38
|
def parse_image_id(output)
|
|
29
39
|
output.split("\n").reverse_each do |line|
|
|
30
40
|
if line =~ /writing image (sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i
|
|
@@ -44,6 +54,10 @@ module Kitchen
|
|
|
44
54
|
raise ActionFailed, "Could not parse Docker build output for image ID"
|
|
45
55
|
end
|
|
46
56
|
|
|
57
|
+
# Removes the built image, unless a container is still using it.
|
|
58
|
+
#
|
|
59
|
+
# @param state [Hash] instance state naming the image
|
|
60
|
+
# @return [void]
|
|
47
61
|
def remove_image(state)
|
|
48
62
|
image_id = state[:image_id]
|
|
49
63
|
if image_in_use?(state)
|
|
@@ -54,10 +68,40 @@ module Kitchen
|
|
|
54
68
|
end
|
|
55
69
|
end
|
|
56
70
|
|
|
71
|
+
# Whether any container was created from the image.
|
|
72
|
+
#
|
|
73
|
+
# Asked with a filter rather than by searching `docker ps -a` output for
|
|
74
|
+
# the id. That output abbreviates the IMAGE column to twelve characters,
|
|
75
|
+
# while state carries the full +sha256:+ digest, so the substring never
|
|
76
|
+
# matched and the answer was always false -- which defeated the guard
|
|
77
|
+
# entirely and let {#remove_image} run `docker rmi` against an image a
|
|
78
|
+
# container was still using.
|
|
79
|
+
#
|
|
80
|
+
# @param state [Hash] instance state naming the image
|
|
81
|
+
# @return [Boolean] whether any container references it
|
|
57
82
|
def image_in_use?(state)
|
|
58
|
-
|
|
83
|
+
return false unless state[:image_id]
|
|
84
|
+
|
|
85
|
+
output = docker_command("ps -a -q --filter ancestor=#{state[:image_id]}",
|
|
86
|
+
suppress_output: !logger.debug?)
|
|
87
|
+
|
|
88
|
+
# Matched line by line rather than by emptiness, so a warning docker
|
|
89
|
+
# writes to stderr is not mistaken for a container id.
|
|
90
|
+
output.lines.map(&:strip).any? do |line|
|
|
91
|
+
line.match?(/\A[0-9a-f]{12}(?:[0-9a-f]{52})?\z/)
|
|
92
|
+
end
|
|
59
93
|
end
|
|
60
94
|
|
|
95
|
+
# Builds the image from the given Dockerfile.
|
|
96
|
+
#
|
|
97
|
+
# The Dockerfile is written to a temp file and also passed on stdin, so the
|
|
98
|
+
# build works both with a build context and without one. The temp file is
|
|
99
|
+
# removed whether or not the build succeeded.
|
|
100
|
+
#
|
|
101
|
+
# @param state [Hash] instance state
|
|
102
|
+
# @param dockerfile [String] the Dockerfile contents
|
|
103
|
+
# @return [String] the new image's id
|
|
104
|
+
# @raise [Kitchen::ActionFailed] if the id cannot be parsed from the output
|
|
61
105
|
def build_image(state, dockerfile)
|
|
62
106
|
cmd = "build"
|
|
63
107
|
cmd << " --no-cache" unless config[:use_cache]
|
|
@@ -82,6 +126,8 @@ module Kitchen
|
|
|
82
126
|
parse_image_id(output)
|
|
83
127
|
end
|
|
84
128
|
|
|
129
|
+
# @param state [Hash] instance state naming the image
|
|
130
|
+
# @return [Boolean] whether the image is present locally
|
|
85
131
|
def image_exists?(state)
|
|
86
132
|
state[:image_id] && !!docker_command("inspect --type=image #{state[:image_id]}") rescue false
|
|
87
133
|
end
|
|
@@ -52,6 +52,7 @@ end
|
|
|
52
52
|
|
|
53
53
|
module Kitchen
|
|
54
54
|
module Docker
|
|
55
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
55
56
|
module Helpers
|
|
56
57
|
# Marker module included by the Docker transport Connection class.
|
|
57
58
|
# Actual verifier patches are applied directly to verifier classes above.
|
|
@@ -26,6 +26,7 @@ require_relative "../docker/helpers/cli_helper"
|
|
|
26
26
|
require_relative "../docker/helpers/container_helper"
|
|
27
27
|
|
|
28
28
|
module Kitchen
|
|
29
|
+
# Test Kitchen's driver plugins.
|
|
29
30
|
module Driver
|
|
30
31
|
# Docker driver for Kitchen.
|
|
31
32
|
#
|
|
@@ -111,28 +112,50 @@ module Kitchen
|
|
|
111
112
|
end
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Checks that the Docker CLI is installed and runnable.
|
|
116
|
+
#
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @raise [Kitchen::UserError] if the binary cannot be run
|
|
114
119
|
def verify_dependencies
|
|
115
120
|
run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
|
|
116
121
|
rescue
|
|
117
122
|
raise UserError, "You must first install the Docker CLI tool https://www.docker.com/get-started"
|
|
118
123
|
end
|
|
119
124
|
|
|
125
|
+
# Builds the image and starts the container.
|
|
126
|
+
#
|
|
127
|
+
# @param state [Hash] mutable instance state
|
|
128
|
+
# @return [void]
|
|
120
129
|
def create(state)
|
|
121
130
|
container.create(state)
|
|
122
131
|
|
|
123
132
|
wait_for_transport(state)
|
|
124
133
|
end
|
|
125
134
|
|
|
135
|
+
# Removes the container, and its image when +remove_images+ is set.
|
|
136
|
+
#
|
|
137
|
+
# @param state [Hash] instance state naming the container
|
|
138
|
+
# @return [void]
|
|
126
139
|
def destroy(state)
|
|
127
140
|
container.destroy(state)
|
|
128
141
|
end
|
|
129
142
|
|
|
143
|
+
# Waits for the transport to accept a connection, unless disabled.
|
|
144
|
+
#
|
|
145
|
+
# @param state [Hash] instance state describing how to connect
|
|
146
|
+
# @return [void]
|
|
130
147
|
def wait_for_transport(state)
|
|
131
148
|
if config[:wait_for_transport]
|
|
132
149
|
instance.transport.connection(state, &:wait_until_ready)
|
|
133
150
|
end
|
|
134
151
|
end
|
|
135
152
|
|
|
153
|
+
# The Docker image implied by the platform name.
|
|
154
|
+
#
|
|
155
|
+
# +ubuntu-22.04+ becomes +ubuntu:22.04+. CentOS is special-cased, since its
|
|
156
|
+
# images are tagged +centos7+ rather than +centos:7+.
|
|
157
|
+
#
|
|
158
|
+
# @return [String] an image reference
|
|
136
159
|
def default_image
|
|
137
160
|
platform, release = instance.platform.name.split("-")
|
|
138
161
|
if platform == "centos" && release
|
|
@@ -141,12 +164,16 @@ module Kitchen
|
|
|
141
164
|
release ? [platform, release].join(":") : platform
|
|
142
165
|
end
|
|
143
166
|
|
|
167
|
+
# @return [String] the platform family, e.g. +ubuntu+ from +ubuntu-22.04+
|
|
144
168
|
def default_platform
|
|
145
169
|
instance.platform.name.split("-").first
|
|
146
170
|
end
|
|
147
171
|
|
|
148
172
|
protected
|
|
149
173
|
|
|
174
|
+
# The container implementation for this platform.
|
|
175
|
+
#
|
|
176
|
+
# @return [Kitchen::Docker::Container] a Windows or Linux container
|
|
150
177
|
def container
|
|
151
178
|
@container ||= if windows_os?
|
|
152
179
|
Kitchen::Docker::Container::Windows.new(config)
|