class Kitchen::Driver::Dokken

Dokken driver for Kitchen.

@author Sean OMeara <sean@sean.io>

Public Instance Methods

create(state) click to toggle source

(see Base#create)

# File lib/kitchen/driver/dokken.rb, line 77
def create(state)
  # Authenticate the private registry
  authenticate!

  # image to config
  pull_platform_image

  # network
  make_dokken_network

  # chef
  pull_chef_image
  create_chef_container state

  # data
  dokken_create_sandbox

  if remote_docker_host? || running_inside_docker?
    make_data_image
    start_data_container state
  end

  # work image
  build_work_image state

  # runner
  start_runner_container state

  # misc
  save_misc_state state
end
destroy(_state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 109
def destroy(_state)
  if remote_docker_host? || running_inside_docker?
    stop_data_container
    delete_data_container
  end

  stop_runner_container
  delete_runner_container
  delete_work_image
  dokken_delete_sandbox
end

Private Instance Methods

api_retries() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 129
def api_retries
  config[:api_retries]
end
authenticate!() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 441
def authenticate!
  # No need to authenticate if the credentials are empty
  return if docker_creds.empty?

  ::Docker.authenticate! docker_creds
end
build_work_image(state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 152
def build_work_image(state)
  info("Building work image..")
  return if ::Docker::Image.exist?(work_image, { "platform" => config[:platform] }, docker_connection)

  begin
    @intermediate_image = ::Docker::Image.build(
      work_image_dockerfile,
      {
        "t" => work_image,
        "platform" => config[:platform],
      },
      docker_connection
    )
  # credit to https://github.com/someara/kitchen-dokken/issues/95#issue-224697526
  rescue Docker::Error::UnexpectedResponseError => e
    msg = "work_image build failed: "
    msg += JSON.parse(e.to_s.split("\r\n").last)["error"].to_s
    msg += ". The common scenarios are incorrect intermediate "
    msg += "instructions such as not including `-y` on an `apt-get` "
    msg += "or similar. The other common scenario is a transient "
    msg += "error such as an unresponsive mirror."
    raise msg
  # fallback rescue above should catch most of the errors
  rescue => e
    raise "work_image build failed: #{e}"
  end

  state[:work_image] = work_image
end
calc_volumes_binds() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 282
def calc_volumes_binds
  volumes = Array.new(Array(config[:volumes]))
  binds = Array.new(Array(config[:binds]))

  # Binds is mutated in-place, volumes *may* be.
  volumes = coerce_volumes(volumes, binds)

  binds_ret = []
  binds_ret << "#{dokken_kitchen_sandbox}:/opt/kitchen" unless dokken_kitchen_sandbox.nil? || remote_docker_host? || running_inside_docker?
  binds_ret << "#{dokken_verifier_sandbox}:/opt/verifier" unless dokken_verifier_sandbox.nil? || remote_docker_host? || running_inside_docker?
  binds_ret << binds unless binds.nil?

  [volumes, binds_ret.flatten]
end
chef_container_name() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 645
def chef_container_name
  config[:platform] != "" ? "chef-#{chef_version}-" + config[:platform].sub("/", "-") : "chef-#{chef_version}"
end
chef_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 649
def chef_image
  "#{config[:chef_image]}:#{chef_version}"
end
chef_version() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 653
def chef_version
  return "latest" if config[:chef_version] == "stable"

  config[:chef_version]
end
coerce_tmpfs(v) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 243
def coerce_tmpfs(v)
  case v
  when Hash, nil
    v
  else
    Array(v).each_with_object({}) do |y, h|
      name, opts = y.split(":", 2)
      h[name.to_s] = opts.to_s
    end
  end
end
coerce_volumes(v, binds) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 262
def coerce_volumes(v, binds)
  case v
  when PartialHash, nil
    v
  when Hash
    PartialHash[v]
  else
    b = []
    v.delete_if do |x|
      parts = x.split(":")
      b << x if parts.length > 1
    end
    b = nil if b.empty?
    binds.push(b) unless binds.include?(b) || b.nil?
    return PartialHash.new if v.empty?

    v.each_with_object(PartialHash.new) { |volume, h| h[volume] = {} }
  end
end
container_exist?(name) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 520
def container_exist?(name)
  return true if ::Docker::Container.get(name, {}, docker_connection)
rescue StandardError, ::Docker::Error::NotFoundError
  false
end
container_state() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 611
def container_state
  @container ? @container.info["State"] : {}
end
create_chef_container(state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 406
def create_chef_container(state)
  lockfile = Lockfile.new "#{home_dir}/.dokken-#{chef_container_name}.lock"
  begin
    lockfile.lock
    with_retries do
      # TEMPORARY FIX - docker-api 2.0.0 has a buggy Docker::Container.get - use .all instead
      # https://github.com/swipely/docker-api/issues/566
      # ::Docker::Container.get(chef_container_name, {}, docker_connection)
      found = ::Docker::Container.all({ all: true }, docker_connection).select { |c| c.info["Names"].include?("/#{chef_container_name}") }
      raise ::Docker::Error::NotFoundError.new(chef_container_name) if found.empty?

      debug "Chef container already exists, continuing"
    end
  rescue ::Docker::Error::NotFoundError
    debug "Chef container does not exist, creating a new Chef container"
    with_retries do
      debug "driver - creating volume container #{chef_container_name} from #{chef_image}"
      config = {
        "name" => chef_container_name,
        "Cmd" => "true",
        "Image" => registry_image_path(chef_image),
        "HostConfig" => {
          "NetworkMode" => self[:network_mode],
        },
      }
      chef_container = create_container(config)
      state[:chef_container] = chef_container.json
    rescue ::Docker::Error, StandardError => e
      raise "driver - #{chef_container_name} failed to create #{e}"
    end
  ensure
    lockfile.unlock
  end
end
create_container(args) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 579
def create_container(args)
  with_retries { @container = ::Docker::Container.get(args["name"], {}, docker_connection) }
rescue
  with_retries do
    args["Env"] = [] if args["Env"].nil?
    args["Env"] << "TEST_KITCHEN=1"
    args["Env"] << "CI=#{ENV["CI"]}" if ENV.include? "CI"
    args["Platform"] = config[:platform]
    info "Creating container #{args["name"]}"
    debug "driver - create_container args #{args}"
    with_retries do
      @container = ::Docker::Container.create(args.clone, docker_connection)
    rescue ::Docker::Error::ConflictError
      debug "driver - rescue ConflictError: #{args["name"]}"
      with_retries { @container = ::Docker::Container.get(args["name"], {}, docker_connection) }
    end
  rescue ::Docker::Error => e
    debug "driver - error :#{e}:"
    raise "driver - failed to create_container #{args["name"]}"
  end
end
data_container_name() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 659
def data_container_name
  "#{instance_name}-data"
end
data_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 663
def data_image
  config[:data_image]
end
delete_chef_container() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 202
def delete_chef_container
  debug "driver - deleting container #{chef_container_name}"
  delete_container chef_container_name
end
delete_container(name) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 625
def delete_container(name)
  with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
  with_retries { @container.delete(force: true, v: true) }
rescue ::Docker::Error::NotFoundError
  debug "Container #{name} not found. Nothing to delete."
end
delete_data_container() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 207
def delete_data_container
  debug "driver - deleting container #{data_container_name}"
  delete_container data_container_name
end
delete_image(name) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 513
def delete_image(name)
  with_retries { @image = ::Docker::Image.get(name, { "platform" => config[:platform] }, docker_connection) }
  with_retries { @image.remove(force: true) }
rescue ::Docker::Error
  puts "Image #{name} not found. Nothing to delete."
end
delete_runner_container() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 212
def delete_runner_container
  debug "driver - deleting container #{runner_container_name}"
  delete_container runner_container_name
end
delete_work_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 140
def delete_work_image
  return unless ::Docker::Image.exist?(work_image, { "platform" => config[:platform] }, docker_connection)

  with_retries { @work_image = ::Docker::Image.get(work_image, { "platform" => config[:platform] }, docker_connection) }

  with_retries do
    with_retries { @work_image.remove(force: true) }
  rescue ::Docker::Error::ConflictError
    debug "driver - #{work_image} cannot be removed"
  end
end
docker_config_creds() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 456
def docker_config_creds
  return @docker_config_creds if @docker_config_creds

  @docker_config_creds = {}
  config_file = ::File.join(::Dir.home, ".docker", "config.json")
  if ::File.exist?(config_file)
    config = JSON.load_file!(config_file)
    if config["auths"]
      config["auths"].each do |k, v|
        next if v["auth"].nil?

        username, password = Base64.decode64(v["auth"]).split(":")
        @docker_config_creds[k] = { serveraddress: k, username: username, password: password }
      end
    end

    if config["credHelpers"]
      config["credHelpers"].each do |k, v|
        @docker_config_creds[k] = Proc.new do
          c = JSON.parse(`echo #{k} | docker-credential-#{v} get`)
          { serveraddress: c["ServerURL"], username: c["Username"], password: c["Secret"] }
        end
      end
    end
  else
    debug("~/.docker/config.json does not exist")
  end

  @docker_config_creds
end
docker_connection() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 133
def docker_connection
  opts = ::Docker.options
  opts[:read_timeout] = config[:read_timeout]
  opts[:write_timeout] = config[:write_timeout]
  @docker_connection ||= ::Docker::Connection.new(config[:docker_host_url], opts)
end
docker_creds() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 448
def docker_creds
  @docker_creds ||= if config[:creds_file]
                      JSON.parse(IO.read(config[:creds_file]))
                    else
                      {}
                    end
end
docker_creds_for_image(image) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 487
def docker_creds_for_image(image)
  return docker_creds if config[:creds_file]

  image_registry = image.split("/").first

  # NOTE: Try to use DockerHub auth if exact registry match isn't found
  default_registry = "https://index.docker.io/v1/"
  if docker_config_creds.key?(image_registry)
    c = docker_config_creds[image_registry]
    c.respond_to?(:call) ? c.call : c
  elsif docker_config_creds.key?(default_registry)
    c = docker_config_creds[default_registry]
    c.respond_to?(:call) ? c.call : c
  end
end
dokken_tmpfs() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 239
def dokken_tmpfs
  coerce_tmpfs(config[:tmpfs])
end
dokken_volumes_from() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 255
def dokken_volumes_from
  ret = []
  ret << chef_container_name
  ret << data_container_name if remote_docker_host? || running_inside_docker?
  ret
end
image_prefix() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 217
def image_prefix
  config[:image_prefix]
end
instance_platform_name() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 221
def instance_platform_name
  instance.platform.name
end
make_data_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 401
def make_data_image
  debug "driver - calling create_data_image"
  create_data_image(config[:docker_registry])
end
make_dokken_network() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 383
def make_dokken_network
  return unless self[:network_mode] == "dokken"

  lockfile = Lockfile.new "#{home_dir}/.dokken-network.lock"
  begin
    lockfile.lock
    with_retries { ::Docker::Network.get("dokken", {}, docker_connection) }
  rescue ::Docker::Error::NotFoundError
    begin
      with_retries { ::Docker::Network.create("dokken", network_settings) }
    rescue ::Docker::Error => e
      debug "driver - error :#{e}:"
    end
  ensure
    lockfile.unlock
  end
end
parse_image_name(image) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 526
def parse_image_name(image)
  parts = image.split(":")

  if parts.size > 2
    tag = parts.pop
    repo = parts.join(":")
  else
    tag = parts[1] || "latest"
    repo = parts[0]
  end

  [repo, tag]
end
parse_registry_host(val) click to toggle source

github.com/docker/docker/blob/4fcb9ac40ce33c4d6e08d5669af6be5e076e2574/registry/auth.go#L231

# File lib/kitchen/driver/dokken.rb, line 683
def parse_registry_host(val)
  val.sub(%r{https?://}, "").split("/").first
end
platform_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 667
def platform_image
  config[:image] || platform_image_from_name
end
platform_image_from_name() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 671
def platform_image_from_name
  platform, release = instance.platform.name.split("-")
  release ? [platform, release].join(":") : platform
end
pull_chef_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 508
def pull_chef_image
  debug "driver - pulling #{short_image_path(chef_image)}"
  config[:pull_chef_image] ? pull_image(chef_image) : pull_if_missing(chef_image)
end
pull_if_missing(image) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 676
def pull_if_missing(image)
  return if ::Docker::Image.exist?(registry_image_path(image), { "platform" => config[:platform] }, docker_connection)

  pull_image image
end
pull_image(image) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 687
def pull_image(image)
  path = registry_image_path(image)
  with_retries do
    if Docker::Image.exist?(path, { "platform" => config[:platform] }, docker_connection)
      original_image = Docker::Image.get(path, { "platform" => config[:platform] }, docker_connection)
    end

    new_image = Docker::Image.create({ "fromImage" => path, "platform" => config[:platform] }, docker_creds_for_image(image), docker_connection)

    !(original_image&.id&.start_with?(new_image.id))
  end
end
pull_platform_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 503
def pull_platform_image
  debug "driver - pulling #{short_image_path(platform_image)}"
  config[:pull_platform_image] ? pull_image(platform_image) : pull_if_missing(platform_image)
end
registry_image_path(image) click to toggle source

Qualifies the results of ‘short_image_path` with any registry the user has requested

@param image [String] the docker image path to parse @return [String] The most fully-qualified registry path we cn make

# File lib/kitchen/driver/dokken.rb, line 571
def registry_image_path(image)
  if config[:docker_registry]
    "#{config[:docker_registry]}/#{short_image_path(image)}"
  else
    short_image_path(image)
  end
end
repo(image) click to toggle source

Return the ‘repo’ half of a docker image path. Agnostic about if a registry is included, this effectively is just “before the colon”

@param image [String] the docker image path to parse @return [String] the repo portion of ‘image`

# File lib/kitchen/driver/dokken.rb, line 545
def repo(image)
  parse_image_name(image)[0]
end
run_container(args) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 601
def run_container(args)
  create_container(args)
  with_retries do
    @container.start
    @container = ::Docker::Container.get(args["name"], {}, docker_connection)
    wait_running_state(args["name"], true)
  end
  @container
end
runner_container_name() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 700
def runner_container_name
  instance_name.to_s
end
save_misc_state(state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 195
def save_misc_state(state)
  state[:platform_image] = platform_image
  state[:instance_name] = instance_name
  state[:instance_platform_name] = instance_platform_name
  state[:image_prefix] = image_prefix
end
short_image_path(image) click to toggle source

Ensures an explicit tag on an image path.

@param image [String] the docker image path to parse @return [String] ‘repo`:`tag`

# File lib/kitchen/driver/dokken.rb, line 562
def short_image_path(image)
  "#{repo(image)}:#{tag(image)}"
end
start_data_container(state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 358
def start_data_container(state)
  debug "driver - creating #{data_container_name}"
  config = {
    "name" => data_container_name,
    # locally built image, must use short-name
    "Image" => short_image_path(data_image),
    "HostConfig" => {
      "PortBindings" => port_bindings,
      "PublishAllPorts" => true,
      "NetworkMode" => "bridge",
    },
  }
  unless %w{host bridge}.include?(self[:network_mode])
    config["NetworkingConfig"] = {
      "EndpointsConfig" => {
        self[:network_mode] => {
          "Aliases" => Array(self[:hostname]),
        },
      },
    }
  end
  data_container = run_container(config)
  state[:data_container] = data_container.json
end
start_runner_container(state) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 297
def start_runner_container(state)
  debug "driver - starting #{runner_container_name}"

  volumes, binds = calc_volumes_binds

  config = {
    "name" => runner_container_name,
    "Cmd" => Shellwords.shellwords(self[:pid_one_command]),
    # locally built image, must use short-name
    "Image" => short_image_path(work_image),
    "Hostname" => self[:hostname],
    "Env" => self[:env],
    "ExposedPorts" => exposed_ports,
    "Volumes" => volumes,
    "HostConfig" => {
      "Privileged" => self[:privileged],
      "VolumesFrom" => dokken_volumes_from,
      "Binds" => binds,
      "Dns" => self[:dns],
      "DnsSearch" => self[:dns_search],
      "Links" => Array(self[:links]),
      "CapAdd" => Array(self[:cap_add]),
      "CapDrop" => Array(self[:cap_drop]),
      "SecurityOpt" => Array(self[:security_opt]),
      "NetworkMode" => self[:network_mode],
      "PortBindings" => port_bindings,
      "Tmpfs" => dokken_tmpfs,
      "Memory" => self[:memory_limit],
    },
  }
  unless %w{host bridge}.include?(self[:network_mode])
    config["NetworkingConfig"] = {
      "EndpointsConfig" => {
        self[:network_mode] => {
          "Aliases" => Array(self[:hostname]).concat(Array(self[:hostname_aliases])),
        },
      },
    }
  end
  unless self[:entrypoint].to_s.empty?
    config["Entrypoint"] = self[:entrypoint]
  end
  if self[:cgroupns_host]
    config["HostConfig"]["CgroupnsMode"] = "host"
  end
  if self[:userns_host]
    config["HostConfig"]["UsernsMode"] = "host"
  end

  if self[:privileged]
    if self[:user_ns_mode] != "host"
      debug "driver - privileged mode is not supported with user namespaces enabled"
      debug "driver - changing UsernsMode from '#{self[:user_ns_mode]}' to 'host'"
    end
    config["HostConfig"]["UsernsMode"] = "host"
  end

  runner_container = run_container(config)
  state[:runner_container] = runner_container.json
end
stop_container(name) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 615
def stop_container(name)
  with_retries { @container = ::Docker::Container.get(name, {}, docker_connection) }
  with_retries do
    @container.stop(force: false)
    wait_running_state(name, false)
  end
rescue ::Docker::Error::NotFoundError
  debug "Container #{name} not found. Nothing to stop."
end
stop_data_container() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 230
def stop_data_container
  debug "driver - stopping container #{data_container_name}"
  stop_container data_container_name
end
stop_runner_container() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 225
def stop_runner_container
  debug "driver - stopping container #{runner_container_name}"
  stop_container runner_container_name
end
tag(image) click to toggle source

Return the ‘tag’ of a docker image path. Will be ‘latest` if there is no explicit tag in the image path.

@param image [String] the docker image path to parse @return [String] the tag of ‘image`

# File lib/kitchen/driver/dokken.rb, line 554
def tag(image)
  parse_image_name(image)[1]
end
wait_running_state(name, v) click to toggle source
# File lib/kitchen/driver/dokken.rb, line 632
def wait_running_state(name, v)
  @container = ::Docker::Container.get(name, {}, docker_connection)
  i = 0
  tries = 20
  until container_state["Running"] == v || container_state["FinishedAt"] != "0001-01-01T00:00:00Z"
    i += 1
    break if i == tries

    sleep 0.1
    @container = ::Docker::Container.get(name, {}, docker_connection)
  end
end
with_retries() { || ... } click to toggle source
# File lib/kitchen/driver/dokken.rb, line 704
def with_retries
  tries = api_retries
  begin
    yield
  # Only catch errors that can be fixed with retries.
  rescue ::Docker::Error::ServerError, # 404
         ::Docker::Error::UnexpectedResponseError, # 400
         ::Docker::Error::TimeoutError,
         ::Docker::Error::IOError => e
    tries -= 1
    sleep 0.1
    retry if tries > 0
    debug "tries: #{tries} error: #{e}"
    raise e
  end
end
work_image() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 235
def work_image
  [image_prefix, instance_name].compact.join("/").downcase
end
work_image_dockerfile() click to toggle source
# File lib/kitchen/driver/dokken.rb, line 182
def work_image_dockerfile
  from = registry_image_path(platform_image)
  debug("driver - Building work image from #{from}")
  dockerfile_contents = [
    "FROM #{from}",
    "LABEL X-Built-By=kitchen-dokken X-Built-From=#{platform_image}",
  ]
  Array(config[:intermediate_instructions]).each do |c|
    dockerfile_contents << c
  end
  dockerfile_contents.join("\n")
end