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 collapse

QUIET_BUILD_IMAGE_ID =

An id on a line of its own, which is all docker build -q prints.

/\A(sha256:[[:xdigit:]]{64})\z/

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



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 115

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



154
155
156
157
158
159
160
161
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 154

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



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 92

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



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 41

def parse_image_id(output)
  output.split("\n").reverse_each do |line|
    line = line.strip
    # `docker build -q` prints the id and nothing else -- none of the
    # wording below appears -- so a build with `build_options: -q` had
    # no line any of these matched and failed with "Could not parse
    # Docker build output for image ID" (#225).
    return Regexp.last_match(1) if line.match(QUIET_BUILD_IMAGE_ID)

    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



71
72
73
74
75
76
77
78
79
# File 'lib/kitchen/docker/helpers/image_helper.rb', line 71

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