lib/kitchen/driver/docker.rb in kitchen-docker-2.9.0 vs lib/kitchen/driver/docker.rb in kitchen-docker-2.10.0
- old
+ new
@@ -1,457 +1,164 @@
-# -*- encoding: utf-8 -*-
-#
-# Copyright (C) 2014, Sean Porter
-#
-# 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 'json'
-require 'securerandom'
-require 'uri'
-require 'net/ssh'
-require 'tempfile'
-require 'shellwords'
-require 'base64'
-
-require 'kitchen/driver/base'
-
-require_relative './docker/erb'
-
-module Kitchen
- module Driver
- # Docker driver for Kitchen.
- #
- # @author Sean Porter <portertech@gmail.com>
- class Docker < Kitchen::Driver::Base
- include ShellOut
-
- default_config :binary, 'docker'
- default_config :socket, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
- default_config :privileged, false
- default_config :cap_add, nil
- default_config :cap_drop, nil
- default_config :security_opt, nil
- default_config :use_cache, true
- default_config :remove_images, false
- default_config :run_command, '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes ' +
- '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
- default_config :username, 'kitchen'
- default_config :tls, false
- default_config :tls_verify, false
- default_config :tls_cacert, nil
- default_config :tls_cert, nil
- default_config :tls_key, nil
- default_config :publish_all, false
- default_config :wait_for_sshd, true
- default_config :private_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa')
- default_config :public_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa.pub')
- default_config :build_options, nil
- default_config :run_options, nil
- default_config :use_internal_docker_network, false
-
- default_config :use_sudo, false
-
- default_config :image do |driver|
- driver.default_image
- end
-
- default_config :platform do |driver|
- driver.default_platform
- end
-
- default_config :disable_upstart, true
-
- default_config :build_context do |driver|
- !driver.remote_socket?
- end
-
- default_config :instance_name do |driver|
- # Borrowed from kitchen-rackspace
- [
- driver.instance.name.gsub(/\W/, ''),
- (Etc.getlogin || 'nologin').gsub(/\W/, ''),
- Socket.gethostname.gsub(/\W/, '')[0..20],
- Array.new(8) { rand(36).to_s(36) }.join
- ].join('-')
- end
-
- MUTEX_FOR_SSH_KEYS = Mutex.new
-
- def verify_dependencies
- run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
- rescue
- raise UserError,
- 'You must first install the Docker CLI tool https://www.docker.com/get-started'
- end
-
- def dev_null
- case RbConfig::CONFIG["host_os"]
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
- "NUL"
- else
- "/dev/null"
- end
- end
-
- def default_image
- platform, release = instance.platform.name.split('-')
- if platform == 'centos' && release
- release = 'centos' + release.split('.').first
- end
- release ? [platform, release].join(':') : platform
- end
-
- def default_platform
- instance.platform.name.split('-').first
- end
-
- def create(state)
- generate_keys
- state[:username] = config[:username]
- state[:ssh_key] = config[:private_key]
- state[:image_id] = build_image(state) unless state[:image_id]
- state[:container_id] = run_container(state) unless state[:container_id]
- state[:hostname] = 'localhost'
- if remote_socket?
- state[:hostname] = socket_uri.host
- elsif config[:use_internal_docker_network]
- state[:hostname] = container_ip(state)
- end
- state[:port] = container_ssh_port(state)
- if config[:wait_for_sshd]
- instance.transport.connection(state) do |conn|
- conn.wait_until_ready
- end
- end
- end
-
- def destroy(state)
- rm_container(state) if container_exists?(state)
- if config[:remove_images] && state[:image_id]
- rm_image(state) if image_exists?(state)
- end
- end
-
- def remote_socket?
- config[:socket] ? socket_uri.scheme == 'tcp' : false
- end
-
- protected
-
- def socket_uri
- URI.parse(config[:socket])
- end
-
- def docker_command(cmd, options={})
- docker = config[:binary].dup
- docker << " -H #{config[:socket]}" if config[:socket]
- docker << " --tls" if config[:tls]
- docker << " --tlsverify" if config[:tls_verify]
- docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
- docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
- docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
- run_command("#{docker} #{cmd}", options.merge({
- quiet: !logger.debug?,
- use_sudo: config[:use_sudo],
- log_subject: Thor::Util.snake_case(self.class.to_s),
- }))
- end
-
- def generate_keys
- MUTEX_FOR_SSH_KEYS.synchronize do
- if !File.exist?(config[:public_key]) || !File.exist?(config[:private_key])
- private_key = OpenSSL::PKey::RSA.new(2048)
- blobbed_key = Base64.encode64(private_key.to_blob).gsub("\n", '')
- public_key = "ssh-rsa #{blobbed_key} kitchen_docker_key"
- File.open(config[:private_key], 'w') do |file|
- file.write(private_key)
- file.chmod(0600)
- end
- File.open(config[:public_key], 'w') do |file|
- file.write(public_key)
- file.chmod(0600)
- end
- end
- end
- end
-
- def build_dockerfile
- from = "FROM #{config[:image]}"
-
- env_variables = ''
- if config[:http_proxy]
- env_variables << "ENV http_proxy #{config[:http_proxy]}\n"
- env_variables << "ENV HTTP_PROXY #{config[:http_proxy]}\n"
- end
-
- if config[:https_proxy]
- env_variables << "ENV https_proxy #{config[:https_proxy]}\n"
- env_variables << "ENV HTTPS_PROXY #{config[:https_proxy]}\n"
- end
-
- if config[:no_proxy]
- env_variables << "ENV no_proxy #{config[:no_proxy]}\n"
- env_variables << "ENV NO_PROXY #{config[:no_proxy]}\n"
- end
-
- platform = case config[:platform]
- when 'debian', 'ubuntu'
- disable_upstart = <<-eos
- RUN [ ! -f "/sbin/initctl" ] || dpkg-divert --local --rename --add /sbin/initctl && ln -sf /bin/true /sbin/initctl
- eos
- packages = <<-eos
- ENV DEBIAN_FRONTEND noninteractive
- ENV container docker
- RUN apt-get update
- RUN apt-get install -y sudo openssh-server curl lsb-release
- eos
- config[:disable_upstart] ? disable_upstart + packages : packages
- when 'rhel', 'centos', 'oraclelinux', 'amazonlinux'
- <<-eos
- ENV container docker
- RUN yum clean all
- RUN yum install -y sudo openssh-server openssh-clients which curl
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
- eos
- when 'fedora'
- <<-eos
- ENV container docker
- RUN dnf clean all
- RUN dnf install -y sudo openssh-server openssh-clients which curl
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
- eos
- when 'opensuse/tumbleweed', 'opensuse/leap', 'opensuse', 'sles'
- <<-eos
- ENV container docker
- RUN zypper install -y sudo openssh which curl
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
- eos
- when 'arch'
- # See https://bugs.archlinux.org/task/47052 for why we
- # blank out limits.conf.
- <<-eos
- RUN pacman --noconfirm -Sy archlinux-keyring
- RUN pacman-db-upgrade
- RUN pacman --noconfirm -Syu openssl openssh sudo curl
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
- RUN echo >/etc/security/limits.conf
- eos
- when 'gentoo'
- <<-eos
- RUN emerge --sync
- RUN emerge net-misc/openssh app-admin/sudo
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
- eos
- when 'gentoo-paludis'
- <<-eos
- RUN cave sync
- RUN cave resolve -zx net-misc/openssh app-admin/sudo
- RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
- RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
- eos
- else
- raise ActionFailed,
- "Unknown platform '#{config[:platform]}'"
- end
-
- username = config[:username]
- public_key = IO.read(config[:public_key]).strip
- homedir = username == 'root' ? '/root' : "/home/#{username}"
-
- base = <<-eos
- RUN if ! getent passwd #{username}; then \
- useradd -d #{homedir} -m -s /bin/bash -p '*' #{username}; \
- fi
- RUN echo "#{username} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
- RUN echo "Defaults !requiretty" >> /etc/sudoers
- RUN mkdir -p #{homedir}/.ssh
- RUN chown -R #{username} #{homedir}/.ssh
- RUN chmod 0700 #{homedir}/.ssh
- RUN touch #{homedir}/.ssh/authorized_keys
- RUN chown #{username} #{homedir}/.ssh/authorized_keys
- RUN chmod 0600 #{homedir}/.ssh/authorized_keys
- RUN mkdir -p /run/sshd
- eos
- custom = ''
- Array(config[:provision_command]).each do |cmd|
- custom << "RUN #{cmd}\n"
- end
- ssh_key = "RUN echo #{Shellwords.escape(public_key)} >> #{homedir}/.ssh/authorized_keys"
- # Empty string to ensure the file ends with a newline.
- [from, env_variables, platform, base, custom, ssh_key, ''].join("\n")
- end
-
- def dockerfile
- if config[:dockerfile]
- template = IO.read(File.expand_path(config[:dockerfile]))
- context = DockerERBContext.new(config.to_hash)
- ERB.new(template).result(context.get_binding)
- else
- build_dockerfile
- end
- end
-
- def parse_image_id(output)
- output.each_line do |line|
- if line =~ /image id|build successful|successfully built/i
- return line.split(/\s+/).last
- end
- end
- raise ActionFailed,
- 'Could not parse Docker build output for image ID'
- end
-
- def build_image(state)
- cmd = "build"
- cmd << " --no-cache" unless config[:use_cache]
- extra_build_options = config_to_options(config[:build_options])
- cmd << " #{extra_build_options}" unless extra_build_options.empty?
- dockerfile_contents = dockerfile
- build_context = config[:build_context] ? '.' : '-'
- file = Tempfile.new('Dockerfile-kitchen', Dir.pwd)
- output = begin
- file.write(dockerfile)
- file.close
- docker_command("#{cmd} -f #{Shellwords.escape(dockerfile_path(file))} #{build_context}", :input => dockerfile_contents)
- ensure
- file.close unless file.closed?
- file.unlink
- end
- parse_image_id(output)
- end
-
- def parse_container_id(output)
- container_id = output.chomp
- unless [12, 64].include?(container_id.size)
- raise ActionFailed,
- 'Could not parse Docker run output for container ID'
- end
- container_id
- end
-
- def build_run_command(image_id)
- cmd = "run -d -p 22"
- Array(config[:forward]).each {|port| cmd << " -p #{port}"}
- Array(config[:dns]).each {|dns| cmd << " --dns #{dns}"}
- Array(config[:add_host]).each {|host, ip| cmd << " --add-host=#{host}:#{ip}"}
- Array(config[:volume]).each {|volume| cmd << " -v #{volume}"}
- Array(config[:volumes_from]).each {|container| cmd << " --volumes-from #{container}"}
- Array(config[:links]).each {|link| cmd << " --link #{link}"}
- Array(config[:devices]).each {|device| cmd << " --device #{device}"}
- cmd << " --name #{config[:instance_name]}" if config[:instance_name]
- cmd << " -P" if config[:publish_all]
- cmd << " -h #{config[:hostname]}" if config[:hostname]
- cmd << " -m #{config[:memory]}" if config[:memory]
- cmd << " -c #{config[:cpu]}" if config[:cpu]
- cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
- cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
- cmd << " --privileged" if config[:privileged]
- Array(config[:cap_add]).each {|cap| cmd << " --cap-add=#{cap}"} if config[:cap_add]
- Array(config[:cap_drop]).each {|cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
- Array(config[:security_opt]).each {|opt| cmd << " --security-opt=#{opt}"} if config[:security_opt]
- extra_run_options = config_to_options(config[:run_options])
- cmd << " #{extra_run_options}" unless extra_run_options.empty?
- cmd << " #{image_id} #{config[:run_command]}"
- cmd
- end
-
- def run_container(state)
- cmd = build_run_command(state[:image_id])
- output = docker_command(cmd)
- parse_container_id(output)
- end
-
- def container_exists?(state)
- state[:container_id] && !!docker_command("top #{state[:container_id]}") rescue false
- end
-
- def image_exists?(state)
- state[:image_id] && !!docker_command("docker inspect --type=image #{state[:image_id]}") rescue false
- end
-
- def parse_container_ssh_port(output)
- begin
- _host, port = output.split(':')
- port.to_i
- rescue
- raise ActionFailed,
- 'Could not parse Docker port output for container SSH port'
- end
- end
-
- def container_ssh_port(state)
- begin
- if config[:use_internal_docker_network]
- return 22
- end
- output = docker_command("port #{state[:container_id]} 22/tcp")
- parse_container_ssh_port(output)
- rescue
- raise ActionFailed,
- 'Docker reports container has no ssh port mapped'
- end
- end
-
- def container_ip(state)
- begin
- cmd = "inspect --format '{{ .NetworkSettings.IPAddress }}'"
- cmd << " #{state[:container_id]}"
- docker_command(cmd).strip
- rescue
- raise ActionFailed,
- 'Error getting internal IP of Docker container'
- end
- end
-
- def rm_container(state)
- container_id = state[:container_id]
- docker_command("stop -t 0 #{container_id}")
- docker_command("rm #{container_id}")
- end
-
- def rm_image(state)
- image_id = state[:image_id]
- docker_command("rmi #{image_id}")
- end
-
- # Convert the config input for `:build_options` or `:run_options` in to a
- # command line string for use with Docker.
- #
- # @since 2.5.0
- # @param config [nil, String, Array, Hash] Config data to convert.
- # @return [String]
- def config_to_options(config)
- case config
- when nil
- ''
- when String
- config
- when Array
- config.map {|c| config_to_options(c) }.join(' ')
- when Hash
- config.map {|k, v| Array(v).map {|c| "--#{k}=#{Shellwords.escape(c)}" }.join(' ') }.join(' ')
- end
- end
-
- def dockerfile_path(file)
- config[:build_context] ? Pathname.new(file.path).relative_path_from(Pathname.pwd).to_s : file.path
- end
-
- end
- end
-end
+#
+# Copyright (C) 2014, Sean Porter
+#
+# 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 'json'
+require 'securerandom'
+require 'net/ssh'
+
+require 'kitchen/driver/base'
+
+require_relative '../docker/container/linux'
+require_relative '../docker/container/windows'
+require_relative '../docker/helpers/cli_helper'
+require_relative '../docker/helpers/container_helper'
+
+module Kitchen
+ module Driver
+ # Docker driver for Kitchen.
+ #
+ # @author Sean Porter <portertech@gmail.com>
+ class Docker < Kitchen::Driver::Base
+ include Kitchen::Docker::Helpers::CliHelper
+ include Kitchen::Docker::Helpers::ContainerHelper
+ include ShellOut
+
+ default_config :binary, 'docker'
+ default_config :build_options, nil
+ default_config :cap_add, nil
+ default_config :cap_drop, nil
+ default_config :disable_upstart, true
+ default_config :env_variables, nil
+ default_config :interactive, false
+ default_config :private_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa')
+ default_config :privileged, false
+ default_config :public_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa.pub')
+ default_config :publish_all, false
+ default_config :remove_images, false
+ default_config :run_options, nil
+ default_config :security_opt, nil
+ default_config :tls, false
+ default_config :tls_cacert, nil
+ default_config :tls_cert, nil
+ default_config :tls_key, nil
+ default_config :tls_verify, false
+ default_config :tty, false
+ default_config :use_cache, true
+ default_config :use_internal_docker_network, false
+ default_config :use_sudo, false
+ default_config :wait_for_transport, true
+
+ default_config :build_context do |driver|
+ !driver.remote_socket?
+ end
+
+ default_config :image do |driver|
+ driver.default_image
+ end
+
+ default_config :instance_name do |driver|
+ # Borrowed from kitchen-rackspace
+ [
+ driver.instance.name.gsub(/\W/, ''),
+ (Etc.getlogin || 'nologin').gsub(/\W/, ''),
+ Socket.gethostname.gsub(/\W/, '')[0..20],
+ Array.new(8) { rand(36).to_s(36) }.join
+ ].join('-')
+ end
+
+ default_config :platform do |driver|
+ driver.default_platform
+ end
+
+ default_config :run_command do |driver|
+ if driver.windows_os?
+ # Launch arbitrary process to keep the Windows container alive
+ # If running in interactive mode, launch powershell.exe instead
+ if driver[:interactive]
+ 'powershell.exe'
+ else
+ 'ping -t localhost'
+ end
+ else
+ '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes '\
+ '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
+ end
+ end
+
+ default_config :socket do |driver|
+ socket = 'unix:///var/run/docker.sock'
+ socket = 'npipe:////./pipe/docker_engine' if driver.windows_os?
+ ENV['DOCKER_HOST'] || socket
+ end
+
+ default_config :username do |driver|
+ # Return nil to prevent username from being added to Docker
+ # command line args for Windows if a username was not specified
+ if driver.windows_os?
+ nil
+ else
+ 'kitchen'
+ end
+ end
+
+ def verify_dependencies
+ run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
+ rescue
+ raise UserError, 'You must first install the Docker CLI tool https://www.docker.com/get-started'
+ end
+
+ def create(state)
+ container.create(state)
+
+ wait_for_transport(state)
+ end
+
+ def destroy(state)
+ container.destroy(state)
+ end
+
+ def wait_for_transport(state)
+ if config[:wait_for_transport]
+ instance.transport.connection(state) do |conn|
+ conn.wait_until_ready
+ end
+ end
+ end
+
+ def default_image
+ platform, release = instance.platform.name.split('-')
+ if platform == 'centos' && release
+ release = 'centos' + release.split('.').first
+ end
+ release ? [platform, release].join(':') : platform
+ end
+
+ def default_platform
+ instance.platform.name.split('-').first
+ end
+
+ protected
+
+ def container
+ @container ||= if windows_os?
+ Kitchen::Docker::Container::Windows.new(config)
+ else
+ Kitchen::Docker::Container::Linux.new(config)
+ end
+ @container
+ end
+ end
+ end
+end