Module: Kitchen::Docker::Helpers::ImageHelper

Includes:
Configurable, CliHelper, ContainerHelper
Included in:
Container
Defined in:
lib/kitchen/docker/helpers/image_helper.rb

Overview

Building, inspecting, and removing Docker images.

Constant Summary

Constants included from ContainerHelper

ContainerHelper::COPIED_MARKER

Instance Method Summary collapse

Methods included from 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 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

#build_image(state, dockerfile) ⇒ String

Builds the image from the given Dockerfile.

The Dockerfile is written to a temp file and also passed on stdin, so the build works both with a build context and without one. The temp file is removed whether or not the build succeeded.

Parameters:

  • state (Hash)

    instance state

  • dockerfile (String)

    the Dockerfile contents

Returns:

  • (String)

    the new image's id

Raises:

  • (Kitchen::ActionFailed)

    if the id cannot be parsed from the output



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 105

def build_image(state, dockerfile)
  cmd = "build"
  cmd << " --no-cache" unless config[:use_cache]
  cmd << " --platform=#{config[:docker_platform]}" if config[:docker_platform]
  extra_build_options = config_to_options(config[:build_options])
  cmd << " #{extra_build_options}" unless extra_build_options.empty?
  dockerfile_contents = dockerfile
  file = Tempfile.new("Dockerfile-kitchen", Pathname.pwd + config[:build_tempdir])
  cmd << " -f #{Shellwords.escape(dockerfile_path(file))}" if config[:build_context]
  build_context = config[:build_context] ? "." : "-"
  output = begin
             file.write(dockerfile)
             file.close
             docker_command("#{cmd} #{build_context}",
               input: dockerfile_contents,
               environment: { BUILDKIT_PROGRESS: "plain" })
           ensure
             file.close unless file.closed?
             file.unlink
           end

  parse_image_id(output)
end

#image_exists?(state) ⇒ Boolean

Whether the image named in state is present locally.

The inspect is silenced, as every other predicate that shells out to docker is. Left speaking, kitchen destroy on an instance with remove_images set printed the image's entire docker inspect JSON -- config, every layer digest, metadata -- into the middle of the destroy output, between the container being removed and the image being removed. Nothing read it: only whether the command succeeded is used.

It is still printed under -l debug, where the rest of the driver's docker traffic is.

Parameters:

  • state (Hash)

    instance state naming the image

Returns:

  • (Boolean)

    whether the image is present locally



144
145
146
147
148
149
150
151
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 144

def image_exists?(state)
  return false unless state[:image_id]

  !!docker_command("inspect --type=image #{state[:image_id]}",
    suppress_output: !logger.debug?)
rescue
  false
end

#image_in_use?(state) ⇒ Boolean

Whether any container was created from the image.

Asked with a filter rather than by searching docker ps -a output for the id. That output abbreviates the IMAGE column to twelve characters, while state carries the full sha256: digest, so the substring never matched and the answer was always false -- which defeated the guard entirely and let #remove_image run docker rmi against an image a container was still using.

Parameters:

  • state (Hash)

    instance state naming the image

Returns:

  • (Boolean)

    whether any container references it



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 82

def image_in_use?(state)
  return false unless state[:image_id]

  output = docker_command("ps -a -q --filter ancestor=#{state[:image_id]}",
    suppress_output: !logger.debug?)

  # Matched line by line rather than by emptiness, so a warning docker
  # writes to stderr is not mistaken for a container id.
  output.lines.map(&:strip).any? do |line|
    line.match?(/\A[0-9a-f]{12}(?:[0-9a-f]{52})?\z/)
  end
end

#parse_image_id(output) ⇒ String

Pulls the built image's id out of docker build output.

Scanned in reverse, and against several patterns, because the wording has changed across Docker and BuildKit versions.

Parameters:

  • output (String)

    the build output

Returns:

  • (String)

    the image id

Raises:

  • (Kitchen::ActionFailed)

    if no id could be found



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 38

def parse_image_id(output)
  output.split("\n").reverse_each do |line|
    if line =~ /writing image (sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i
      img_id = line[/writing image (sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i, 1]
      return img_id
    end
    if line =~ /image id|build successful|successfully built/i
      img_id = line.split(/\s+/).last
      return img_id
    end
    # Docker ~v4.31 support
    if line =~ /naming to moby-dangling@(sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i
      img_id = line[/naming to moby-dangling@(sha256:[[:xdigit:]]{64})(?: \d*\.\ds)? done/i, 1]
      return img_id
    end
  end
  raise ActionFailed, "Could not parse Docker build output for image ID"
end

#remove_image(state) ⇒ void

This method returns an undefined value.

Removes the built image, unless a container is still using it.

Parameters:

  • state (Hash)

    instance state naming the image



61
62
63
64
65
66
67
68
69
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 61

def remove_image(state)
  image_id = state[:image_id]
  if image_in_use?(state)
    info("[Docker] Image ID #{image_id} is in use. Skipping removal")
  else
    info("[Docker] Removing image with Image ID #{image_id}.")
    docker_command("rmi #{image_id}")
  end
end