## Author:: Sean OMeara (<sean@sean.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"net/scp"require"tmpdir"unlessdefined?(Dir.mktmpdir)require"digest/sha1"unlessdefined?(Digest::SHA1)require_relative"../helpers"includeDokken::HelpersmoduleKitchenmoduleTransport# Wrapped exception for any internally raised errors.## @author Sean OMeara <sean@sean.io>classDockerExecFailed<TransportFailed;end# A Transport which uses Docker tricks to execute commands and# transfer files.## @author Sean OMeara <sean@sean.io>classDokken<Kitchen::Transport::Basekitchen_transport_api_version2plugin_versionKitchen::VERSIONdefault_config:docker_info,docker_infodefault_config:docker_host_url,default_docker_hostdefault_config:read_timeout,3600default_config:write_timeout,3600default_config:login_command,"docker"default_config:host_ip_overridedo|transport|ifrunning_inside_docker_desktop?"host.docker.internal"elsiftransport.docker_for_mac_or_win?"localhost"elsefalseendend# (see Base#connection)defconnection(state,&block)options=connection_options(config.to_hash.merge(state))if@connection&&@connection_options==optionsreuse_connection(&block)elsecreate_new_connection(options,&block)endend# @author Sean OMeara <sean@sean.io>classConnection<Kitchen::Transport::Dokken::Connectiondefdocker_connection@docker_connection||=::Docker::Connection.new(options[:docker_host_url],options[:docker_host_options])enddefexecute(command)returnifcommand.nil?with_retries{@runner=::Docker::Container.get(instance_name,{},docker_connection)}with_retriesdoo=@runner.exec(Shellwords.shellwords(command),wait: options[:timeout],"e"=>{"TERM"=>"xterm"})do|_stream,chunk|logger<<chunkend@exit_code=o[2]endraiseTransport::DockerExecFailed.new("Docker Exec (#{@exit_code}) for command: [#{command}]",@exit_code)if@exit_code!=0enddefupload(locals,remote)ifoptions[:host_ip_override]# Allow connecting to any ip/hostname to support sibling containersssh_ip=options[:host_ip_override]ssh_port=options[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostPort]elsif/unix:/.match?(options[:docker_host_url])ifoptions[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostIp]=="0.0.0.0"ssh_ip=options[:data_container][:NetworkSettings][:IPAddress]ssh_port="22"else# we should read the proper mapped ip, since this allows us to upload the filesssh_ip=options[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostIp]ssh_port=options[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostPort]endelsif/tcp:/.match?(options[:docker_host_url])name=options[:data_container][:Name]# DOCKER_HOSTdocker_host_url_ip=options[:docker_host_url].split("tcp://")[1].split(":")[0]# mapped IP of data containercandidate_ip=::Docker::Container.all.finddo|x|x.info["Names"][0].eql?(name)end.info["NetworkSettings"]["Networks"]["dokken"]["IPAddress"]# mapped portcandidate_ssh_port=options[:data_container][:NetworkSettings][:Ports][:"22/tcp"][0][:HostPort]debug"candidate_ip - #{candidate_ip}"debug"candidate_ssh_port - #{candidate_ssh_port}"ifport_open?(candidate_ip,candidate_ssh_port)debug"candidate_ip - #{candidate_ip}/#{candidate_ssh_port} open"ssh_ip=candidate_ipssh_port=candidate_ssh_portelsifport_open?(candidate_ip,"22")ssh_ip=candidate_ipssh_port="22"debug"candidate_ip - #{candidate_ip}/22 open"elsessh_ip=docker_host_url_ipssh_port=candidate_ssh_portendelseraiseKitchen::UserError,"docker_host_url must be tcp:// or unix://"enddebug"ssh_ip : #{ssh_ip}"debug"ssh_port : #{ssh_port}"tmpdir=Dir.tmpdir+"/dokken/"FileUtils.mkdir_ptmpdir.to_s,mode: 0o777tmpdir+=Process.uid.to_sFileUtils.mkdir_ptmpdir.to_sFile.write("#{tmpdir}/id_rsa",insecure_ssh_private_key)FileUtils.chmod(0o600,"#{tmpdir}/id_rsa")beginrsync_cmd="/usr/bin/rsync -a -e"rsync_cmd<<" '"rsync_cmd<<"ssh -2"rsync_cmd<<" -i #{tmpdir}/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 #{ssh_port}"rsync_cmd<<"'"rsync_cmd<<" #{locals.join(" ")} root@#{ssh_ip}:#{remote}"debug"rsync_cmd :#{rsync_cmd}:"`#{rsync_cmd}`rescueErrno::ENOENTdebug"Rsync is not installed. Falling back to SCP."locals.eachdo|local|Net::SCP.upload!(ssh_ip,"root",local,remote,recursive: true,ssh: {port: ssh_port,keys: ["#{tmpdir}/id_rsa"]})endendenddeflogin_command@runner=options[:instance_name].to_scols=`tput cols`lines=`tput lines`args=["exec","-e","COLUMNS=#{cols}","-e","LINES=#{lines}","-it",@runner,"/bin/bash","-login","-i"]LoginCommand.new(options[:login_command],args)endprivatedefinstance_nameoptions[:instance_name]enddefwork_imagereturn"#{image_prefix}/#{instance_name}"unlessimage_prefix.nil?instance_nameenddefimage_prefixoptions[:image_prefix]enddefwith_retriestries=20beginyield# Only catch errors that can be fixed with retries.rescue::Docker::Error::ServerError,# 404::Docker::Error::UnexpectedResponseError,# 400::Docker::Error::TimeoutError,::Docker::Error::IOError=>etries-=1retryiftries>0raiseeendendend# Detect whether or not we are running in Docker for Mac or Windows## @return [TrueClass,FalseClass]defdocker_for_mac_or_win?::Docker.info(::Docker::Connection.new(config[:docker_host_url],{}))["Name"]=="docker-desktop"rescuefalseendprivate# 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 privatedefconnection_options(data)opts={}opts[:logger]=loggeropts[:host_ip_override]=config[:host_ip_override]opts[:docker_host_url]=config[:docker_host_url]opts[:docker_host_options]=::Docker.optionsopts[:data_container]=data[:data_container]opts[:instance_name]=data[:instance_name]opts[:timeout]=data[:write_timeout]opts[:login_command]=data[:login_command]optsend# 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 privatedefcreate_new_connection(options,&block)if@connectionlogger.debug("[Dokken] shutting previous connection #{@connection}")@connection.closeend@connection=Kitchen::Transport::Dokken::Connection.new(options,&block)end# Return the last saved Dokken connection instance.## @return [Dokken::Connection] an Dokken Connection instance# @api privatedefreuse_connectionlogger.debug("[Dokken] reusing existing connection #{@connection}")yield@connectionifblock_given?@connectionendendendend