Class: Kitchen::Driver::Docker

Inherits:
Base
  • Object
show all
Includes:
Kitchen::Docker::Helpers::CliHelper, Kitchen::Docker::Helpers::ContainerHelper, ShellOut
Defined in:
lib/kitchen/driver/docker.rb

Overview

Docker driver for Kitchen.

Author:

Constant Summary

Constants included from Kitchen::Docker::Helpers::ContainerHelper

Kitchen::Docker::Helpers::ContainerHelper::COPIED_MARKER

Instance Method Summary collapse

Methods included from Kitchen::Docker::Helpers::ContainerHelper

#container_env_variables, #container_exec, #container_exists?, #container_ip_address, #container_running?, #copy_file_to_container, #create_dir_on_container, #dockerfile_path, #dockerfile_proxy_config, #dockerfile_template, #file_on_container?, #ip_address?, #parse_container_id, #proxy_env_vars, #remote_socket?, #remove_container, #replace_env_variables, #run_container, #socket_uri, #verify_file_copied

Methods included from Kitchen::Docker::Helpers::CliHelper

#build_copy_command, #build_env_variable_args, #build_exec_command, #build_powershell_command, #build_run_command, #config_to_options, #dev_null, #docker_command, #docker_shell_opts, #docker_sudo_opts, #run_command, #shell_escape

Instance Method Details

#containerKitchen::Docker::Container (protected)

The container implementation for this platform.



308
309
310
311
312
313
314
315
# File 'lib/kitchen/driver/docker.rb', line 308

def container
  @container ||= if windows_os?
                   Kitchen::Docker::Container::Windows.new(config)
                 else
                   Kitchen::Docker::Container::Linux.new(config)
                 end
  @container
end

#create(state) ⇒ void

This method returns an undefined value.

Builds the image and starts the container.



146
147
148
149
150
# File 'lib/kitchen/driver/docker.rb', line 146

def create(state)
  container.create(state)

  wait_for_transport(state)
end

#default_imageString

The Docker image implied by the platform name.

ubuntu-22.04 becomes ubuntu:22.04. CentOS is special-cased, since its images are tagged centos7 rather than centos:7.



252
253
254
255
256
257
258
# File 'lib/kitchen/driver/docker.rb', line 252

def default_image
  platform, release = instance.platform.name.split("-")
  if platform == "centos" && release
    release = "centos" + release.split(".").first
  end
  release ? [platform, release].join(":") : platform
end

#default_platformString



261
262
263
# File 'lib/kitchen/driver/docker.rb', line 261

def default_platform
  instance.platform.name.split("-").first
end

#destroy(state) ⇒ void

This method returns an undefined value.

Removes the container, and its image when remove_images is set.



156
157
158
# File 'lib/kitchen/driver/docker.rb', line 156

def destroy(state)
  container.destroy(state)
end

#doctor(state) ⇒ Boolean

Checks the configuration and the daemon it points at.

Run by kitchen doctor. A true return is how Test Kitchen decides to exit non-zero, so every check runs and the results are OR-ed together rather than returning at the first problem -- somebody running doctor wants the whole list, not the first item on it.



202
203
204
205
206
207
208
# File 'lib/kitchen/driver/docker.rb', line 202

def doctor(state)
  [
    doctor_daemon,
    doctor_files,
    doctor_container(state),
  ].any?
end

#doctor_container(state) ⇒ Boolean (protected)



296
297
298
299
300
301
302
303
# File 'lib/kitchen/driver/docker.rb', line 296

def doctor_container(state)
  return false unless state[:container_id]
  return false if container_exists?(state)

  error("The state file names container #{state[:container_id]}, which the daemon does " \
        "not have. Run `kitchen destroy` to clear it.")
  true
end

#doctor_daemonBoolean (protected)



268
269
270
271
272
273
274
275
# File 'lib/kitchen/driver/docker.rb', line 268

def doctor_daemon
  version = docker_command("version --format '{{.Server.Version}}'", suppress_output: true).strip
  info("Docker daemon at #{config[:socket]} is reachable, running #{version}.")
  false
rescue => e
  error("Cannot reach the Docker daemon at #{config[:socket]}. #{e}")
  true
end

#doctor_filesBoolean (protected)

Checks paths the configuration names.

A missing TLS file or Dockerfile is worth catching here because docker reports it far from the cause -- a missing client certificate surfaces as a connection error rather than as a missing file.



284
285
286
287
288
289
290
291
292
# File 'lib/kitchen/driver/docker.rb', line 284

def doctor_files
  i{tls_cacert tls_cert tls_key dockerfile}.map do |key|
    path = config[key]
    next false if path.nil? || ::File.exist?(::File.expand_path(path))

    error("#{key} is set to #{path}, which does not exist.")
    true
  end.any?
end

#package(state) ⇒ void

This method returns an undefined value.

Commits the container to a Docker image.

kitchen package asks a driver to turn a converged instance into something reusable. For Docker that is an image: docker commit on the running container, which is the artifact every other docker tool already takes. Run docker save against the result for a tarball.

Raises:

  • (Kitchen::ActionFailed)

    if the instance has not been created



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/kitchen/driver/docker.rb', line 170

def package(state)
  unless state[:container_id]
    raise ActionFailed, "Cannot package #{instance.name}: it has not been created."
  end

  # Asked here rather than left to `docker commit`, which reports a
  # container that is gone as a bare "Error response from daemon: No such
  # container: <64 hex characters>" with nothing about the instance or
  # what to do next.
  unless container_exists?(state)
    raise ActionFailed, "Cannot package #{instance.name}: the state file names container " \
                        "#{state[:container_id]}, which the daemon does not have. " \
                        "Run `kitchen destroy` to clear it."
  end

  name = config[:package_name]
  info("[Docker] Committing container #{state[:container_id]} to #{name}")
  output = docker_command("commit #{shell_escape(state[:container_id])} #{shell_escape(name)}",
    suppress_output: !logger.debug?)
  image_id = output.lines.map(&:strip).find { |line| line.match?(/\Asha256:[[:xdigit:]]{64}\z/) }
  info("[Docker] Packaged #{instance.name} as #{name}#{" (#{image_id})" if image_id}")
end

#status(state) ⇒ Hash

Reports whether the container backing this instance is up.

Read by kitchen list --live, which showed "unknown" for every instance: Base cannot know, and this driver never said. Docker can answer directly, and these are the same two questions create and destroy already ask.



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/kitchen/driver/docker.rb', line 219

def status(state)
  common = { source: "driver", checked_at: Time.now.utc.iso8601, resource_id: state[:container_id] }

  if !state[:container_id]
    common.merge(live: false, state: "not created",
      message: "No container is recorded in the state file")
  elsif !container_exists?(state)
    common.merge(live: false, state: "gone",
      message: "The state file names a container the daemon does not have")
  elsif container_running?(state)
    common.merge(live: true, state: "running")
  else
    common.merge(live: false, state: "stopped",
      message: "The container exists but is not running")
  end
end

#verify_dependenciesvoid

This method returns an undefined value.

Checks that the Docker CLI is installed and runnable.

Raises:

  • (Kitchen::UserError)

    if the binary cannot be run



136
137
138
139
140
# File 'lib/kitchen/driver/docker.rb', line 136

def verify_dependencies
  run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
rescue
  raise UserError, "You must first install the Docker CLI tool https://www.docker.com/get-started"
end

#wait_for_transport(state) ⇒ void

This method returns an undefined value.

Waits for the transport to accept a connection, unless disabled.



240
241
242
243
244
# File 'lib/kitchen/driver/docker.rb', line 240

def wait_for_transport(state)
  if config[:wait_for_transport]
    instance.transport.connection(state, &:wait_until_ready)
  end
end