lib/kitchen/transport/dokken.rb
# # Author:: Sean OMeara (<sean@chef.io>) # # Copyright (C) 2015, Sean OMeara # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'kitchen' require 'tmpdir' require 'digest/sha1' require_relative 'dokken/helpers' include Dokken::Transport::Helpers module Kitchen module Transport # Wrapped exception for any internally raised errors. # # @author Sean OMeara <sean@chef.io> class DockerExecFailed < TransportFailed; end # A Transport which uses Docker tricks to execute commands and # transfer files. # # @author Sean OMeara <sean@chef.io> class Dokken < Kitchen::Transport::Base kitchen_transport_api_version 1 plugin_version Kitchen::VERSION default_config :docker_host_url, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock' default_config :read_timeout, 3600 default_config :write_timeout, 3600 # (see Base#connection) def connection(state, &block) options = connection_options(config.to_hash.merge(state)) if @connection && @connection_options == options reuse_connection(&block) else create_new_connection(options, &block) end end # @author Sean OMeara <sean@chef.io> class Connection < Kitchen::Transport::Dokken::Connection def docker_connection @docker_connection ||= Docker::Connection.new(options[:docker_host_url], options[:docker_host_options]) end def execute(command) return if command.nil? runner = Docker::Container.get(instance_name, {}, docker_connection) o = runner.exec(Shellwords.shellwords(command)) { |stream, chunk| puts "#{stream}: #{chunk}" } exit_code = o[2] if exit_code != 0 fail Transport::DockerExecFailed, "Docker Exec (#{exit_code}) for command: [#{command}]" end begin old_image = Docker::Image.get(work_image, {}, docker_connection) old_image.remove rescue debug "#{work_image} not present. nothing to remove." end new_image = runner.commit new_image.tag('repo' => work_image, 'tag' => 'latest', 'force' => 'true') end def upload(locals, remote) ip = options[:docker_host_url].split('://')[1].split(':')[0] port = options[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostPort] tmpdir = Dir.tmpdir FileUtils.mkdir_p "#{tmpdir}/dokken" File.write("#{tmpdir}/dokken/id_rsa", insecure_ssh_private_key) FileUtils.chmod(0600, "#{tmpdir}/dokken/id_rsa") rsync_cmd = '/usr/bin/rsync -a -e' rsync_cmd << ' \'' rsync_cmd << 'ssh -2' rsync_cmd << " -i #{tmpdir}/dokken/id_rsa" rsync_cmd << ' -o CheckHostIP=no' rsync_cmd << ' -o Compression=no' rsync_cmd << ' -o PasswordAuthentication=no' rsync_cmd << ' -o StrictHostKeyChecking=no' rsync_cmd << ' -o UserKnownHostsFile=/dev/null' rsync_cmd << ' -o LogLevel=ERROR' rsync_cmd << " -p #{port}" rsync_cmd << '\'' rsync_cmd << " #{locals.join(' ')} root@#{ip}:#{remote}" system(rsync_cmd) end def login_command runner = "#{options[:instance_name]}" args = ['exec', '-it', runner, '/bin/bash', '-login', '-i'] LoginCommand.new('docker', args) end private def instance_name options[:instance_name] end def work_image return "#{image_prefix}/#{instance_name}" unless image_prefix.nil? instance_name end def image_prefix options[:image_prefix] end end private # Builds the hash of options needed by the Connection object on # construction. # # @param data [Hash] merged configuration and mutable state data # @return [Hash] hash of connection options # @api private def connection_options(data) # rubocop:disable Metrics/MethodLength opts = {} opts[:docker_host_url] = config[:docker_host_url] opts[:docker_host_options] = Docker.options opts[:data_container] = data[:data_container] opts[:instance_name] = data[:instance_name] opts end # Creates a new Dokken Connection instance and save it for potential future # reuse. # # @param options [Hash] conneciton options # @return [Ssh::Connection] an SSH Connection instance # @api private def create_new_connection(options, &block) if @connection logger.debug("[Dokken] shutting previous connection #{@connection}") @connection.close end @connection_options = options @connection = Kitchen::Transport::Dokken::Connection.new(options, &block) end # Return the last saved Dokken connection instance. # # @return [Dokken::Connection] an Dokken Connection instance # @api private def reuse_connection logger.debug("[Dokken] reusing existing connection #{@connection}") yield @connection if block_given? @connection end end end end