class Kitchen::Driver::Dokken
@author Sean OMeara <sean@sean.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 'latest' if config[:chef_version] == 'stable' 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].push(b) unless config[:binds].include?(b) || 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)
def create(state) # 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? 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) lockfile = Lockfile.new "#{home_dir}/.dokken-#{chef_container_name}.lock" begin lockfile.lock with_retries { ::Docker::Container.get(chef_container_name, {}, docker_connection) } rescue ::Docker::Error::NotFoundError with_retries do begin debug "driver - creating volume container #{chef_container_name} from #{chef_image}" config = { 'name' => chef_container_name, 'Cmd' => 'true', 'Image' => "#{repo(chef_image)}:#{tag(chef_image)}", 'HostConfig' => { 'NetworkMode' => self[:network_mode], }, } chef_container = create_container(config) state[:chef_container] = chef_container.json rescue ::Docker::Error => e raise "driver - #{chef_container_name} failed to create #{e}" end end ensure lockfile.unlock end end
def create_container(args)
def create_container(args) with_retries { @container = ::Docker::Container.get(args['name'], {}, docker_connection) } rescue with_retries do begin info "Creating container #{args['name']}" debug "driver - create_container args #{args}" with_retries do begin @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 end rescue ::Docker::Error => e debug "driver - error :#{e}:" raise "driver - failed to create_container #{args['name']}" end end 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) } with_retries do begin with_retries { @work_image.remove(force: true) } rescue ::Docker::Error::ConflictError debug "driver - #{work_image} cannot be removed" end 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 dokken_delete_sandbox 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_kitchen_sandbox}:/opt/kitchen" unless dokken_kitchen_sandbox.nil? || remote_docker_host? ret << "#{dokken_verifier_sandbox}:/opt/verifier" unless dokken_verifier_sandbox.nil? || remote_docker_host? 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 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 make_dokken_network
def make_dokken_network lockfile = Lockfile.new "#{home_dir}/.dokken-network.lock" begin lockfile.lock with_retries { ::Docker::Network.get('dokken', {}, docker_connection) } rescue begin with_retries { ::Docker::Network.create('dokken', {}) } rescue ::Docker::Error => e debug "driver - error :#{e}:" end ensure lockfile.unlock end end
def parse_image_name(image)
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
def platform_image
def platform_image config[:image] || platform_image_from_name end
def platform_image_from_name
def platform_image_from_name platform, release = instance.platform.name.split('-') release ? [platform, release].join(':') : platform 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 repo(image)
def repo(image) parse_image_name(image)[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}" config = { 'name' => data_container_name, 'Image' => "#{repo(data_image)}:#{tag(data_image)}", 'HostConfig' => { 'PortBindings' => port_bindings, 'PublishAllPorts' => true, 'NetworkMode' => 'bridge', }, 'NetworkingConfig' => { 'EndpointsConfig' => { self[:network_mode] => { 'Aliases' => Array(self[:hostname]), }, }, }, } data_container = run_container(config) state[:data_container] = data_container.json end
def start_runner_container(state)
def start_runner_container(state) debug "driver - starting #{runner_container_name}" config = { 'name' => runner_container_name, 'Cmd' => Shellwords.shellwords(self[:pid_one_command]), 'Image' => "#{repo(work_image)}:#{tag(work_image)}", 'Hostname' => self[:hostname], 'Env' => self[:env], 'ExposedPorts' => exposed_ports, 'Volumes' => dokken_volumes, 'HostConfig' => { 'Privileged' => self[:privileged], 'VolumesFrom' => dokken_volumes_from, 'Binds' => dokken_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, }, 'NetworkingConfig' => { 'EndpointsConfig' => { self[:network_mode] => { 'Aliases' => Array(self[:hostname]), }, }, }, } unless self[:entrypoint].to_s.length == 0 config.merge!('Entrypoint' => self[:entrypoint]) end runner_container = run_container(config) 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) parse_image_name(image)[1] 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, ::Docker::Error::NotFoundError => e tries -= 1 sleep 0.1 retry if tries > 0 debug "tries: #{tries} error: #{e}" 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 dockerfile_contents = [ "FROM #{platform_image}", "LABEL X-Built-By=kitchen-dokken X-Built-From=#{platform_image}" ] Array(config[:intermediate_instructions]).each { |c| dockerfile_contents << c } dockerfile_contents.join("\n") end