kitchen-docker 3.3.0 → 3.3.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/.rubocop.yml +0 -1
- data/.yardopts +11 -0
- data/CHANGELOG.md +381 -21
- 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 +50 -0
- data/lib/kitchen/docker/container/windows.rb +27 -0
- data/lib/kitchen/docker/container.rb +34 -0
- 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 +56 -3
- data/lib/kitchen/docker/helpers/container_helper.rb +91 -19
- data/lib/kitchen/docker/helpers/dockerfile_helper.rb +54 -0
- data/lib/kitchen/docker/helpers/file_helper.rb +8 -0
- data/lib/kitchen/docker/helpers/image_helper.rb +28 -0
- 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 +312 -0
- data/spec/container_helper_spec.rb +178 -0
- data/spec/dockerfile_helper_spec.rb +97 -61
- data/spec/erb_context_spec.rb +60 -0
- data/spec/image_helper_spec.rb +142 -0
- data/spec/linux_container_spec.rb +194 -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 +13 -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]})")
|
|
@@ -72,6 +94,12 @@ module Kitchen
|
|
|
72
94
|
|
|
73
95
|
protected
|
|
74
96
|
|
|
97
|
+
# Generates the SSH keypair used to log into Linux containers.
|
|
98
|
+
#
|
|
99
|
+
# Guarded by {MUTEX_FOR_SSH_KEYS} because concurrent instances share one
|
|
100
|
+
# key path and would otherwise write the file while another reads it.
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
75
103
|
def generate_keys
|
|
76
104
|
MUTEX_FOR_SSH_KEYS.synchronize do
|
|
77
105
|
if !File.exist?(@config[:public_key]) || !File.exist?(@config[:private_key])
|
|
@@ -90,6 +118,11 @@ module Kitchen
|
|
|
90
118
|
end
|
|
91
119
|
end
|
|
92
120
|
|
|
121
|
+
# Pulls the published port out of `docker port` output.
|
|
122
|
+
#
|
|
123
|
+
# @param output [String] e.g. +"0.0.0.0:32768"+
|
|
124
|
+
# @return [Integer] the host-side port
|
|
125
|
+
# @raise [Kitchen::ActionFailed] if the output cannot be parsed
|
|
93
126
|
def parse_container_ssh_port(output)
|
|
94
127
|
_host, port = output.split(":")
|
|
95
128
|
port.to_i
|
|
@@ -97,6 +130,15 @@ module Kitchen
|
|
|
97
130
|
raise ActionFailed, "Could not parse Docker port output for container SSH port. #{e}"
|
|
98
131
|
end
|
|
99
132
|
|
|
133
|
+
# The port Test Kitchen should connect to for SSH.
|
|
134
|
+
#
|
|
135
|
+
# On the internal Docker network the container is reached directly, so the
|
|
136
|
+
# unmapped port 22 is correct; otherwise Docker's published mapping is
|
|
137
|
+
# looked up.
|
|
138
|
+
#
|
|
139
|
+
# @param state [Hash] instance state naming the container
|
|
140
|
+
# @return [Integer] the port to connect on
|
|
141
|
+
# @raise [Kitchen::ActionFailed] if no SSH port is mapped
|
|
100
142
|
def container_ssh_port(state)
|
|
101
143
|
return 22 if @config[:use_internal_docker_network]
|
|
102
144
|
|
|
@@ -106,6 +148,14 @@ module Kitchen
|
|
|
106
148
|
raise ActionFailed, "Docker reports container has no ssh port mapped. #{e}"
|
|
107
149
|
end
|
|
108
150
|
|
|
151
|
+
# Builds the Dockerfile for a Linux container.
|
|
152
|
+
#
|
|
153
|
+
# A configured +dockerfile+ is used as-is after ERB rendering. Otherwise
|
|
154
|
+
# one is generated: the base image, proxy settings, the platform's own
|
|
155
|
+
# package setup, any +provision_command+ entries, and the generated public
|
|
156
|
+
# key appended to the login user's authorized_keys.
|
|
157
|
+
#
|
|
158
|
+
# @return [String] the Dockerfile contents
|
|
109
159
|
def dockerfile
|
|
110
160
|
return dockerfile_template if @config[:dockerfile]
|
|
111
161
|
|
|
@@ -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")
|
|
@@ -62,6 +81,14 @@ module Kitchen
|
|
|
62
81
|
|
|
63
82
|
protected
|
|
64
83
|
|
|
84
|
+
# Builds the Dockerfile for a Windows container.
|
|
85
|
+
#
|
|
86
|
+
# Much shorter than the Linux equivalent: there is no SSH server and no
|
|
87
|
+
# key to inject, so only the base image, proxy settings, and any
|
|
88
|
+
# +provision_command+ entries are emitted.
|
|
89
|
+
#
|
|
90
|
+
# @return [String] the Dockerfile contents
|
|
91
|
+
# @raise [Kitchen::ActionFailed] if the platform is not +windows+
|
|
65
92
|
def dockerfile
|
|
66
93
|
raise ActionFailed, "Unknown platform '#{@config[:platform]}'" unless @config[:platform] == "windows"
|
|
67
94
|
return dockerfile_template if @config[:dockerfile]
|
|
@@ -18,16 +18,33 @@ 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
|
+
# @param state [Hash] mutable instance state; gains +username+
|
|
46
|
+
# @return [void]
|
|
47
|
+
# @raise [Kitchen::ActionFailed] if state names a container that is gone
|
|
31
48
|
def create(state)
|
|
32
49
|
if container_exists?(state)
|
|
33
50
|
info("Container ID #{state[:container_id]} already exists.")
|
|
@@ -39,6 +56,10 @@ module Kitchen
|
|
|
39
56
|
state[:username] = @config[:username]
|
|
40
57
|
end
|
|
41
58
|
|
|
59
|
+
# Removes the container, and its image when +remove_images+ is set.
|
|
60
|
+
#
|
|
61
|
+
# @param state [Hash] instance state naming the container
|
|
62
|
+
# @return [void]
|
|
42
63
|
def destroy(state)
|
|
43
64
|
info("[Docker] Destroying Docker container #{state[:container_id]}") if state[:container_id]
|
|
44
65
|
remove_container(state) if container_exists?(state)
|
|
@@ -48,6 +69,14 @@ module Kitchen
|
|
|
48
69
|
end
|
|
49
70
|
end
|
|
50
71
|
|
|
72
|
+
# Works out the address Test Kitchen should connect to.
|
|
73
|
+
#
|
|
74
|
+
# A remote Docker socket means the container is reachable at the socket's
|
|
75
|
+
# own host; +use_internal_docker_network+ means its container IP; anything
|
|
76
|
+
# else is a published port on localhost.
|
|
77
|
+
#
|
|
78
|
+
# @param state [Hash] instance state naming the container
|
|
79
|
+
# @return [String] a hostname or IP address
|
|
51
80
|
def hostname(state)
|
|
52
81
|
hostname = "localhost"
|
|
53
82
|
|
|
@@ -60,6 +89,11 @@ module Kitchen
|
|
|
60
89
|
hostname
|
|
61
90
|
end
|
|
62
91
|
|
|
92
|
+
# Copies local files into the container.
|
|
93
|
+
#
|
|
94
|
+
# @param locals [String, Array<String>] one path or several
|
|
95
|
+
# @param remote [String] destination path inside the container
|
|
96
|
+
# @return [Array<String>] the files copied
|
|
63
97
|
def upload(locals, remote)
|
|
64
98
|
files = locals
|
|
65
99
|
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.1".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
|
|
@@ -18,14 +18,22 @@ require "kitchen/shell_out"
|
|
|
18
18
|
|
|
19
19
|
module Kitchen
|
|
20
20
|
module Docker
|
|
21
|
+
# Mixins shared by the driver, transport, and container classes.
|
|
21
22
|
module Helpers
|
|
22
|
-
# rubocop:disable Metrics/ModuleLength
|
|
23
|
+
# rubocop:disable Metrics/ModuleLength
|
|
24
|
+
# Builds and runs docker CLI command lines.
|
|
23
25
|
module CliHelper
|
|
24
26
|
include Configurable
|
|
25
27
|
include Logging
|
|
26
28
|
include ShellOut
|
|
27
29
|
|
|
28
30
|
# rubocop:disable Metrics/AbcSize
|
|
31
|
+
|
|
32
|
+
# Runs a docker CLI command with the configured connection flags.
|
|
33
|
+
#
|
|
34
|
+
# @param cmd [String] the docker subcommand and its arguments
|
|
35
|
+
# @param options [Hash] shell-out options
|
|
36
|
+
# @return [String] the command's combined stdout and stderr
|
|
29
37
|
def docker_command(cmd, options = {})
|
|
30
38
|
docker = config[:binary].dup
|
|
31
39
|
docker << " -H #{config[:socket]}" if config[:socket]
|
|
@@ -39,8 +47,17 @@ module Kitchen
|
|
|
39
47
|
end
|
|
40
48
|
# rubocop:enable Metrics/AbcSize
|
|
41
49
|
|
|
42
|
-
# Copied from kitchen because we need stderr
|
|
43
50
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
51
|
+
|
|
52
|
+
# Runs a shell command, returning stderr as well as stdout.
|
|
53
|
+
#
|
|
54
|
+
# Test Kitchen's own +run_command+ discards stderr, but docker writes build
|
|
55
|
+
# progress and image ids there, so this reimplements it to keep both.
|
|
56
|
+
#
|
|
57
|
+
# @param cmd [String] the command to run
|
|
58
|
+
# @param options [Hash] shell-out options
|
|
59
|
+
# @return [String] combined stdout and stderr
|
|
60
|
+
# @raise [Kitchen::ShellCommandFailed] if the command exits non-zero
|
|
44
61
|
def run_command(cmd, options = {})
|
|
45
62
|
if options.fetch(:use_sudo, false)
|
|
46
63
|
cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
|
|
@@ -62,6 +79,12 @@ module Kitchen
|
|
|
62
79
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
63
80
|
|
|
64
81
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
82
|
+
|
|
83
|
+
# Builds the `docker run` command line from the configuration.
|
|
84
|
+
#
|
|
85
|
+
# @param image_id [String] the image to run
|
|
86
|
+
# @param transport_port [Integer, nil] container port to publish, if any
|
|
87
|
+
# @return [String] the docker subcommand and its arguments
|
|
65
88
|
def build_run_command(image_id, transport_port = nil)
|
|
66
89
|
cmd = "run -d"
|
|
67
90
|
cmd << " -i" if config[:interactive]
|
|
@@ -100,6 +123,12 @@ module Kitchen
|
|
|
100
123
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
101
124
|
|
|
102
125
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
126
|
+
|
|
127
|
+
# Builds a `docker exec` command line from the configuration.
|
|
128
|
+
#
|
|
129
|
+
# @param state [Hash] instance state naming the container
|
|
130
|
+
# @param command [String] the command to run inside it
|
|
131
|
+
# @return [String] the docker subcommand and its arguments
|
|
103
132
|
def build_exec_command(state, command)
|
|
104
133
|
cmd = "exec"
|
|
105
134
|
cmd << " -d" if config[:detach]
|
|
@@ -116,6 +145,12 @@ module Kitchen
|
|
|
116
145
|
end
|
|
117
146
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
118
147
|
|
|
148
|
+
# Builds a `docker cp` command line.
|
|
149
|
+
#
|
|
150
|
+
# @param local_file [String] source path
|
|
151
|
+
# @param remote_file [String] destination, as +container:path+
|
|
152
|
+
# @param opts [Hash] +:archive+ to preserve ownership and mode
|
|
153
|
+
# @return [String] the docker subcommand and its arguments
|
|
119
154
|
def build_copy_command(local_file, remote_file, opts = {})
|
|
120
155
|
cmd = "cp"
|
|
121
156
|
cmd << " -a" if opts[:archive]
|
|
@@ -123,6 +158,10 @@ module Kitchen
|
|
|
123
158
|
cmd
|
|
124
159
|
end
|
|
125
160
|
|
|
161
|
+
# Wraps PowerShell code so it can be run through `docker exec`.
|
|
162
|
+
#
|
|
163
|
+
# @param args [String] the PowerShell arguments
|
|
164
|
+
# @return [String] the full powershell invocation
|
|
126
165
|
def build_powershell_command(args)
|
|
127
166
|
cmd = "powershell -ExecutionPolicy Bypass -NoLogo "
|
|
128
167
|
cmd << args
|
|
@@ -130,6 +169,11 @@ module Kitchen
|
|
|
130
169
|
cmd
|
|
131
170
|
end
|
|
132
171
|
|
|
172
|
+
# Turns a hash of environment variables into `-e` flags.
|
|
173
|
+
#
|
|
174
|
+
# @param vars [Hash] variable names to values
|
|
175
|
+
# @return [String] the flags, each preceded by a space
|
|
176
|
+
# @raise [Kitchen::ActionFailed] if given something other than a Hash
|
|
133
177
|
def build_env_variable_args(vars)
|
|
134
178
|
raise ActionFailed, "Environment variables are not of a Hash type" unless vars.is_a?(Hash)
|
|
135
179
|
|
|
@@ -141,6 +185,8 @@ module Kitchen
|
|
|
141
185
|
args
|
|
142
186
|
end
|
|
143
187
|
|
|
188
|
+
# @return [String] the platform's null device, +NUL+ on Windows and
|
|
189
|
+
# +/dev/null+ everywhere else
|
|
144
190
|
def dev_null
|
|
145
191
|
case RbConfig::CONFIG["host_os"]
|
|
146
192
|
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
@@ -150,6 +196,13 @@ module Kitchen
|
|
|
150
196
|
end
|
|
151
197
|
end
|
|
152
198
|
|
|
199
|
+
# Normalizes shell-out options for a docker command.
|
|
200
|
+
#
|
|
201
|
+
# Translates +:suppress_output+ into silencing the live stream, and removes
|
|
202
|
+
# it, since Mixlib::ShellOut would reject the unknown key.
|
|
203
|
+
#
|
|
204
|
+
# @param options [Hash] the options to normalize
|
|
205
|
+
# @return [Hash] options Mixlib::ShellOut accepts
|
|
153
206
|
def docker_shell_opts(options = {})
|
|
154
207
|
options[:live_stream] = nil if options[:suppress_output]
|
|
155
208
|
options.delete(:suppress_output)
|
|
@@ -178,7 +231,7 @@ module Kitchen
|
|
|
178
231
|
end
|
|
179
232
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
180
233
|
end
|
|
181
|
-
# rubocop:enable Metrics/ModuleLength
|
|
234
|
+
# rubocop:enable Metrics/ModuleLength
|
|
182
235
|
end
|
|
183
236
|
end
|
|
184
237
|
end
|
|
@@ -24,12 +24,22 @@ 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; anything else
|
|
38
|
+
# means the output was not an id at all.
|
|
39
|
+
#
|
|
40
|
+
# @param output [String] the command output
|
|
41
|
+
# @return [String] the container id
|
|
42
|
+
# @raise [Kitchen::ActionFailed] if no id could be parsed
|
|
33
43
|
def parse_container_id(output)
|
|
34
44
|
container_id = output.chomp
|
|
35
45
|
|
|
@@ -40,28 +50,49 @@ module Kitchen
|
|
|
40
50
|
container_id
|
|
41
51
|
end
|
|
42
52
|
|
|
53
|
+
# Renders the configured Dockerfile through ERB.
|
|
54
|
+
#
|
|
55
|
+
# @return [String] the rendered Dockerfile
|
|
43
56
|
def dockerfile_template
|
|
44
57
|
template = IO.read(File.expand_path(config[:dockerfile]))
|
|
45
58
|
context = Kitchen::Docker::ERBContext.new(config.to_hash)
|
|
46
59
|
ERB.new(template).result(context.get_binding)
|
|
47
60
|
end
|
|
48
61
|
|
|
62
|
+
# @return [Boolean] whether the configured socket is a TCP one, meaning
|
|
63
|
+
# the daemon is not on this machine
|
|
49
64
|
def remote_socket?
|
|
50
65
|
config[:socket] ? socket_uri.scheme == "tcp" : false
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
# @return [URI] the configured Docker socket
|
|
53
69
|
def socket_uri
|
|
54
70
|
URI.parse(config[:socket])
|
|
55
71
|
end
|
|
56
72
|
|
|
73
|
+
# The path to pass to `docker build -f`.
|
|
74
|
+
#
|
|
75
|
+
# With a build context the path has to be relative to it; without one
|
|
76
|
+
# docker reads the Dockerfile from stdin and the absolute path is fine.
|
|
77
|
+
#
|
|
78
|
+
# @param file [File] the temp Dockerfile
|
|
79
|
+
# @return [String] the path to use
|
|
57
80
|
def dockerfile_path(file)
|
|
58
81
|
config[:build_context] ? Pathname.new(file.path).relative_path_from(Pathname.pwd).to_s : file.path
|
|
59
82
|
end
|
|
60
83
|
|
|
84
|
+
# @param state [Hash] instance state naming the container
|
|
85
|
+
# @return [Boolean] whether the container is present and running
|
|
61
86
|
def container_exists?(state)
|
|
62
87
|
state[:container_id] && !!docker_command("top #{state[:container_id]}") rescue false
|
|
63
88
|
end
|
|
64
89
|
|
|
90
|
+
# Runs a command inside the container.
|
|
91
|
+
#
|
|
92
|
+
# @param state [Hash] instance state naming the container
|
|
93
|
+
# @param command [String] the command to run
|
|
94
|
+
# @return [String] the command's combined output
|
|
95
|
+
# @raise [RuntimeError] if the command fails
|
|
65
96
|
def container_exec(state, command)
|
|
66
97
|
cmd = build_exec_command(state, command)
|
|
67
98
|
docker_command(cmd)
|
|
@@ -69,6 +100,13 @@ module Kitchen
|
|
|
69
100
|
raise "Failed to execute command on Docker container. #{e}"
|
|
70
101
|
end
|
|
71
102
|
|
|
103
|
+
# Creates a directory inside the container, on Linux or Windows.
|
|
104
|
+
#
|
|
105
|
+
# @param state [Hash] instance state naming the container
|
|
106
|
+
# @param path [String] the directory to create; environment variable
|
|
107
|
+
# references are expanded first
|
|
108
|
+
# @return [String] the command's combined output
|
|
109
|
+
# @raise [RuntimeError] if the directory cannot be created
|
|
72
110
|
def create_dir_on_container(state, path)
|
|
73
111
|
path = replace_env_variables(state, path)
|
|
74
112
|
cmd = "mkdir -p #{path}"
|
|
@@ -84,6 +122,13 @@ module Kitchen
|
|
|
84
122
|
raise "Failed to create directory #{path} on container. #{e}"
|
|
85
123
|
end
|
|
86
124
|
|
|
125
|
+
# Copies a local file into the container.
|
|
126
|
+
#
|
|
127
|
+
# @param state [Hash] instance state naming the container
|
|
128
|
+
# @param local_file [String] source path
|
|
129
|
+
# @param remote_file [String] destination path inside the container
|
|
130
|
+
# @return [String] the command's combined output
|
|
131
|
+
# @raise [RuntimeError] if the copy fails
|
|
87
132
|
def copy_file_to_container(state, local_file, remote_file)
|
|
88
133
|
debug("Copying local file #{local_file} to #{remote_file} on container")
|
|
89
134
|
|
|
@@ -97,6 +142,11 @@ module Kitchen
|
|
|
97
142
|
end
|
|
98
143
|
|
|
99
144
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
145
|
+
|
|
146
|
+
# Reads the container's environment.
|
|
147
|
+
#
|
|
148
|
+
# @param state [Hash] instance state naming the container
|
|
149
|
+
# @return [Hash] variable names to values
|
|
100
150
|
def container_env_variables(state)
|
|
101
151
|
# Retrieves all environment variables from inside container
|
|
102
152
|
vars = {}
|
|
@@ -116,6 +166,14 @@ module Kitchen
|
|
|
116
166
|
end
|
|
117
167
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
118
168
|
|
|
169
|
+
# Expands a container-side environment variable reference in a path.
|
|
170
|
+
#
|
|
171
|
+
# Handles both +$env:TEMP+ and +$TEMP+ forms. The value has to be read from
|
|
172
|
+
# inside the container, since the workstation's environment is unrelated.
|
|
173
|
+
#
|
|
174
|
+
# @param state [Hash] instance state naming the container
|
|
175
|
+
# @param str [String] the string to expand
|
|
176
|
+
# @return [String] the expanded string
|
|
119
177
|
def replace_env_variables(state, str)
|
|
120
178
|
if str.include?("$env:")
|
|
121
179
|
key = str[/\$env:(.*?)(\\|$)/, 1]
|
|
@@ -130,12 +188,20 @@ module Kitchen
|
|
|
130
188
|
str
|
|
131
189
|
end
|
|
132
190
|
|
|
191
|
+
# Runs the container and returns its id.
|
|
192
|
+
#
|
|
193
|
+
# @param state [Hash] instance state naming the image
|
|
194
|
+
# @param transport_port [Integer, nil] container port to publish, if any
|
|
195
|
+
# @return [String] the new container's id
|
|
133
196
|
def run_container(state, transport_port = nil)
|
|
134
197
|
cmd = build_run_command(state[:image_id], transport_port)
|
|
135
198
|
output = docker_command(cmd)
|
|
136
199
|
parse_container_id(output)
|
|
137
200
|
end
|
|
138
201
|
|
|
202
|
+
# @param state [Hash] instance state naming the container
|
|
203
|
+
# @return [String] the container's address on the Docker network
|
|
204
|
+
# @raise [Kitchen::ActionFailed] if it cannot be determined
|
|
139
205
|
def container_ip_address(state)
|
|
140
206
|
cmd = "inspect --format '{{ .NetworkSettings.IPAddress }}'"
|
|
141
207
|
cmd << " #{state[:container_id]}"
|
|
@@ -144,35 +210,41 @@ module Kitchen
|
|
|
144
210
|
raise ActionFailed, "Error getting internal IP of Docker container"
|
|
145
211
|
end
|
|
146
212
|
|
|
213
|
+
# Stops and removes the container.
|
|
214
|
+
#
|
|
215
|
+
# @param state [Hash] instance state naming the container
|
|
216
|
+
# @return [void]
|
|
147
217
|
def remove_container(state)
|
|
148
218
|
container_id = state[:container_id]
|
|
149
219
|
docker_command("stop -t 0 #{container_id}")
|
|
150
220
|
docker_command("rm #{container_id}")
|
|
151
221
|
end
|
|
152
222
|
|
|
153
|
-
#
|
|
223
|
+
# Dockerfile ENV lines carrying the configured proxy settings.
|
|
224
|
+
#
|
|
225
|
+
# Each is emitted in both lower and upper case, because different tools
|
|
226
|
+
# inside the image read different spellings.
|
|
227
|
+
#
|
|
228
|
+
# @return [String] the ENV lines, empty when no proxy is configured
|
|
154
229
|
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
|
|
230
|
+
%i{http_proxy https_proxy no_proxy}.map do |proxy_type|
|
|
231
|
+
proxy_env_vars(proxy_type)
|
|
232
|
+
end.join
|
|
233
|
+
end
|
|
165
234
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
235
|
+
# ENV lines for one proxy setting, in both spellings.
|
|
236
|
+
#
|
|
237
|
+
# @param proxy_type [Symbol] +:http_proxy+, +:https_proxy+, or
|
|
238
|
+
# +:no_proxy+
|
|
239
|
+
# @return [String] two ENV lines, or empty when that proxy is unset
|
|
240
|
+
def proxy_env_vars(proxy_type)
|
|
241
|
+
return "" unless config[proxy_type]
|
|
170
242
|
|
|
171
|
-
|
|
243
|
+
value = config[proxy_type]
|
|
244
|
+
"ENV #{proxy_type}=#{value}\nENV #{proxy_type.upcase}=#{value}\n"
|
|
172
245
|
end
|
|
173
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
174
246
|
end
|
|
175
|
-
# rubocop:enable Metrics/ModuleLength
|
|
247
|
+
# rubocop:enable Metrics/ModuleLength
|
|
176
248
|
end
|
|
177
249
|
end
|
|
178
250
|
end
|