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
data/Rakefile
CHANGED
|
@@ -16,4 +16,22 @@ rescue LoadError
|
|
|
16
16
|
puts "cookstyle/chefstyle is not available. (sudo) gem install cookstyle to do style checking."
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
begin
|
|
20
|
+
require "yard"
|
|
21
|
+
|
|
22
|
+
# Options and the file list live in .yardopts so that a bare `yard` from the
|
|
23
|
+
# command line produces exactly what `rake doc` does.
|
|
24
|
+
YARD::Rake::YardocTask.new(:doc)
|
|
25
|
+
|
|
26
|
+
desc "List anything in lib/ that is still undocumented"
|
|
27
|
+
task :doc_coverage do
|
|
28
|
+
sh "yard stats --list-undoc"
|
|
29
|
+
end
|
|
30
|
+
rescue LoadError
|
|
31
|
+
desc "Generate YARD documentation (not installed)"
|
|
32
|
+
task :doc do
|
|
33
|
+
abort "YARD is not installed. Run: bundle install"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
task default: %i{style test}
|
|
@@ -22,15 +22,28 @@ require_relative "../helpers/dockerfile_helper"
|
|
|
22
22
|
module Kitchen
|
|
23
23
|
module Docker
|
|
24
24
|
class Container
|
|
25
|
+
# A Linux container, reached over SSH.
|
|
26
|
+
#
|
|
27
|
+
# The generated image installs and runs an SSH server, and Test Kitchen
|
|
28
|
+
# connects to a published port with a generated key. That is why the
|
|
29
|
+
# Dockerfile here is so much larger than the Windows one.
|
|
25
30
|
class Linux < Kitchen::Docker::Container
|
|
26
31
|
include Kitchen::Docker::Helpers::DockerfileHelper
|
|
27
32
|
|
|
33
|
+
# Serializes SSH key generation across concurrently running instances,
|
|
34
|
+
# which share one key path.
|
|
28
35
|
MUTEX_FOR_SSH_KEYS = Mutex.new
|
|
29
36
|
|
|
37
|
+
# @param config [Hash] the driver configuration
|
|
30
38
|
def initialize(config)
|
|
31
39
|
super
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# Builds the image, runs the container, and publishes its SSH port.
|
|
43
|
+
#
|
|
44
|
+
# @param state [Hash] mutable instance state; gains +ssh_key+, +image_id+,
|
|
45
|
+
# +container_id+, +hostname+, and +port+
|
|
46
|
+
# @return [void]
|
|
34
47
|
def create(state)
|
|
35
48
|
super
|
|
36
49
|
|
|
@@ -44,6 +57,15 @@ module Kitchen
|
|
|
44
57
|
state[:port] = container_ssh_port(state)
|
|
45
58
|
end
|
|
46
59
|
|
|
60
|
+
# Runs a command in the container by uploading it as a shell script.
|
|
61
|
+
#
|
|
62
|
+
# The command is written to a temp file and executed with bash rather than
|
|
63
|
+
# passed on the command line, which keeps long converge scripts clear of
|
|
64
|
+
# argument-length and quoting limits.
|
|
65
|
+
#
|
|
66
|
+
# @param command [String] the shell code to run
|
|
67
|
+
# @return [String] the command's combined output
|
|
68
|
+
# @raise [RuntimeError] if the command fails
|
|
47
69
|
def execute(command)
|
|
48
70
|
# Create temp script file and upload files to container
|
|
49
71
|
debug("Executing command on Linux container (Platform: #{@config[:platform]})")
|
|
@@ -58,9 +80,6 @@ module Kitchen
|
|
|
58
80
|
debug("Uploading temp file #{temp_file} to #{remote_path} on container")
|
|
59
81
|
upload(temp_file, remote_path)
|
|
60
82
|
|
|
61
|
-
debug("Deleting temp file from local filesystem")
|
|
62
|
-
::File.delete(temp_file)
|
|
63
|
-
|
|
64
83
|
# Replace any environment variables used in the path and execute script file
|
|
65
84
|
debug("Executing temp script #{remote_path}/#{filename} on container")
|
|
66
85
|
remote_path = replace_env_variables(@config, remote_path)
|
|
@@ -68,10 +87,23 @@ module Kitchen
|
|
|
68
87
|
container_exec(@config, "/bin/bash #{remote_path}/#{filename}")
|
|
69
88
|
rescue => e
|
|
70
89
|
raise "Failed to execute command on Linux container. #{e}"
|
|
90
|
+
ensure
|
|
91
|
+
# Removed here rather than after the upload, so that a failure part
|
|
92
|
+
# way through does not leave the script behind in .kitchen/temp.
|
|
93
|
+
if temp_file && ::File.exist?(temp_file)
|
|
94
|
+
debug("Deleting temp file from local filesystem")
|
|
95
|
+
::File.delete(temp_file)
|
|
96
|
+
end
|
|
71
97
|
end
|
|
72
98
|
|
|
73
99
|
protected
|
|
74
100
|
|
|
101
|
+
# Generates the SSH keypair used to log into Linux containers.
|
|
102
|
+
#
|
|
103
|
+
# Guarded by {MUTEX_FOR_SSH_KEYS} because concurrent instances share one
|
|
104
|
+
# key path and would otherwise write the file while another reads it.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
75
107
|
def generate_keys
|
|
76
108
|
MUTEX_FOR_SSH_KEYS.synchronize do
|
|
77
109
|
if !File.exist?(@config[:public_key]) || !File.exist?(@config[:private_key])
|
|
@@ -90,13 +122,40 @@ module Kitchen
|
|
|
90
122
|
end
|
|
91
123
|
end
|
|
92
124
|
|
|
125
|
+
# Pulls the published port out of `docker port` output.
|
|
126
|
+
#
|
|
127
|
+
# One line is printed per published binding, and the host part varies:
|
|
128
|
+
#
|
|
129
|
+
# 0.0.0.0:32768
|
|
130
|
+
# [::]:32768
|
|
131
|
+
#
|
|
132
|
+
# The port is taken from the end of the line rather than by splitting on
|
|
133
|
+
# ":" from the left, because an IPv6 host contains colons of its own --
|
|
134
|
+
# splitting left-to-right returned "" for those, and "".to_i is 0, so a
|
|
135
|
+
# daemon publishing on IPv6 handed Test Kitchen port 0 to connect to.
|
|
136
|
+
#
|
|
137
|
+
# @param output [String] e.g. +"0.0.0.0:32768\n[::]:32768\n"+
|
|
138
|
+
# @return [Integer] the host-side port
|
|
139
|
+
# @raise [Kitchen::ActionFailed] if no port could be found
|
|
93
140
|
def parse_container_ssh_port(output)
|
|
94
|
-
|
|
141
|
+
port = output.lines.filter_map { |line| line[/:(\d+)\s*\z/, 1] }.first
|
|
142
|
+
|
|
143
|
+
if port.nil?
|
|
144
|
+
raise ActionFailed, "Could not parse Docker port output for container SSH port: #{output.inspect}"
|
|
145
|
+
end
|
|
146
|
+
|
|
95
147
|
port.to_i
|
|
96
|
-
rescue => e
|
|
97
|
-
raise ActionFailed, "Could not parse Docker port output for container SSH port. #{e}"
|
|
98
148
|
end
|
|
99
149
|
|
|
150
|
+
# The port Test Kitchen should connect to for SSH.
|
|
151
|
+
#
|
|
152
|
+
# On the internal Docker network the container is reached directly, so the
|
|
153
|
+
# unmapped port 22 is correct; otherwise Docker's published mapping is
|
|
154
|
+
# looked up.
|
|
155
|
+
#
|
|
156
|
+
# @param state [Hash] instance state naming the container
|
|
157
|
+
# @return [Integer] the port to connect on
|
|
158
|
+
# @raise [Kitchen::ActionFailed] if no SSH port is mapped
|
|
100
159
|
def container_ssh_port(state)
|
|
101
160
|
return 22 if @config[:use_internal_docker_network]
|
|
102
161
|
|
|
@@ -106,6 +165,14 @@ module Kitchen
|
|
|
106
165
|
raise ActionFailed, "Docker reports container has no ssh port mapped. #{e}"
|
|
107
166
|
end
|
|
108
167
|
|
|
168
|
+
# Builds the Dockerfile for a Linux container.
|
|
169
|
+
#
|
|
170
|
+
# A configured +dockerfile+ is used as-is after ERB rendering. Otherwise
|
|
171
|
+
# one is generated: the base image, proxy settings, the platform's own
|
|
172
|
+
# package setup, any +provision_command+ entries, and the generated public
|
|
173
|
+
# key appended to the login user's authorized_keys.
|
|
174
|
+
#
|
|
175
|
+
# @return [String] the Dockerfile contents
|
|
109
176
|
def dockerfile
|
|
110
177
|
return dockerfile_template if @config[:dockerfile]
|
|
111
178
|
|
|
@@ -18,11 +18,25 @@ require_relative "../container"
|
|
|
18
18
|
module Kitchen
|
|
19
19
|
module Docker
|
|
20
20
|
class Container
|
|
21
|
+
# A Windows container, driven through `docker exec`.
|
|
22
|
+
#
|
|
23
|
+
# There is no SSH server and no key to inject: commands are uploaded as
|
|
24
|
+
# PowerShell scripts and executed in the container directly, so no port
|
|
25
|
+
# is published.
|
|
21
26
|
class Windows < Kitchen::Docker::Container
|
|
27
|
+
# @param config [Hash] the driver configuration
|
|
22
28
|
def initialize(config)
|
|
23
29
|
super
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
# Builds the image and runs the container.
|
|
33
|
+
#
|
|
34
|
+
# No port is published: Windows containers are driven through `docker exec`
|
|
35
|
+
# rather than SSH, so there is nothing to map.
|
|
36
|
+
#
|
|
37
|
+
# @param state [Hash] mutable instance state; gains +username+, +image_id+,
|
|
38
|
+
# +container_id+, and +hostname+
|
|
39
|
+
# @return [void]
|
|
26
40
|
def create(state)
|
|
27
41
|
super
|
|
28
42
|
|
|
@@ -33,6 +47,11 @@ module Kitchen
|
|
|
33
47
|
state[:hostname] = hostname(state)
|
|
34
48
|
end
|
|
35
49
|
|
|
50
|
+
# Runs a command in the container by uploading it as a PowerShell script.
|
|
51
|
+
#
|
|
52
|
+
# @param command [String] the PowerShell code to run
|
|
53
|
+
# @return [String] the command's combined output
|
|
54
|
+
# @raise [RuntimeError] if the command fails
|
|
36
55
|
def execute(command)
|
|
37
56
|
# Create temp script file and upload files to container
|
|
38
57
|
debug("Executing command on Windows container")
|
|
@@ -47,9 +66,6 @@ module Kitchen
|
|
|
47
66
|
debug("Uploading temp file #{temp_file} to #{remote_path} on container")
|
|
48
67
|
upload(temp_file, remote_path)
|
|
49
68
|
|
|
50
|
-
debug("Deleting temp file from local filesystem")
|
|
51
|
-
::File.delete(temp_file)
|
|
52
|
-
|
|
53
69
|
# Replace any environment variables used in the path and execute script file
|
|
54
70
|
debug("Executing temp script #{remote_path}\\#{filename} on container")
|
|
55
71
|
remote_path = replace_env_variables(@config, remote_path)
|
|
@@ -58,10 +74,25 @@ module Kitchen
|
|
|
58
74
|
container_exec(@config, cmd)
|
|
59
75
|
rescue => e
|
|
60
76
|
raise "Failed to execute command on Windows container. #{e}"
|
|
77
|
+
ensure
|
|
78
|
+
# Removed here rather than after the upload, so that a failure part
|
|
79
|
+
# way through does not leave the script behind in .kitchen/temp.
|
|
80
|
+
if temp_file && ::File.exist?(temp_file)
|
|
81
|
+
debug("Deleting temp file from local filesystem")
|
|
82
|
+
::File.delete(temp_file)
|
|
83
|
+
end
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
protected
|
|
64
87
|
|
|
88
|
+
# Builds the Dockerfile for a Windows container.
|
|
89
|
+
#
|
|
90
|
+
# Much shorter than the Linux equivalent: there is no SSH server and no
|
|
91
|
+
# key to inject, so only the base image, proxy settings, and any
|
|
92
|
+
# +provision_command+ entries are emitted.
|
|
93
|
+
#
|
|
94
|
+
# @return [String] the Dockerfile contents
|
|
95
|
+
# @raise [Kitchen::ActionFailed] if the platform is not +windows+
|
|
65
96
|
def dockerfile
|
|
66
97
|
raise ActionFailed, "Unknown platform '#{@config[:platform]}'" unless @config[:platform] == "windows"
|
|
67
98
|
return dockerfile_template if @config[:dockerfile]
|
|
@@ -18,20 +18,47 @@ require_relative "helpers/image_helper"
|
|
|
18
18
|
|
|
19
19
|
module Kitchen
|
|
20
20
|
module Docker
|
|
21
|
+
# Base class for the container the instance under test runs in.
|
|
22
|
+
#
|
|
23
|
+
# Holds the behaviour that is the same on every platform -- checking
|
|
24
|
+
# whether the container exists, removing it, working out its address, and
|
|
25
|
+
# copying files in. {Linux} and {Windows} add how the image is built and
|
|
26
|
+
# how commands are run, which share almost nothing.
|
|
21
27
|
class Container
|
|
22
28
|
include Kitchen::Docker::Helpers::CliHelper
|
|
23
29
|
include Kitchen::Docker::Helpers::ContainerHelper
|
|
24
30
|
include Kitchen::Docker::Helpers::FileHelper
|
|
25
31
|
include Kitchen::Docker::Helpers::ImageHelper
|
|
26
32
|
|
|
33
|
+
# @param config [Hash] the driver or transport configuration
|
|
27
34
|
def initialize(config)
|
|
28
35
|
@config = config
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Checks the container named in state and records the login user.
|
|
39
|
+
#
|
|
40
|
+
# A state file naming a container that no longer exists is an error rather
|
|
41
|
+
# than something to build over, because the stale id usually means the
|
|
42
|
+
# container was removed behind Test Kitchen's back and silently creating a
|
|
43
|
+
# new one would hide that.
|
|
44
|
+
#
|
|
45
|
+
# A container that exists but has stopped is also an error, and a separate
|
|
46
|
+
# one: it is still there to be cleaned up, so the message points at
|
|
47
|
+
# `kitchen destroy` rather than claiming the container is gone.
|
|
48
|
+
#
|
|
49
|
+
# @param state [Hash] mutable instance state; gains +username+
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @raise [Kitchen::ActionFailed] if state names a container that is gone,
|
|
52
|
+
# or one that exists but is not running
|
|
31
53
|
def create(state)
|
|
32
54
|
if container_exists?(state)
|
|
55
|
+
unless container_running?(state)
|
|
56
|
+
raise ActionFailed, "Container ID #{state[:container_id]} was found in the kitchen state data, " \
|
|
57
|
+
"but the container is not running. Run `kitchen destroy` to remove it."
|
|
58
|
+
end
|
|
59
|
+
|
|
33
60
|
info("Container ID #{state[:container_id]} already exists.")
|
|
34
|
-
elsif
|
|
61
|
+
elsif state[:container_id]
|
|
35
62
|
raise ActionFailed, "Container ID #{state[:container_id]} was found in the kitchen state data, " \
|
|
36
63
|
"but the container does not exist."
|
|
37
64
|
end
|
|
@@ -39,6 +66,10 @@ module Kitchen
|
|
|
39
66
|
state[:username] = @config[:username]
|
|
40
67
|
end
|
|
41
68
|
|
|
69
|
+
# Removes the container, and its image when +remove_images+ is set.
|
|
70
|
+
#
|
|
71
|
+
# @param state [Hash] instance state naming the container
|
|
72
|
+
# @return [void]
|
|
42
73
|
def destroy(state)
|
|
43
74
|
info("[Docker] Destroying Docker container #{state[:container_id]}") if state[:container_id]
|
|
44
75
|
remove_container(state) if container_exists?(state)
|
|
@@ -48,6 +79,14 @@ module Kitchen
|
|
|
48
79
|
end
|
|
49
80
|
end
|
|
50
81
|
|
|
82
|
+
# Works out the address Test Kitchen should connect to.
|
|
83
|
+
#
|
|
84
|
+
# A remote Docker socket means the container is reachable at the socket's
|
|
85
|
+
# own host; +use_internal_docker_network+ means its container IP; anything
|
|
86
|
+
# else is a published port on localhost.
|
|
87
|
+
#
|
|
88
|
+
# @param state [Hash] instance state naming the container
|
|
89
|
+
# @return [String] a hostname or IP address
|
|
51
90
|
def hostname(state)
|
|
52
91
|
hostname = "localhost"
|
|
53
92
|
|
|
@@ -60,6 +99,11 @@ module Kitchen
|
|
|
60
99
|
hostname
|
|
61
100
|
end
|
|
62
101
|
|
|
102
|
+
# Copies local files into the container.
|
|
103
|
+
#
|
|
104
|
+
# @param locals [String, Array<String>] one path or several
|
|
105
|
+
# @param remote [String] destination path inside the container
|
|
106
|
+
# @return [Array<String>] the files copied
|
|
63
107
|
def upload(locals, remote)
|
|
64
108
|
files = locals
|
|
65
109
|
files = Array(locals) unless locals.is_a?(Array)
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
+
# Test Kitchen's top-level namespace.
|
|
16
17
|
module Kitchen
|
|
18
|
+
# Everything belonging to the kitchen-docker plugin.
|
|
17
19
|
module Docker
|
|
18
|
-
#
|
|
19
|
-
DOCKER_VERSION = "3.3.
|
|
20
|
+
# The version of the kitchen-docker gem.
|
|
21
|
+
DOCKER_VERSION = "3.3.3".freeze
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -17,13 +17,22 @@ require "erb" unless defined?(Erb)
|
|
|
17
17
|
|
|
18
18
|
module Kitchen
|
|
19
19
|
module Docker
|
|
20
|
+
# Evaluation context for a user-supplied Dockerfile template.
|
|
21
|
+
#
|
|
22
|
+
# Each configuration key becomes an instance variable, so a template can
|
|
23
|
+
# refer to +@image+, +@username+, and the rest.
|
|
20
24
|
class ERBContext
|
|
25
|
+
# Exposes each config key to the template as an instance variable, so a
|
|
26
|
+
# custom Dockerfile can refer to +@image+, +@username+, and the rest.
|
|
27
|
+
#
|
|
28
|
+
# @param config [Hash] the configuration to expose
|
|
21
29
|
def initialize(config = {})
|
|
22
30
|
config.each do |key, value|
|
|
23
31
|
instance_variable_set("@" + key.to_s, value)
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# @return [Binding] a binding for ERB to evaluate the template in
|
|
27
36
|
def get_binding
|
|
28
37
|
binding
|
|
29
38
|
end
|
|
@@ -15,32 +15,71 @@ require "kitchen"
|
|
|
15
15
|
require "kitchen/configurable"
|
|
16
16
|
require "kitchen/logging"
|
|
17
17
|
require "kitchen/shell_out"
|
|
18
|
+
require "shellwords" unless defined?(Shellwords)
|
|
18
19
|
|
|
19
20
|
module Kitchen
|
|
20
21
|
module Docker
|
|
22
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
21
23
|
module Helpers
|
|
22
|
-
# rubocop:disable Metrics/ModuleLength
|
|
24
|
+
# rubocop:disable Metrics/ModuleLength
|
|
25
|
+
# Builds and runs docker CLI command lines.
|
|
23
26
|
module CliHelper
|
|
24
27
|
include Configurable
|
|
25
28
|
include Logging
|
|
26
29
|
include ShellOut
|
|
27
30
|
|
|
31
|
+
# Escapes a configured value for the shell.
|
|
32
|
+
#
|
|
33
|
+
# Docker command lines are assembled as strings and handed to a shell, so
|
|
34
|
+
# a value that is a single datum -- a path, a name, a port mapping -- has
|
|
35
|
+
# to be escaped, or a space inside it is read as an argument separator and
|
|
36
|
+
# the rest of the value is taken as the image name.
|
|
37
|
+
#
|
|
38
|
+
# Values that are deliberately shell fragments are *not* escaped:
|
|
39
|
+
# +run_command+, +run_options+, +build_options+, and the command handed to
|
|
40
|
+
# `docker exec` are all documented as accepting flags and arguments.
|
|
41
|
+
#
|
|
42
|
+
# Escaping is a no-op for ordinary values -- +db:db+ and +8.8.8.8+ come
|
|
43
|
+
# back unchanged -- so this only alters command lines that were already
|
|
44
|
+
# broken.
|
|
45
|
+
#
|
|
46
|
+
# @param value [#to_s] the configured value
|
|
47
|
+
# @return [String] the value, safe to interpolate into a command line
|
|
48
|
+
def shell_escape(value)
|
|
49
|
+
Shellwords.escape(value.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
28
52
|
# rubocop:disable Metrics/AbcSize
|
|
53
|
+
|
|
54
|
+
# Runs a docker CLI command with the configured connection flags.
|
|
55
|
+
#
|
|
56
|
+
# @param cmd [String] the docker subcommand and its arguments
|
|
57
|
+
# @param options [Hash] shell-out options
|
|
58
|
+
# @return [String] the command's combined stdout and stderr
|
|
29
59
|
def docker_command(cmd, options = {})
|
|
30
60
|
docker = config[:binary].dup
|
|
31
|
-
docker << " -H #{config[:socket]}" if config[:socket]
|
|
61
|
+
docker << " -H #{shell_escape(config[:socket])}" if config[:socket]
|
|
32
62
|
docker << " --tls" if config[:tls]
|
|
33
63
|
docker << " --tlsverify" if config[:tls_verify]
|
|
34
|
-
docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
|
|
35
|
-
docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
|
|
36
|
-
docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
|
|
64
|
+
docker << " --tlscacert=#{shell_escape(config[:tls_cacert])}" if config[:tls_cacert]
|
|
65
|
+
docker << " --tlscert=#{shell_escape(config[:tls_cert])}" if config[:tls_cert]
|
|
66
|
+
docker << " --tlskey=#{shell_escape(config[:tls_key])}" if config[:tls_key]
|
|
37
67
|
logger.debug("docker_command: #{docker} #{cmd} shell_opts: #{docker_shell_opts(options)}")
|
|
38
68
|
run_command("#{docker} #{cmd}", docker_shell_opts(options))
|
|
39
69
|
end
|
|
40
70
|
# rubocop:enable Metrics/AbcSize
|
|
41
71
|
|
|
42
|
-
# Copied from kitchen because we need stderr
|
|
43
72
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
73
|
+
|
|
74
|
+
# Runs a shell command, returning stderr as well as stdout.
|
|
75
|
+
#
|
|
76
|
+
# Test Kitchen's own +run_command+ discards stderr, but docker writes build
|
|
77
|
+
# progress and image ids there, so this reimplements it to keep both.
|
|
78
|
+
#
|
|
79
|
+
# @param cmd [String] the command to run
|
|
80
|
+
# @param options [Hash] shell-out options
|
|
81
|
+
# @return [String] combined stdout and stderr
|
|
82
|
+
# @raise [Kitchen::ShellCommandFailed] if the command exits non-zero
|
|
44
83
|
def run_command(cmd, options = {})
|
|
45
84
|
if options.fetch(:use_sudo, false)
|
|
46
85
|
cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
|
|
@@ -62,44 +101,57 @@ module Kitchen
|
|
|
62
101
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
63
102
|
|
|
64
103
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
104
|
+
|
|
105
|
+
# Builds the `docker run` command line from the configuration.
|
|
106
|
+
#
|
|
107
|
+
# @param image_id [String] the image to run
|
|
108
|
+
# @param transport_port [Integer, nil] container port to publish, if any
|
|
109
|
+
# @return [String] the docker subcommand and its arguments
|
|
65
110
|
def build_run_command(image_id, transport_port = nil)
|
|
66
111
|
cmd = "run -d"
|
|
67
112
|
cmd << " -i" if config[:interactive]
|
|
68
113
|
cmd << " -t" if config[:tty]
|
|
69
114
|
cmd << build_env_variable_args(config[:env_variables]) if config[:env_variables]
|
|
70
115
|
cmd << " -p #{transport_port}" unless transport_port.nil?
|
|
71
|
-
Array(config[:forward]).each { |port| cmd << " -p #{port}" }
|
|
72
|
-
Array(config[:dns]).each { |dns| cmd << " --dns #{dns}" }
|
|
73
|
-
Array(config[:add_host]).each { |host, ip| cmd << " --add-host=#{host}:#{ip}" }
|
|
74
|
-
Array(config[:volume]).each { |volume| cmd << " -v #{volume}" }
|
|
75
|
-
Array(config[:volumes_from]).each { |container| cmd << " --volumes-from #{container}" }
|
|
76
|
-
Array(config[:links]).each { |link| cmd << " --link #{link}" }
|
|
77
|
-
Array(config[:devices]).each { |device| cmd << " --device #{device}" }
|
|
78
|
-
Array(config[:mount]).each { |mount| cmd << " --mount #{mount}" }
|
|
79
|
-
Array(config[:tmpfs]).each { |tmpfs| cmd << " --tmpfs #{tmpfs}" }
|
|
80
|
-
cmd << " --name #{config[:instance_name]}" if config[:instance_name]
|
|
116
|
+
Array(config[:forward]).each { |port| cmd << " -p #{shell_escape(port)}" }
|
|
117
|
+
Array(config[:dns]).each { |dns| cmd << " --dns #{shell_escape(dns)}" }
|
|
118
|
+
Array(config[:add_host]).each { |host, ip| cmd << " --add-host=#{shell_escape("#{host}:#{ip}")}" }
|
|
119
|
+
Array(config[:volume]).each { |volume| cmd << " -v #{shell_escape(volume)}" }
|
|
120
|
+
Array(config[:volumes_from]).each { |container| cmd << " --volumes-from #{shell_escape(container)}" }
|
|
121
|
+
Array(config[:links]).each { |link| cmd << " --link #{shell_escape(link)}" }
|
|
122
|
+
Array(config[:devices]).each { |device| cmd << " --device #{shell_escape(device)}" }
|
|
123
|
+
Array(config[:mount]).each { |mount| cmd << " --mount #{shell_escape(mount)}" }
|
|
124
|
+
Array(config[:tmpfs]).each { |tmpfs| cmd << " --tmpfs #{shell_escape(tmpfs)}" }
|
|
125
|
+
cmd << " --name #{shell_escape(config[:instance_name])}" if config[:instance_name]
|
|
81
126
|
cmd << " -P" if config[:publish_all]
|
|
82
|
-
cmd << " -h #{config[:hostname]}" if config[:hostname]
|
|
83
|
-
cmd << " -m #{config[:memory]}" if config[:memory]
|
|
84
|
-
cmd << " -c #{config[:cpu]}" if config[:cpu]
|
|
85
|
-
cmd << " --gpus #{config[:gpus]}" if config[:gpus]
|
|
86
|
-
cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
|
|
87
|
-
cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
|
|
127
|
+
cmd << " -h #{shell_escape(config[:hostname])}" if config[:hostname]
|
|
128
|
+
cmd << " -m #{shell_escape(config[:memory])}" if config[:memory]
|
|
129
|
+
cmd << " -c #{shell_escape(config[:cpu])}" if config[:cpu]
|
|
130
|
+
cmd << " --gpus #{shell_escape(config[:gpus])}" if config[:gpus]
|
|
131
|
+
cmd << " -e http_proxy=#{shell_escape(config[:http_proxy])}" if config[:http_proxy]
|
|
132
|
+
cmd << " -e https_proxy=#{shell_escape(config[:https_proxy])}" if config[:https_proxy]
|
|
88
133
|
cmd << " --privileged" if config[:privileged]
|
|
89
|
-
cmd << " --isolation #{config[:isolation]}" if config[:isolation]
|
|
90
|
-
Array(config[:cap_add]).each { |cap| cmd << " --cap-add=#{cap}" } if config[:cap_add]
|
|
91
|
-
Array(config[:cap_drop]).each { |cap| cmd << " --cap-drop=#{cap}" } if config[:cap_drop]
|
|
92
|
-
Array(config[:security_opt]).each { |opt| cmd << " --security-opt=#{opt}" } if config[:security_opt]
|
|
93
|
-
cmd << " --platform=#{config[:docker_platform]}" if config[:docker_platform]
|
|
134
|
+
cmd << " --isolation #{shell_escape(config[:isolation])}" if config[:isolation]
|
|
135
|
+
Array(config[:cap_add]).each { |cap| cmd << " --cap-add=#{shell_escape(cap)}" } if config[:cap_add]
|
|
136
|
+
Array(config[:cap_drop]).each { |cap| cmd << " --cap-drop=#{shell_escape(cap)}" } if config[:cap_drop]
|
|
137
|
+
Array(config[:security_opt]).each { |opt| cmd << " --security-opt=#{shell_escape(opt)}" } if config[:security_opt]
|
|
138
|
+
cmd << " --platform=#{shell_escape(config[:docker_platform])}" if config[:docker_platform]
|
|
94
139
|
extra_run_options = config_to_options(config[:run_options])
|
|
95
140
|
cmd << " #{extra_run_options}" unless extra_run_options.empty?
|
|
96
|
-
|
|
141
|
+
# run_command is a command line, not a single value, so it stays raw.
|
|
142
|
+
cmd << " #{shell_escape(image_id)} #{config[:run_command]}"
|
|
97
143
|
logger.debug("build_run_command: #{cmd}")
|
|
98
144
|
cmd
|
|
99
145
|
end
|
|
100
146
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
101
147
|
|
|
102
148
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
149
|
+
|
|
150
|
+
# Builds a `docker exec` command line from the configuration.
|
|
151
|
+
#
|
|
152
|
+
# @param state [Hash] instance state naming the container
|
|
153
|
+
# @param command [String] the command to run inside it
|
|
154
|
+
# @return [String] the docker subcommand and its arguments
|
|
103
155
|
def build_exec_command(state, command)
|
|
104
156
|
cmd = "exec"
|
|
105
157
|
cmd << " -d" if config[:detach]
|
|
@@ -107,22 +159,33 @@ module Kitchen
|
|
|
107
159
|
cmd << " --privileged" if config[:privileged]
|
|
108
160
|
cmd << " -t" if config[:tty]
|
|
109
161
|
cmd << " -i" if config[:interactive]
|
|
110
|
-
cmd << " -u #{config[:username]}" if config[:username]
|
|
111
|
-
cmd << " -w #{config[:working_dir]}" if config[:working_dir]
|
|
112
|
-
cmd << " #{state[:container_id]}"
|
|
162
|
+
cmd << " -u #{shell_escape(config[:username])}" if config[:username]
|
|
163
|
+
cmd << " -w #{shell_escape(config[:working_dir])}" if config[:working_dir]
|
|
164
|
+
cmd << " #{shell_escape(state[:container_id])}"
|
|
165
|
+
# command is a command line, not a single value, so it stays raw.
|
|
113
166
|
cmd << " #{command}"
|
|
114
167
|
logger.debug("build_exec_command: #{cmd}")
|
|
115
168
|
cmd
|
|
116
169
|
end
|
|
117
170
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
118
171
|
|
|
172
|
+
# Builds a `docker cp` command line.
|
|
173
|
+
#
|
|
174
|
+
# @param local_file [String] source path
|
|
175
|
+
# @param remote_file [String] destination, as +container:path+
|
|
176
|
+
# @param opts [Hash] +:archive+ to preserve ownership and mode
|
|
177
|
+
# @return [String] the docker subcommand and its arguments
|
|
119
178
|
def build_copy_command(local_file, remote_file, opts = {})
|
|
120
179
|
cmd = "cp"
|
|
121
180
|
cmd << " -a" if opts[:archive]
|
|
122
|
-
cmd << " #{local_file} #{remote_file}"
|
|
181
|
+
cmd << " #{shell_escape(local_file)} #{shell_escape(remote_file)}"
|
|
123
182
|
cmd
|
|
124
183
|
end
|
|
125
184
|
|
|
185
|
+
# Wraps PowerShell code so it can be run through `docker exec`.
|
|
186
|
+
#
|
|
187
|
+
# @param args [String] the PowerShell arguments
|
|
188
|
+
# @return [String] the full powershell invocation
|
|
126
189
|
def build_powershell_command(args)
|
|
127
190
|
cmd = "powershell -ExecutionPolicy Bypass -NoLogo "
|
|
128
191
|
cmd << args
|
|
@@ -130,17 +193,24 @@ module Kitchen
|
|
|
130
193
|
cmd
|
|
131
194
|
end
|
|
132
195
|
|
|
196
|
+
# Turns a hash of environment variables into `-e` flags.
|
|
197
|
+
#
|
|
198
|
+
# @param vars [Hash] variable names to values
|
|
199
|
+
# @return [String] the flags, each preceded by a space
|
|
200
|
+
# @raise [Kitchen::ActionFailed] if given something other than a Hash
|
|
133
201
|
def build_env_variable_args(vars)
|
|
134
202
|
raise ActionFailed, "Environment variables are not of a Hash type" unless vars.is_a?(Hash)
|
|
135
203
|
|
|
136
204
|
args = ""
|
|
137
205
|
vars.each do |k, v|
|
|
138
|
-
args << " -e #{k.to_s.strip}
|
|
206
|
+
args << " -e #{shell_escape("#{k.to_s.strip}=#{v.to_s.strip}")}"
|
|
139
207
|
end
|
|
140
208
|
|
|
141
209
|
args
|
|
142
210
|
end
|
|
143
211
|
|
|
212
|
+
# @return [String] the platform's null device, +NUL+ on Windows and
|
|
213
|
+
# +/dev/null+ everywhere else
|
|
144
214
|
def dev_null
|
|
145
215
|
case RbConfig::CONFIG["host_os"]
|
|
146
216
|
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
@@ -150,6 +220,13 @@ module Kitchen
|
|
|
150
220
|
end
|
|
151
221
|
end
|
|
152
222
|
|
|
223
|
+
# Normalizes shell-out options for a docker command.
|
|
224
|
+
#
|
|
225
|
+
# Translates +:suppress_output+ into silencing the live stream, and removes
|
|
226
|
+
# it, since Mixlib::ShellOut would reject the unknown key.
|
|
227
|
+
#
|
|
228
|
+
# @param options [Hash] the options to normalize
|
|
229
|
+
# @return [Hash] options Mixlib::ShellOut accepts
|
|
153
230
|
def docker_shell_opts(options = {})
|
|
154
231
|
options[:live_stream] = nil if options[:suppress_output]
|
|
155
232
|
options.delete(:suppress_output)
|
|
@@ -178,7 +255,7 @@ module Kitchen
|
|
|
178
255
|
end
|
|
179
256
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
180
257
|
end
|
|
181
|
-
# rubocop:enable Metrics/ModuleLength
|
|
258
|
+
# rubocop:enable Metrics/ModuleLength
|
|
182
259
|
end
|
|
183
260
|
end
|
|
184
261
|
end
|