class Kitchen::Driver::Dokken

@author Sean OMeara <sean@chef.io>
Dokken driver for Kitchen.

def api_retries

def api_retries
  config[:api_retries]
end

def build_work_image(state)

def build_work_image(state)
  info('Building work image..')
  return if ::Docker::Image.exist?(work_image, {}, docker_connection)
  begin
    @intermediate_image = ::Docker::Image.build(
      work_image_dockerfile,
      {
        't' => work_image,
      },
      docker_connection
    )
  rescue
    raise 'work_image build failed'
  end
  state[:work_image] = work_image
end

def chef_container_name

def chef_container_name
  "chef-#{chef_version}"
end

def chef_image

def chef_image
  "#{config[:chef_image]}:#{chef_version}"
end

def chef_version

def chef_version
  return 'current' if config[:chef_version] == 'latest'
  config[:chef_version]
end

def coerce_volumes(v)

def coerce_volumes(v)
  case v
  when PartialHash, nil
    v
  when Hash
    PartialHash[v]
  else
    b = []
    v = Array(v).to_a # in case v.is_A?(Chef::Node::ImmutableArray)
    v.delete_if do |x|
      parts = x.split(':')
      b << x if parts.length > 1
    end
    b = nil if b.empty?
    config[:binds] << b unless b.nil?
    return PartialHash.new if v.empty?
    v.each_with_object(PartialHash.new) { |volume, h| h[volume] = {} }
  end
end

def container_exist?(name)

def container_exist?(name)
  return true if ::Docker::Container.get(name, {}, docker_connection)
rescue
  false
end

def container_state

def container_state
  @container ? @container.info['State'] : {}
end

def create(state)

(see Base#create)
def create(state)
  # image to config
  pull_platform_image
  # chef
  pull_chef_image
  create_chef_container state
  # data
  dokken_create_sandbox
  if remote_docker_host?
    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

def create_chef_container(state)

def create_chef_container(state)
  ::Docker::Container.get(chef_container_name, {}, docker_connection)
rescue ::Docker::Error::NotFoundError
  begin
    debug "driver - creating volume container #{chef_container_name} from #{chef_image}"
    chef_container = create_container(
      'name' => chef_container_name,
      'Cmd' => 'true',
      'Image' => "#{repo(chef_image)}:#{tag(chef_image)}"
    )
    state[:chef_container] = chef_container.json
  rescue
    debug "driver - #{chef_container_name} already exists"
  end
end

def create_container(args)

def create_container(args)
  with_retries do
    @container = ::Docker::Container.create(args.clone, docker_connection)
    @container = ::Docker::Container.get(args['name'], {}, docker_connection)
  end
rescue ::Docker::Error::ConflictError
  with_retries { @container = ::Docker::Container.get(args['name'], {}, docker_connection) }
end

def data_container_name

def data_container_name
  "#{instance_name}-data"
end

def data_image

def data_image
  config[:data_image]
end

def delete_chef_container

def delete_chef_container
  debug "driver - deleting container #{chef_container_name}"
  delete_container chef_container_name
end

def delete_container(name)

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

def delete_data_container

def delete_data_container
  debug "driver - deleting container #{data_container_name}"
  delete_container data_container_name
end

def delete_image(name)

def delete_image(name)
  with_retries { @image = ::Docker::Image.get(name, {}, docker_connection) }
  with_retries { @image.remove(force: true) }
rescue ::Docker::Error
  puts "Image #{name} not found. Nothing to delete."
end

def delete_runner_container

def delete_runner_container
  debug "driver - deleting container #{runner_container_name}"
  delete_container runner_container_name
end

def delete_work_image

def delete_work_image
  return unless ::Docker::Image.exist?(work_image, {}, docker_connection)
  with_retries { @work_image = ::Docker::Image.get(work_image, {}, docker_connection) }
  begin
    with_retries { @work_image.remove(force: true) }
  rescue ::Docker::Error::ConflictError
    debug "driver - #{work_image} cannot be removed"
  end
end

def destroy(_state)

def destroy(_state)
  if remote_docker_host?
    stop_data_container
    delete_data_container
  end
  stop_runner_container
  delete_runner_container
  delete_work_image
end

def docker_connection

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

def dokken_binds

def dokken_binds
  ret = []
  ret << "#{dokken_sandbox_path}:/opt/kitchen" unless dokken_sandbox_path.nil?
  ret << Array(config[:binds]) unless config[:binds].nil?
  ret.flatten
end

def dokken_volumes

def dokken_volumes
  coerce_volumes(config[:volumes])
end

def dokken_volumes_from

def dokken_volumes_from
  ret = []
  ret << chef_container_name
  ret << data_container_name if remote_docker_host?
  ret
end

def exposed_ports(config, rules)

def exposed_ports(config, rules)
  Array(rules).each do |prt_string|
    guest, _host = prt_string.to_s.split(':').reverse
    config["#{guest}/tcp"] = {}
  end
  config
end

def image_prefix

def image_prefix
  config[:image_prefix]
end

def instance_platform_name

def instance_platform_name
  instance.platform.name
end

def make_data_image

def make_data_image
  debug 'driver - calling create_data_image'
  create_data_image
end

def platform_image

def platform_image
  config[:image]
end

def port_forwards(config, rules)

def port_forwards(config, rules)
  Array(rules).each do |prt_string|
    guest, host = prt_string.to_s.split(':').reverse
    config["#{guest}/tcp"] = [{
      HostPort: host || '',
    }]
  end
  config
end

def pull_chef_image

def pull_chef_image
  debug "driver - pulling #{chef_image} #{repo(chef_image)} #{tag(chef_image)}"
  pull_if_missing chef_image
end

def pull_if_missing(image)

def pull_if_missing(image)
  return if ::Docker::Image.exist?("#{repo(image)}:#{tag(image)}", {}, docker_connection)
  pull_image image
end

def pull_image(image)

def pull_image(image)
  with_retries do
    ::Docker::Image.create({ 'fromImage' => "#{repo(image)}:#{tag(image)}" }, nil, docker_connection)
  end
end

def pull_platform_image

def pull_platform_image
  debug "driver - pulling #{chef_image} #{repo(platform_image)} #{tag(platform_image)}"
  pull_if_missing platform_image
end

def remote_docker_host?

def remote_docker_host?
  return true if config[:docker_host_url] =~ /^tcp:/
  false
end

def repo(image)

def repo(image)
  image.split(':')[0]
end

def run_container(args)

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

def runner_container_name

def runner_container_name
  instance_name.to_s
end

def save_misc_state(state)

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

def start_data_container(state)

def start_data_container(state)
  debug "driver - creating #{data_container_name}"
  data_container = run_container(
    'name' => data_container_name,
    'Image' => "#{repo(data_image)}:#{tag(data_image)}",
    'HostConfig' => {
      'PortBindings' => port_forwards({}, '22'),
      'PublishAllPorts' => true,
    }
  )
  state[:data_container] = data_container.json
end

def start_runner_container(state)

def start_runner_container(state)
  debug "driver - starting #{runner_container_name}"
  runner_container = run_container(
    'name' => runner_container_name,
    'Cmd' => Shellwords.shellwords(config[:pid_one_command]),
    'Image' => "#{repo(work_image)}:#{tag(work_image)}",
    'Hostname' => config[:hostname],
    'ExposedPorts' => exposed_ports({}, config[:forward]),
    'Volumes' => dokken_volumes,
    'HostConfig' => {
      'Privileged' => config[:privileged],
      'VolumesFrom' => dokken_volumes_from,
      'Binds' => dokken_binds,
      'Dns' => config[:dns],
      'DnsSearch' => config[:dns_search],
      'Links' => Array(config[:links]),
      'CapAdd' => Array(config[:cap_add]),
      'CapDrop' => Array(config[:cap_drop]),
      'SecurityOpt' => Array(config[:security_opt]),
      'NetworkMode' => config[:network_mode],
      'PortBindings' => port_forwards({}, config[:forward]),
    }
  )
  state[:runner_container] = runner_container.json
end

def stop_container(name)

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

def stop_data_container

def stop_data_container
  debug "driver - stopping container #{data_container_name}"
  stop_container data_container_name
end

def stop_runner_container

def stop_runner_container
  debug "driver - stopping container #{runner_container_name}"
  stop_container runner_container_name
end

def tag(image)

def tag(image)
  image.split(':')[1] || 'latest'
end

def wait_running_state(name, v)

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

def with_retries

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
    retry if tries > 0
    raise e
  end
end

def work_image

def work_image
  return "#{image_prefix}/#{instance_name}" unless image_prefix.nil?
  instance_name
end

def work_image_dockerfile

def work_image_dockerfile
  from = "FROM #{platform_image}"
  custom = ['RUN /bin/sh -c "echo Built with Test Kitchen"']
  Array(config[:intermediate_instructions]).each { |c| custom << c }
  [from, custom].join("\n")
end