test-kitchen / kitchen-docker

@@ -1,4 +1,3 @@
Loading
1 -
# -*- encoding: utf-8 -*-
2 1
#
3 2
# Copyright (C) 2014, Sean Porter
4 3
#
@@ -17,10 +16,8 @@
Loading
17 16
require 'erb'
18 17
19 18
module Kitchen
20 -
21 -
  module Driver
22 -
23 -
    class DockerERBContext
19 +
  module Docker
20 +
    class ERBContext
24 21
      def initialize(config={})
25 22
        config.each do |key, value|
26 23
          instance_variable_set('@' + key.to_s, value)

@@ -0,0 +1,75 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require_relative 'helpers/cli_helper'
15 +
require_relative 'helpers/container_helper'
16 +
require_relative 'helpers/file_helper'
17 +
require_relative 'helpers/image_helper'
18 +
19 +
module Kitchen
20 +
  module Docker
21 +
    class Container
22 +
      include Kitchen::Docker::Helpers::CliHelper
23 +
      include Kitchen::Docker::Helpers::ContainerHelper
24 +
      include Kitchen::Docker::Helpers::FileHelper
25 +
      include Kitchen::Docker::Helpers::ImageHelper
26 +
27 +
      def initialize(config)
28 +
        @config = config
29 +
      end
30 +
31 +
      def create(state)
32 +
        if container_exists?(state)
33 +
          info("Container ID #{state[:container_id]} already exists.")
34 +
        elsif !container_exists?(state) && state[:container_id]
35 +
          raise ActionFailed, "Container ID #{state[:container_id]} was found in the kitchen state data, "\
36 +
                              'but the container does not exist.'
37 +
        end
38 +
39 +
        state[:username] = @config[:username]
40 +
      end
41 +
42 +
      def destroy(state)
43 +
        info("[Docker] Destroying Docker container #{state[:container_id]}") if state[:container_id]
44 +
        remove_container(state) if container_exists?(state)
45 +
46 +
        if @config[:remove_images] && state[:image_id]
47 +
          remove_image(state) if image_exists?(state)
48 +
        end
49 +
      end
50 +
51 +
      def hostname(state)
52 +
        hostname = 'localhost'
53 +
54 +
        if remote_socket?
55 +
          hostname = socket_uri.host
56 +
        elsif @config[:use_internal_docker_network]
57 +
          hostname = container_ip_address(state)
58 +
        end
59 +
60 +
        hostname
61 +
      end
62 +
63 +
      def upload(locals, remote)
64 +
        files = locals
65 +
        files = Array(locals) unless locals.is_a?(Array)
66 +
67 +
        files.each do |file|
68 +
          copy_file_to_container(@config, file, remote)
69 +
        end
70 +
71 +
        files
72 +
      end
73 +
    end
74 +
  end
75 +
end

@@ -0,0 +1,172 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'erb'
15 +
require 'json'
16 +
require 'shellwords'
17 +
require 'tempfile'
18 +
require 'uri'
19 +
20 +
require 'kitchen'
21 +
require 'kitchen/configurable'
22 +
require_relative '../erb_context'
23 +
require_relative 'cli_helper'
24 +
25 +
module Kitchen
26 +
  module Docker
27 +
    module Helpers
28 +
      module ContainerHelper
29 +
        include Configurable
30 +
        include Kitchen::Docker::Helpers::CliHelper
31 +
32 +
        def parse_container_id(output)
33 +
          container_id = output.chomp
34 +
35 +
          unless [12, 64].include?(container_id.size)
36 +
            raise ActionFailed, 'Could not parse Docker run output for container ID'
37 +
          end
38 +
39 +
          container_id
40 +
        end
41 +
42 +
        def dockerfile_template
43 +
          template = IO.read(File.expand_path(config[:dockerfile]))
44 +
          context = Kitchen::Docker::ERBContext.new(config.to_hash)
45 +
          ERB.new(template).result(context.get_binding)
46 +
        end
47 +
48 +
        def remote_socket?
49 +
          config[:socket] ? socket_uri.scheme == 'tcp' : false
50 +
        end
51 +
52 +
        def socket_uri
53 +
          URI.parse(config[:socket])
54 +
        end
55 +
56 +
        def dockerfile_path(file)
57 +
          config[:build_context] ? Pathname.new(file.path).relative_path_from(Pathname.pwd).to_s : file.path
58 +
        end
59 +
60 +
        def container_exists?(state)
61 +
          state[:container_id] && !!docker_command("top #{state[:container_id]}") rescue false
62 +
        end
63 +
64 +
        def container_exec(state, command)
65 +
          cmd = build_exec_command(state, command)
66 +
          docker_command(cmd)
67 +
        rescue => e
68 +
          raise "Failed to execute command on Docker container. #{e}"
69 +
        end
70 +
71 +
        def create_dir_on_container(state, path)
72 +
          path = replace_env_variables(state, path)
73 +
          cmd = "mkdir -p #{path}"
74 +
75 +
          if state[:platform].include?('windows')
76 +
            psh = "-Command if(-not (Test-Path \'#{path}\')) { New-Item -Path \'#{path}\' -Force }"
77 +
            cmd = build_powershell_command(psh)
78 +
          end
79 +
80 +
          cmd = build_exec_command(state, cmd)
81 +
          docker_command(cmd)
82 +
        rescue => e
83 +
          raise "Failed to create directory #{path} on container. #{e}"
84 +
        end
85 +
86 +
        def copy_file_to_container(state, local_file, remote_file)
87 +
          debug("Copying local file #{local_file} to #{remote_file} on container")
88 +
89 +
          remote_file = replace_env_variables(state, remote_file)
90 +
91 +
          remote_file = "#{state[:container_id]}:#{remote_file}"
92 +
          cmd = build_copy_command(local_file, remote_file)
93 +
          docker_command(cmd)
94 +
        rescue => e
95 +
          raise "Failed to copy file #{local_file} to container. #{e}"
96 +
        end
97 +
98 +
        def container_env_variables(state)
99 +
          # Retrieves all environment variables from inside container
100 +
          vars = {}
101 +
102 +
          if state[:platform].include?('windows')
103 +
            cmd = build_powershell_command('-Command [System.Environment]::GetEnvironmentVariables() ^| ConvertTo-Json')
104 +
            cmd = build_exec_command(state, cmd)
105 +
            stdout = docker_command(cmd, suppress_output: !logger.debug?).strip
106 +
            vars = ::JSON.parse(stdout)
107 +
          else
108 +
            cmd = build_exec_command(state, 'printenv')
109 +
            stdout = docker_command(cmd, suppress_output: !logger.debug?).strip
110 +
            stdout.split("\n").each { |line| vars[line.split('=')[0]] = line.split('=')[1] }
111 +
          end
112 +
113 +
          vars
114 +
        end
115 +
116 +
        def replace_env_variables(state, str)
117 +
          if str.include?('$env:')
118 +
            key = str[/\$env:(.*?)(\\|$)/, 1]
119 +
            value = container_env_variables(state)[key].to_s.strip
120 +
            str = str.gsub("$env:#{key}", value)
121 +
          elsif str.include?('$')
122 +
            key = str[/\$(.*?)(\/|$)/, 1]
123 +
            value = container_env_variables(state)[key].to_s.strip
124 +
            str = str.gsub("$#{key}", value)
125 +
          end
126 +
127 +
          str
128 +
        end
129 +
130 +
        def run_container(state, transport_port = nil)
131 +
          cmd = build_run_command(state[:image_id], transport_port)
132 +
          output = docker_command(cmd)
133 +
          parse_container_id(output)
134 +
        end
135 +
136 +
        def container_ip_address(state)
137 +
          cmd = "inspect --format '{{ .NetworkSettings.IPAddress }}'"
138 +
          cmd << " #{state[:container_id]}"
139 +
          docker_command(cmd).strip
140 +
        rescue
141 +
          raise ActionFailed, 'Error getting internal IP of Docker container'
142 +
        end
143 +
144 +
        def remove_container(state)
145 +
          container_id = state[:container_id]
146 +
          docker_command("stop -t 0 #{container_id}")
147 +
          docker_command("rm #{container_id}")
148 +
        end
149 +
150 +
        def dockerfile_proxy_config
151 +
          env_variables = ''
152 +
          if config[:http_proxy]
153 +
            env_variables << "ENV http_proxy #{config[:http_proxy]}\n"
154 +
            env_variables << "ENV HTTP_PROXY #{config[:http_proxy]}\n"
155 +
          end
156 +
157 +
          if config[:https_proxy]
158 +
            env_variables << "ENV https_proxy #{config[:https_proxy]}\n"
159 +
            env_variables << "ENV HTTPS_PROXY #{config[:https_proxy]}\n"
160 +
          end
161 +
162 +
          if config[:no_proxy]
163 +
            env_variables << "ENV no_proxy #{config[:no_proxy]}\n"
164 +
            env_variables << "ENV NO_PROXY #{config[:no_proxy]}\n"
165 +
          end
166 +
167 +
          env_variables
168 +
        end
169 +
      end
170 +
    end
171 +
  end
172 +
end

@@ -0,0 +1,149 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'kitchen'
15 +
require 'kitchen/configurable'
16 +
require 'kitchen/logging'
17 +
require 'kitchen/shell_out'
18 +
19 +
module Kitchen
20 +
  module Docker
21 +
    module Helpers
22 +
      module CliHelper
23 +
        include Configurable
24 +
        include Logging
25 +
        include ShellOut
26 +
27 +
        def docker_command(cmd, options={})
28 +
          docker = config[:binary].dup
29 +
          docker << " -H #{config[:socket]}" if config[:socket]
30 +
          docker << ' --tls' if config[:tls]
31 +
          docker << ' --tlsverify' if config[:tls_verify]
32 +
          docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
33 +
          docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
34 +
          docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
35 +
          logger.debug("docker_command: #{docker} #{cmd} shell_opts: #{docker_shell_opts(options)}")
36 +
          run_command("#{docker} #{cmd}", docker_shell_opts(options))
37 +
        end
38 +
39 +
        def build_run_command(image_id, transport_port = nil)
40 +
          cmd = 'run -d'
41 +
          cmd << ' -i' if config[:interactive]
42 +
          cmd << ' -t' if config[:tty]
43 +
          cmd << build_env_variable_args(config[:env_variables]) if config[:env_variables]
44 +
          cmd << " -p #{transport_port}" unless transport_port.nil?
45 +
          Array(config[:forward]).each { |port| cmd << " -p #{port}" }
46 +
          Array(config[:dns]).each { |dns| cmd << " --dns #{dns}" }
47 +
          Array(config[:add_host]).each { |host, ip| cmd << " --add-host=#{host}:#{ip}" }
48 +
          Array(config[:volume]).each { |volume| cmd << " -v #{volume}" }
49 +
          Array(config[:volumes_from]).each { |container| cmd << " --volumes-from #{container}" }
50 +
          Array(config[:links]).each { |link| cmd << " --link #{link}" }
51 +
          Array(config[:devices]).each { |device| cmd << " --device #{device}" }
52 +
          cmd << " --name #{config[:instance_name]}" if config[:instance_name]
53 +
          cmd << ' -P' if config[:publish_all]
54 +
          cmd << " -h #{config[:hostname]}" if config[:hostname]
55 +
          cmd << " -m #{config[:memory]}" if config[:memory]
56 +
          cmd << " -c #{config[:cpu]}" if config[:cpu]
57 +
          cmd << " --gpus #{config[:gpus]}" if config[:gpus]
58 +
          cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
59 +
          cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
60 +
          cmd << ' --privileged' if config[:privileged]
61 +
          cmd << " --isolation #{config[:isolation]}" if config[:isolation]
62 +
          Array(config[:cap_add]).each { |cap| cmd << " --cap-add=#{cap}"} if config[:cap_add]
63 +
          Array(config[:cap_drop]).each { |cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
64 +
          Array(config[:security_opt]).each { |opt| cmd << " --security-opt=#{opt}"} if config[:security_opt]
65 +
          extra_run_options = config_to_options(config[:run_options])
66 +
          cmd << " #{extra_run_options}" unless extra_run_options.empty?
67 +
          cmd << " #{image_id} #{config[:run_command]}"
68 +
          logger.debug("build_run_command: #{cmd}")
69 +
          cmd
70 +
        end
71 +
72 +
        def build_exec_command(state, command)
73 +
          cmd = 'exec'
74 +
          cmd << ' -d' if config[:detach]
75 +
          cmd << build_env_variable_args(config[:env_variables]) if config[:env_variables]
76 +
          cmd << ' --privileged' if config[:privileged]
77 +
          cmd << ' -t' if config[:tty]
78 +
          cmd << ' -i' if config[:interactive]
79 +
          cmd << " -u #{config[:username]}" if config[:username]
80 +
          cmd << " -w #{config[:working_dir]}" if config[:working_dir]
81 +
          cmd << " #{state[:container_id]}"
82 +
          cmd << " #{command}"
83 +
          logger.debug("build_exec_command: #{cmd}")
84 +
          cmd
85 +
        end
86 +
87 +
        def build_copy_command(local_file, remote_file, opts = {})
88 +
          cmd = 'cp'
89 +
          cmd << ' -a' if opts[:archive]
90 +
          cmd << " #{local_file} #{remote_file}"
91 +
          cmd
92 +
        end
93 +
94 +
        def build_powershell_command(args)
95 +
          cmd = 'powershell -ExecutionPolicy Bypass -NoLogo '
96 +
          cmd << args
97 +
          logger.debug("build_powershell_command: #{cmd}")
98 +
          cmd
99 +
        end
100 +
101 +
        def build_env_variable_args(vars)
102 +
          raise ActionFailed, 'Environment variables are not of a Hash type' unless vars.is_a?(Hash)
103 +
104 +
          args = ''
105 +
          vars.each do |k, v|
106 +
            args << " -e #{k.to_s.strip}=\"#{v.to_s.strip}\""
107 +
          end
108 +
109 +
          args
110 +
        end
111 +
112 +
        def dev_null
113 +
          case RbConfig::CONFIG['host_os']
114 +
          when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
115 +
            'NUL'
116 +
          else
117 +
            '/dev/null'
118 +
          end
119 +
        end
120 +
121 +
        def docker_shell_opts(options = {})
122 +
          options[:live_stream] = nil if options[:suppress_output]
123 +
          options.delete(:suppress_output)
124 +
125 +
          options
126 +
        end
127 +
128 +
        # Convert the config input for `:build_options` or `:run_options` in to a
129 +
        # command line string for use with Docker.
130 +
        #
131 +
        # @since 2.5.0
132 +
        # @param config [nil, String, Array, Hash] Config data to convert.
133 +
        # @return [String]
134 +
        def config_to_options(config)
135 +
          case config
136 +
          when nil
137 +
            ''
138 +
          when String
139 +
            config
140 +
          when Array
141 +
            config.map { |c| config_to_options(c) }.join(' ')
142 +
          when Hash
143 +
            config.map { |k, v| Array(v).map { |c| "--#{k}=#{Shellwords.escape(c)}" }.join(' ') }.join(' ')
144 +
          end
145 +
        end
146 +
      end
147 +
    end
148 +
  end
149 +
end

@@ -0,0 +1,136 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'kitchen'
15 +
require 'kitchen/configurable'
16 +
17 +
module Kitchen
18 +
  module Docker
19 +
    module Helpers
20 +
      module DockerfileHelper     
21 +
        include Configurable
22 +
23 +
        def dockerfile_platform
24 +
          case config[:platform]
25 +
          when 'arch'
26 +
            arch_platform
27 +
          when 'debian', 'ubuntu'
28 +
            debian_platform
29 +
          when 'fedora'
30 +
            fedora_platform
31 +
          when 'gentoo'
32 +
            gentoo_platform
33 +
          when 'gentoo-paludis'
34 +
            gentoo_paludis_platform
35 +
          when 'opensuse/tumbleweed', 'opensuse/leap', 'opensuse', 'sles'
36 +
            opensuse_platform
37 +
          when 'rhel', 'centos', 'oraclelinux', 'amazonlinux'
38 +
            rhel_platform
39 +
          else
40 +
            raise ActionFailed, "Unknown platform '#{config[:platform]}'"
41 +
          end
42 +
        end
43 +
44 +
        def arch_platform
45 +
          # See https://bugs.archlinux.org/task/47052 for why we
46 +
          # blank out limits.conf.
47 +
          <<-CODE
48 +
            RUN pacman --noconfirm -Sy archlinux-keyring
49 +
            RUN pacman-db-upgrade
50 +
            RUN pacman --noconfirm -Syu openssl openssh sudo curl
51 +
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
52 +
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
53 +
            RUN echo >/etc/security/limits.conf
54 +
          CODE
55 +
        end
56 +
57 +
        def debian_platform
58 +
          disable_upstart = <<-CODE
59 +
            RUN [ ! -f "/sbin/initctl" ] || dpkg-divert --local --rename --add /sbin/initctl \
60 +
                && ln -sf /bin/true /sbin/initctl
61 +
          CODE
62 +
          packages = <<-CODE
63 +
            ENV DEBIAN_FRONTEND noninteractive
64 +
            ENV container docker
65 +
            RUN apt-get update
66 +
            RUN apt-get install -y sudo openssh-server curl lsb-release
67 +
          CODE
68 +
          config[:disable_upstart] ? disable_upstart + packages : packages
69 +
        end
70 +
71 +
        def fedora_platform
72 +
          <<-CODE
73 +
            ENV container docker
74 +
            RUN dnf clean all
75 +
            RUN dnf install -y sudo openssh-server openssh-clients which curl
76 +
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
77 +
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
78 +
          CODE
79 +
        end
80 +
81 +
        def gentoo_platform
82 +
          <<-CODE
83 +
            RUN emerge --sync
84 +
            RUN emerge net-misc/openssh app-admin/sudo
85 +
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
86 +
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
87 +
          CODE
88 +
        end
89 +
90 +
        def gentoo_paludis_platform
91 +
          <<-CODE
92 +
            RUN cave sync
93 +
            RUN cave resolve -zx net-misc/openssh app-admin/sudo
94 +
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
95 +
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
96 +
          CODE
97 +
        end
98 +
99 +
        def opensuse_platform
100 +
          <<-CODE
101 +
            ENV container docker
102 +
            RUN zypper install -y sudo openssh which curl
103 +
            RUN /usr/sbin/sshd-gen-keys-start
104 +
          CODE
105 +
        end
106 +
107 +
        def rhel_platform
108 +
          <<-CODE
109 +
            ENV container docker
110 +
            RUN yum clean all
111 +
            RUN yum install -y sudo openssh-server openssh-clients which curl
112 +
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
113 +
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
114 +
          CODE
115 +
        end
116 +
117 +
        def dockerfile_base_linux(username, homedir)
118 +
          <<-CODE
119 +
            RUN if ! getent passwd #{username}; then \
120 +
                  useradd -d #{homedir} -m -s /bin/bash -p '*' #{username}; \
121 +
                fi
122 +
            RUN echo "#{username} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/#{username}
123 +
            RUN echo "Defaults !requiretty" >> /etc/sudoers.d/#{username}
124 +
            RUN mkdir -p #{homedir}/.ssh
125 +
            RUN chown -R #{username} #{homedir}/.ssh
126 +
            RUN chmod 0700 #{homedir}/.ssh
127 +
            RUN touch #{homedir}/.ssh/authorized_keys
128 +
            RUN chown #{username} #{homedir}/.ssh/authorized_keys
129 +
            RUN chmod 0600 #{homedir}/.ssh/authorized_keys
130 +
            RUN mkdir -p /run/sshd
131 +
          CODE
132 +
        end
133 +
      end
134 +
    end
135 +
  end
136 +
end

@@ -0,0 +1,85 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'securerandom'
15 +
16 +
require_relative '../container'
17 +
18 +
module Kitchen
19 +
  module Docker
20 +
    class Container
21 +
      class Windows < Kitchen::Docker::Container
22 +
        def initialize(config)
23 +
          super
24 +
        end
25 +
26 +
        def create(state)
27 +
          super
28 +
29 +
          debug('Creating Windows container')
30 +
          state[:username] = @config[:username]
31 +
          state[:image_id] = build_image(state, dockerfile) unless state[:image_id]
32 +
          state[:container_id] = run_container(state) unless state[:container_id]
33 +
          state[:hostname] = hostname(state)
34 +
        end
35 +
36 +
        def execute(command)
37 +
          # Create temp script file and upload files to container
38 +
          debug('Executing command on Windows container')
39 +
          filename = "docker-#{::SecureRandom.uuid}.ps1"
40 +
          temp_file = ".\\.kitchen\\temp\\#{filename}"
41 +
          create_temp_file(temp_file, command)
42 +
43 +
          remote_path = @config[:temp_dir].tr('/', '\\')
44 +
          debug("Creating directory #{remote_path} on container")
45 +
          create_dir_on_container(@config, remote_path)
46 +
47 +
          debug("Uploading temp file #{temp_file} to #{remote_path} on container")
48 +
          upload(temp_file, remote_path)
49 +
50 +
          debug('Deleting temp file from local filesystem')
51 +
          ::File.delete(temp_file)
52 +
53 +
          # Replace any environment variables used in the path and execute script file
54 +
          debug("Executing temp script #{remote_path}\\#{filename} on container")
55 +
          remote_path = replace_env_variables(@config, remote_path)
56 +
          cmd = build_powershell_command("-File #{remote_path}\\#{filename}")
57 +
58 +
          container_exec(@config, cmd)
59 +
        rescue => e
60 +
          raise "Failed to execute command on Windows container. #{e}"
61 +
        end
62 +
63 +
        protected
64 +
65 +
        def dockerfile
66 +
          raise ActionFailed, "Unknown platform '#{@config[:platform]}'" unless @config[:platform] == 'windows'
67 +
          return dockerfile_template if @config[:dockerfile]
68 +
69 +
          from = "FROM #{@config[:image]}"
70 +
71 +
          custom = ''
72 +
          Array(@config[:provision_command]).each do |cmd|
73 +
            custom << "RUN #{cmd}\n"
74 +
          end
75 +
76 +
          output = [from, dockerfile_proxy_config, custom, ''].join("\n")
77 +
          debug('--- Start Dockerfile ---')
78 +
          debug(output.strip)
79 +
          debug('--- End Dockerfile ---')
80 +
          output
81 +
        end
82 +
      end
83 +
    end
84 +
  end
85 +
end
0 86
imilarity index 89%
1 87
ename from lib/kitchen/driver/docker_version.rb
2 88
ename to lib/kitchen/docker/docker_version.rb

@@ -0,0 +1,68 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'kitchen'
15 +
require 'kitchen/configurable'
16 +
require_relative 'cli_helper'
17 +
require_relative 'container_helper'
18 +
19 +
module Kitchen
20 +
  module Docker
21 +
    module Helpers
22 +
      module ImageHelper
23 +
        include Configurable
24 +
        include Kitchen::Docker::Helpers::CliHelper
25 +
        include Kitchen::Docker::Helpers::ContainerHelper
26 +
27 +
        def parse_image_id(output)
28 +
          output.each_line do |line|
29 +
            if line =~ /image id|build successful|successfully built/i
30 +
              return line.split(/\s+/).last
31 +
            end
32 +
          end
33 +
          raise ActionFailed, 'Could not parse Docker build output for image ID'
34 +
        end
35 +
36 +
        def remove_image(state)
37 +
          image_id = state[:image_id]
38 +
          docker_command("rmi #{image_id}")
39 +
        end
40 +
41 +
        def build_image(state, dockerfile)
42 +
          cmd = 'build'
43 +
          cmd << ' --no-cache' unless config[:use_cache]
44 +
          extra_build_options = config_to_options(config[:build_options])
45 +
          cmd << " #{extra_build_options}" unless extra_build_options.empty?
46 +
          dockerfile_contents = dockerfile
47 +
          build_context = config[:build_context] ? '.' : '-'
48 +
          file = Tempfile.new('Dockerfile-kitchen', Dir.pwd)
49 +
          output = begin
50 +
                     file.write(dockerfile)
51 +
                     file.close
52 +
                     docker_command("#{cmd} -f #{Shellwords.escape(dockerfile_path(file))} #{build_context}",
53 +
                                    input: dockerfile_contents)
54 +
                   ensure
55 +
                     file.close unless file.closed?
56 +
                     file.unlink
57 +
                   end
58 +
59 +
          parse_image_id(output)
60 +
        end
61 +
62 +
        def image_exists?(state)
63 +
          state[:image_id] && !!docker_command("inspect --type=image #{state[:image_id]}") rescue false
64 +
        end
65 +
      end
66 +
    end
67 +
  end
68 +
end

@@ -0,0 +1,136 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'base64'
15 +
require 'openssl'
16 +
require 'securerandom'
17 +
require 'shellwords'
18 +
19 +
require_relative '../container'
20 +
require_relative '../helpers/dockerfile_helper'
21 +
22 +
module Kitchen
23 +
  module Docker
24 +
    class Container
25 +
      class Linux < Kitchen::Docker::Container
26 +
        include Kitchen::Docker::Helpers::DockerfileHelper
27 +
28 +
        MUTEX_FOR_SSH_KEYS = Mutex.new
29 +
30 +
        def initialize(config)
31 +
          super
32 +
        end
33 +
34 +
        def create(state)
35 +
          super
36 +
37 +
          debug('Creating Linux container')
38 +
          generate_keys
39 +
40 +
          state[:ssh_key] = @config[:private_key]
41 +
          state[:image_id] = build_image(state, dockerfile) unless state[:image_id]
42 +
          state[:container_id] = run_container(state, 22) unless state[:container_id]
43 +
          state[:hostname] = hostname(state)
44 +
          state[:port] = container_ssh_port(state)
45 +
        end
46 +
47 +
        def execute(command)
48 +
          # Create temp script file and upload files to container
49 +
          debug("Executing command on Linux container (Platform: #{@config[:platform]})")
50 +
          filename = "docker-#{::SecureRandom.uuid}.sh"
51 +
          temp_file = "./.kitchen/temp/#{filename}"
52 +
          create_temp_file(temp_file, command)
53 +
54 +
          remote_path = @config[:temp_dir]
55 +
          debug("Creating directory #{remote_path} on container")
56 +
          create_dir_on_container(@config, remote_path)
57 +
58 +
          debug("Uploading temp file #{temp_file} to #{remote_path} on container")
59 +
          upload(temp_file, remote_path)
60 +
61 +
          debug('Deleting temp file from local filesystem')
62 +
          ::File.delete(temp_file)
63 +
64 +
          # Replace any environment variables used in the path and execute script file
65 +
          debug("Executing temp script #{remote_path}/#{filename} on container")
66 +
          remote_path = replace_env_variables(@config, remote_path)
67 +
68 +
          container_exec(@config, "/bin/bash #{remote_path}/#{filename}")
69 +
        rescue => e
70 +
          raise "Failed to execute command on Linux container. #{e}"
71 +
        end
72 +
73 +
        protected
74 +
75 +
        def generate_keys
76 +
          MUTEX_FOR_SSH_KEYS.synchronize do
77 +
            if !File.exist?(@config[:public_key]) || !File.exist?(@config[:private_key])
78 +
              private_key = OpenSSL::PKey::RSA.new(2048)
79 +
              blobbed_key = Base64.encode64(private_key.to_blob).gsub("\n", '')
80 +
              public_key = "ssh-rsa #{blobbed_key} kitchen_docker_key"
81 +
              File.open(@config[:private_key], 'w') do |file|
82 +
                file.write(private_key)
83 +
                file.chmod(0600)
84 +
              end
85 +
              File.open(@config[:public_key], 'w') do |file|
86 +
                file.write(public_key)
87 +
                file.chmod(0600)
88 +
              end
89 +
            end
90 +
          end
91 +
        end
92 +
93 +
        def parse_container_ssh_port(output)
94 +
          _host, port = output.split(':')
95 +
          port.to_i
96 +
        rescue => e
97 +
          raise ActionFailed, "Could not parse Docker port output for container SSH port. #{e}"
98 +
        end
99 +
100 +
        def container_ssh_port(state)
101 +
          return 22 if @config[:use_internal_docker_network]
102 +
103 +
          output = docker_command("port #{state[:container_id]} 22/tcp")
104 +
          parse_container_ssh_port(output)
105 +
        rescue => e
106 +
          raise ActionFailed, "Docker reports container has no ssh port mapped. #{e}"
107 +
        end
108 +
109 +
        def dockerfile
110 +
          return dockerfile_template if @config[:dockerfile]
111 +
112 +
          from = "FROM #{@config[:image]}"
113 +
          platform = dockerfile_platform
114 +
          username = @config[:username]
115 +
          public_key = IO.read(@config[:public_key]).strip
116 +
          homedir = username == 'root' ? '/root' : "/home/#{username}"
117 +
          base = dockerfile_base_linux(username, homedir)
118 +
119 +
          custom = ''
120 +
          Array(@config[:provision_command]).each do |cmd|
121 +
            custom << "RUN #{cmd}\n"
122 +
          end
123 +
124 +
          ssh_key = "RUN echo #{Shellwords.escape(public_key)} >> #{homedir}/.ssh/authorized_keys"
125 +
126 +
          # Empty string to ensure the file ends with a newline.
127 +
          output = [from, dockerfile_proxy_config, platform, base, custom, ssh_key, ''].join("\n")
128 +
          debug('--- Start Dockerfile ---')
129 +
          debug(output.strip)
130 +
          debug('--- End Dockerfile ---')
131 +
          output
132 +
        end
133 +
      end
134 +
    end
135 +
  end
136 +
end

@@ -0,0 +1,40 @@
Loading
1 +
#
2 +
# Licensed under the Apache License, Version 2.0 (the "License");
3 +
# you may not use this file except in compliance with the License.
4 +
# You may obtain a copy of the License at
5 +
#
6 +
#    http://www.apache.org/licenses/LICENSE-2.0
7 +
#
8 +
# Unless required by applicable law or agreed to in writing, software
9 +
# distributed under the License is distributed on an "AS IS" BASIS,
10 +
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 +
# See the License for the specific language governing permissions and
12 +
# limitations under the License.
13 +
14 +
require 'fileutils'
15 +
16 +
module Kitchen
17 +
  module Docker
18 +
    module Helpers
19 +
      module FileHelper
20 +
        def create_temp_file(file, contents)
21 +
          debug("[Docker] Creating temp file #{file}")
22 +
          debug('[Docker] --- Start Temp File Contents ---')
23 +
          debug(contents)
24 +
          debug('[Docker] --- End Temp File Contents ---')
25 +
26 +
          begin
27 +
            path = ::File.dirname(file)
28 +
            ::FileUtils.mkdir_p(path) unless ::Dir.exist?(path)
29 +
            file = ::File.open(file, 'w')
30 +
            file.write(contents)
31 +
          rescue IOError => e
32 +
            raise "Failed to write temp file. Error Details: #{e}"
33 +
          ensure
34 +
            file.close unless file.nil?
35 +
          end
36 +
        end
37 +
      end
38 +
    end
39 +
  end
40 +
end

@@ -1,4 +1,3 @@
Loading
1 -
# -*- encoding: utf-8 -*-
2 1
#
3 2
# Copyright (C) 2014, Sean Porter
4 3
#
@@ -17,15 +16,14 @@
Loading
17 16
require 'kitchen'
18 17
require 'json'
19 18
require 'securerandom'
20 -
require 'uri'
21 19
require 'net/ssh'
22 -
require 'tempfile'
23 -
require 'shellwords'
24 -
require 'base64'
25 20
26 21
require 'kitchen/driver/base'
27 22
28 -
require_relative './docker/erb'
23 +
require_relative '../docker/container/linux'
24 +
require_relative '../docker/container/windows'
25 +
require_relative '../docker/helpers/cli_helper'
26 +
require_relative '../docker/helpers/container_helper'
29 27
30 28
module Kitchen
31 29
  module Driver
@@ -33,48 +31,44 @@
Loading
33 31
    #
34 32
    # @author Sean Porter <portertech@gmail.com>
35 33
    class Docker < Kitchen::Driver::Base
34 +
      include Kitchen::Docker::Helpers::CliHelper
35 +
      include Kitchen::Docker::Helpers::ContainerHelper
36 36
      include ShellOut
37 37
38 38
      default_config :binary,        'docker'
39 -
      default_config :socket,        ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
40 -
      default_config :privileged,    false
39 +
      default_config :build_options, nil
41 40
      default_config :cap_add,       nil
42 41
      default_config :cap_drop,      nil
43 -
      default_config :security_opt,  nil
44 -
      default_config :use_cache,     true
42 +
      default_config :disable_upstart, true
43 +
      default_config :env_variables, nil
44 +
      default_config :isolation,     nil
45 +
      default_config :interactive,   false
46 +
      default_config :private_key,   File.join(Dir.pwd, '.kitchen', 'docker_id_rsa')
47 +
      default_config :privileged,    false
48 +
      default_config :public_key,    File.join(Dir.pwd, '.kitchen', 'docker_id_rsa.pub')
49 +
      default_config :publish_all,   false
45 50
      default_config :remove_images, false
46 -
      default_config :run_command,   '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes ' +
47 -
                                     '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
48 -
      default_config :username,      'kitchen'
51 +
      default_config :run_options,   nil
52 +
      default_config :security_opt,  nil
49 53
      default_config :tls,           false
50 -
      default_config :tls_verify,    false
51 54
      default_config :tls_cacert,    nil
52 55
      default_config :tls_cert,      nil
53 56
      default_config :tls_key,       nil
54 -
      default_config :publish_all,   false
55 -
      default_config :wait_for_sshd, true
56 -
      default_config :private_key,   File.join(Dir.pwd, '.kitchen', 'docker_id_rsa')
57 -
      default_config :public_key,    File.join(Dir.pwd, '.kitchen', 'docker_id_rsa.pub')
58 -
      default_config :build_options, nil
59 -
      default_config :run_options,   nil
57 +
      default_config :tls_verify,    false
58 +
      default_config :tty,           false
59 +
      default_config :use_cache,     true
60 60
      default_config :use_internal_docker_network, false
61 -
62 61
      default_config :use_sudo, false
63 -
64 -
      default_config :image do |driver|
65 -
        driver.default_image
66 -
      end
67 -
68 -
      default_config :platform do |driver|
69 -
        driver.default_platform
70 -
      end
71 -
72 -
      default_config :disable_upstart, true
62 +
      default_config :wait_for_transport, true
73 63
74 64
      default_config :build_context do |driver|
75 65
        !driver.remote_socket?
76 66
      end
77 67
68 +
      default_config :image do |driver|
69 +
        driver.default_image
70 +
      end
71 +
78 72
      default_config :instance_name do |driver|
79 73
        # Borrowed from kitchen-rackspace
80 74
        [
@@ -85,365 +79,87 @@
Loading
85 79
        ].join('-')
86 80
      end
87 81
88 -
      MUTEX_FOR_SSH_KEYS = Mutex.new
89 -
90 -
      def verify_dependencies
91 -
        run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
92 -
        rescue
93 -
          raise UserError,
94 -
          'You must first install the Docker CLI tool http://www.docker.io/gettingstarted/'
95 -
      end
96 -
97 -
      def dev_null
98 -
        case RbConfig::CONFIG["host_os"]
99 -
        when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
100 -
          "NUL"
101 -
        else
102 -
          "/dev/null"
103 -
        end
104 -
      end
105 -
106 -
      def default_image
107 -
        platform, release = instance.platform.name.split('-')
108 -
        if platform == 'centos' && release
109 -
          release = 'centos' + release.split('.').first
110 -
        end
111 -
        release ? [platform, release].join(':') : platform
112 -
      end
113 -
114 -
      def default_platform
115 -
        instance.platform.name.split('-').first
116 -
      end
117 -
118 -
      def create(state)
119 -
        generate_keys
120 -
        state[:username] = config[:username]
121 -
        state[:ssh_key] = config[:private_key]
122 -
        state[:image_id] = build_image(state) unless state[:image_id]
123 -
        state[:container_id] = run_container(state) unless state[:container_id]
124 -
        state[:hostname] = 'localhost'
125 -
        if remote_socket?
126 -
          state[:hostname] = socket_uri.host
127 -
        elsif config[:use_internal_docker_network]
128 -
          state[:hostname] = container_ip(state)
129 -
        end
130 -
        state[:port] = container_ssh_port(state)
131 -
        if config[:wait_for_sshd]
132 -
          instance.transport.connection(state) do |conn|
133 -
            conn.wait_until_ready
134 -
          end
135 -
        end
136 -
      end
137 -
138 -
      def destroy(state)
139 -
        rm_container(state) if container_exists?(state)
140 -
        if config[:remove_images] && state[:image_id]
141 -
          rm_image(state) if image_exists?(state)
142 -
        end
143 -
      end
144 -
145 -
      def remote_socket?
146 -
        config[:socket] ? socket_uri.scheme == 'tcp' : false
147 -
      end
148 -
149 -
      protected
150 -
151 -
      def socket_uri
152 -
        URI.parse(config[:socket])
153 -
      end
154 -
155 -
      def docker_command(cmd, options={})
156 -
        docker = config[:binary].dup
157 -
        docker << " -H #{config[:socket]}" if config[:socket]
158 -
        docker << " --tls" if config[:tls]
159 -
        docker << " --tlsverify" if config[:tls_verify]
160 -
        docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
161 -
        docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
162 -
        docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
163 -
        run_command("#{docker} #{cmd}", options.merge({
164 -
          quiet: !logger.debug?,
165 -
          use_sudo: config[:use_sudo],
166 -
          log_subject: Thor::Util.snake_case(self.class.to_s),
167 -
        }))
82 +
      default_config :platform do |driver|
83 +
        driver.default_platform
168 84
      end
169 85
170 -
      def generate_keys
171 -
        MUTEX_FOR_SSH_KEYS.synchronize do
172 -
          if !File.exist?(config[:public_key]) || !File.exist?(config[:private_key])
173 -
            private_key = OpenSSL::PKey::RSA.new(2048)
174 -
            blobbed_key = Base64.encode64(private_key.to_blob).gsub("\n", '')
175 -
            public_key = "ssh-rsa #{blobbed_key} kitchen_docker_key"
176 -
            File.open(config[:private_key], 'w') do |file|
177 -
              file.write(private_key)
178 -
              file.chmod(0600)
179 -
            end
180 -
            File.open(config[:public_key], 'w') do |file|
181 -
              file.write(public_key)
182 -
              file.chmod(0600)
183 -
            end
86 +
      default_config :run_command do |driver|
87 +
        if driver.windows_os?
88 +
          # Launch arbitrary process to keep the Windows container alive
89 +
          # If running in interactive mode, launch powershell.exe instead
90 +
          if driver[:interactive]
91 +
            'powershell.exe'
92 +
          else
93 +
            'ping -t localhost'
184 94
          end
185 -
        end
186 -
      end
187 -
188 -
      def build_dockerfile
189 -
        from = "FROM #{config[:image]}"
190 -
191 -
        env_variables = ''
192 -
        if config[:http_proxy]
193 -
          env_variables << "ENV http_proxy #{config[:http_proxy]}\n"
194 -
          env_variables << "ENV HTTP_PROXY #{config[:http_proxy]}\n"
195 -
        end
196 -
197 -
        if config[:https_proxy]
198 -
          env_variables << "ENV https_proxy #{config[:https_proxy]}\n"
199 -
          env_variables << "ENV HTTPS_PROXY #{config[:https_proxy]}\n"
200 -
        end
201 -
202 -
        if config[:no_proxy]
203 -
          env_variables << "ENV no_proxy #{config[:no_proxy]}\n"
204 -
          env_variables << "ENV NO_PROXY #{config[:no_proxy]}\n"
205 -
        end
206 -
207 -
        platform = case config[:platform]
208 -
        when 'debian', 'ubuntu'
209 -
          disable_upstart = <<-eos
210 -
            RUN [ ! -f "/sbin/initctl" ] || dpkg-divert --local --rename --add /sbin/initctl && ln -sf /bin/true /sbin/initctl
211 -
          eos
212 -
          packages = <<-eos
213 -
            ENV DEBIAN_FRONTEND noninteractive
214 -
            ENV container docker
215 -
            RUN apt-get update
216 -
            RUN apt-get install -y sudo openssh-server curl lsb-release
217 -
          eos
218 -
          config[:disable_upstart] ? disable_upstart + packages : packages
219 -
        when 'rhel', 'centos', 'fedora', 'oraclelinux'
220 -
          <<-eos
221 -
            ENV container docker
222 -
            RUN yum clean all
223 -
            RUN yum install -y sudo openssh-server openssh-clients which curl
224 -
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
225 -
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
226 -
          eos
227 -
        when 'opensuse', 'sles'
228 -
          <<-eos
229 -
            ENV container docker
230 -
            RUN zypper install -y sudo openssh which curl
231 -
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
232 -
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
233 -
          eos
234 -
        when 'arch'
235 -
          # See https://bugs.archlinux.org/task/47052 for why we
236 -
          # blank out limits.conf.
237 -
          <<-eos
238 -
            RUN pacman --noconfirm -Sy archlinux-keyring
239 -
            RUN pacman-db-upgrade
240 -
            RUN pacman --noconfirm -Syu openssl openssh sudo curl
241 -
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
242 -
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
243 -
            RUN echo >/etc/security/limits.conf
244 -
          eos
245 -
        when 'gentoo'
246 -
          <<-eos
247 -
            RUN emerge --sync
248 -
            RUN emerge net-misc/openssh app-admin/sudo
249 -
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
250 -
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
251 -
          eos
252 -
        when 'gentoo-paludis'
253 -
          <<-eos
254 -
            RUN cave sync
255 -
            RUN cave resolve -zx net-misc/openssh app-admin/sudo
256 -
            RUN [ -f "/etc/ssh/ssh_host_rsa_key" ] || ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
257 -
            RUN [ -f "/etc/ssh/ssh_host_dsa_key" ] || ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
258 -
          eos
259 -
        else
260 -
          raise ActionFailed,
261 -
          "Unknown platform '#{config[:platform]}'"
262 -
        end
263 -
264 -
        username = config[:username]
265 -
        public_key = IO.read(config[:public_key]).strip
266 -
        homedir = username == 'root' ? '/root' : "/home/#{username}"
267 -
268 -
        base = <<-eos
269 -
          RUN if ! getent passwd #{username}; then \
270 -
                useradd -d #{homedir} -m -s /bin/bash -p '*' #{username}; \
271 -
              fi
272 -
          RUN echo "#{username} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/#{username}
273 -
          RUN echo "Defaults !requiretty" >> /etc/sudoers.d/#{username}
274 -
          RUN mkdir -p #{homedir}/.ssh
275 -
          RUN chown -R #{username} #{homedir}/.ssh
276 -
          RUN chmod 0700 #{homedir}/.ssh
277 -
          RUN touch #{homedir}/.ssh/authorized_keys
278 -
          RUN chown #{username} #{homedir}/.ssh/authorized_keys
279 -
          RUN chmod 0600 #{homedir}/.ssh/authorized_keys
280 -
          RUN mkdir -p /run/sshd
281 -
        eos
282 -
        custom = ''
283 -
        Array(config[:provision_command]).each do |cmd|
284 -
          custom << "RUN #{cmd}\n"
285 -
        end
286 -
        ssh_key = "RUN echo #{Shellwords.escape(public_key)} >> #{homedir}/.ssh/authorized_keys"
287 -
        # Empty string to ensure the file ends with a newline.
288 -
        [from, env_variables, platform, base, custom, ssh_key, ''].join("\n")
289 -
      end
290 -
291 -
      def dockerfile
292 -
        if config[:dockerfile]
293 -
          template = IO.read(File.expand_path(config[:dockerfile]))
294 -
          context = DockerERBContext.new(config.to_hash)
295 -
          ERB.new(template).result(context.get_binding)
296 95
        else
297 -
          build_dockerfile
298 -
        end
299 -
      end
300 -
301 -
      def parse_image_id(output)
302 -
        output.each_line do |line|
303 -
          if line =~ /image id|build successful|successfully built/i
304 -
            return line.split(/\s+/).last
305 -
          end
96 +
          '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes '\
97 +
          '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
306 98
        end
307 -
        raise ActionFailed,
308 -
          'Could not parse Docker build output for image ID'
309 99
      end
310 100
311 -
      def build_image(state)
312 -
        cmd = "build"
313 -
        cmd << " --no-cache" unless config[:use_cache]
314 -
        extra_build_options = config_to_options(config[:build_options])
315 -
        cmd << " #{extra_build_options}" unless extra_build_options.empty?
316 -
        dockerfile_contents = dockerfile
317 -
        build_context = config[:build_context] ? '.' : '-'
318 -
        file = Tempfile.new('Dockerfile-kitchen', Dir.pwd)
319 -
        output = begin
320 -
          file.write(dockerfile)
321 -
          file.close
322 -
          docker_command("#{cmd} -f #{Shellwords.escape(dockerfile_path(file))} #{build_context}", :input => dockerfile_contents)
323 -
        ensure
324 -
          file.close unless file.closed?
325 -
          file.unlink
326 -
        end
327 -
        parse_image_id(output)
101 +
      default_config :socket do |driver|
102 +
        socket = 'unix:///var/run/docker.sock'
103 +
        socket = 'npipe:////./pipe/docker_engine' if driver.windows_os?
104 +
        ENV['DOCKER_HOST'] || socket
328 105
      end
329 106
330 -
      def parse_container_id(output)
331 -
        container_id = output.chomp
332 -
        unless [12, 64].include?(container_id.size)
333 -
          raise ActionFailed,
334 -
          'Could not parse Docker run output for container ID'
107 +
      default_config :username do |driver|
108 +
        # Return nil to prevent username from being added to Docker
109 +
        # command line args for Windows if a username was not specified
110 +
        if driver.windows_os?
111 +
          nil
112 +
        else
113 +
          'kitchen'
335 114
        end
336 -
        container_id
337 115
      end
338 116
339 -
      def build_run_command(image_id)
340 -
        cmd = "run -d -p 22"
341 -
        Array(config[:forward]).each {|port| cmd << " -p #{port}"}
342 -
        Array(config[:dns]).each {|dns| cmd << " --dns #{dns}"}
343 -
        Array(config[:add_host]).each {|host, ip| cmd << " --add-host=#{host}:#{ip}"}
344 -
        Array(config[:volume]).each {|volume| cmd << " -v #{volume}"}
345 -
        Array(config[:volumes_from]).each {|container| cmd << " --volumes-from #{container}"}
346 -
        Array(config[:links]).each {|link| cmd << " --link #{link}"}
347 -
        Array(config[:devices]).each {|device| cmd << " --device #{device}"}
348 -
        cmd << " --name #{config[:instance_name]}" if config[:instance_name]
349 -
        cmd << " -P" if config[:publish_all]
350 -
        cmd << " -h #{config[:hostname]}" if config[:hostname]
351 -
        cmd << " -m #{config[:memory]}" if config[:memory]
352 -
        cmd << " -c #{config[:cpu]}" if config[:cpu]
353 -
        cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
354 -
        cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
355 -
        cmd << " --privileged" if config[:privileged]
356 -
        Array(config[:cap_add]).each {|cap| cmd << " --cap-add=#{cap}"} if config[:cap_add]
357 -
        Array(config[:cap_drop]).each {|cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
358 -
        Array(config[:security_opt]).each {|opt| cmd << " --security-opt=#{opt}"} if config[:security_opt]
359 -
        extra_run_options = config_to_options(config[:run_options])
360 -
        cmd << " #{extra_run_options}" unless extra_run_options.empty?
361 -
        cmd << " #{image_id} #{config[:run_command]}"
362 -
        cmd
363 -
      end
364 -
365 -
      def run_container(state)
366 -
        cmd = build_run_command(state[:image_id])
367 -
        output = docker_command(cmd)
368 -
        parse_container_id(output)
117 +
      def verify_dependencies
118 +
        run_command("#{config[:binary]} >> #{dev_null} 2>&1", quiet: true, use_sudo: config[:use_sudo])
119 +
      rescue
120 +
        raise UserError, 'You must first install the Docker CLI tool https://www.docker.com/get-started'
369 121
      end
370 122
371 -
      def container_exists?(state)
372 -
        state[:container_id] && !!docker_command("top #{state[:container_id]}") rescue false
373 -
      end
123 +
      def create(state)
124 +
        container.create(state)
374 125
375 -
      def image_exists?(state)
376 -
        state[:image_id] && !!docker_command("docker inspect --type=image #{state[:image_id]}") rescue false
126 +
        wait_for_transport(state)
377 127
      end
378 128
379 -
      def parse_container_ssh_port(output)
380 -
        begin
381 -
          _host, port = output.split(':')
382 -
          port.to_i
383 -
        rescue
384 -
          raise ActionFailed,
385 -
            'Could not parse Docker port output for container SSH port'
386 -
        end
129 +
      def destroy(state)
130 +
        container.destroy(state)
387 131
      end
388 132
389 -
      def container_ssh_port(state)
390 -
        begin
391 -
          if config[:use_internal_docker_network]
392 -
            return 22
133 +
      def wait_for_transport(state)
134 +
        if config[:wait_for_transport]
135 +
          instance.transport.connection(state) do |conn|
136 +
            conn.wait_until_ready
393 137
          end
394 -
          output = docker_command("port #{state[:container_id]} 22/tcp")
395 -
          parse_container_ssh_port(output)
396 -
        rescue
397 -
          raise ActionFailed,
398 -
          'Docker reports container has no ssh port mapped'
399 138
        end
400 139
      end
401 140
402 -
      def container_ip(state)
403 -
        begin
404 -
          cmd = "inspect --format '{{ .NetworkSettings.IPAddress }}'"
405 -
          cmd << " #{state[:container_id]}"
406 -
          docker_command(cmd).strip
407 -
        rescue
408 -
          raise ActionFailed,
409 -
          'Error getting internal IP of Docker container'
141 +
      def default_image
142 +
        platform, release = instance.platform.name.split('-')
143 +
        if platform == 'centos' && release
144 +
          release = 'centos' + release.split('.').first
410 145
        end
146 +
        release ? [platform, release].join(':') : platform
411 147
      end
412 148
413 -
      def rm_container(state)
414 -
        container_id = state[:container_id]
415 -
        docker_command("stop -t 0 #{container_id}")
416 -
        docker_command("rm #{container_id}")
417 -
      end
418 -
419 -
      def rm_image(state)
420 -
        image_id = state[:image_id]
421 -
        docker_command("rmi #{image_id}")
149 +
      def default_platform
150 +
        instance.platform.name.split('-').first
422 151
      end
423 152
424 -
      # Convert the config input for `:build_options` or `:run_options` in to a
425 -
      # command line string for use with Docker.
426 -
      #
427 -
      # @since 2.5.0
428 -
      # @param config [nil, String, Array, Hash] Config data to convert.
429 -
      # @return [String]
430 -
      def config_to_options(config)
431 -
        case config
432 -
        when nil
433 -
          ''
434 -
        when String
435 -
          config
436 -
        when Array
437 -
          config.map {|c| config_to_options(c) }.join(' ')
438 -
        when Hash
439 -
          config.map {|k, v| Array(v).map {|c| "--#{k}=#{Shellwords.escape(c)}" }.join(' ') }.join(' ')
440 -
        end
441 -
      end
153 +
      protected
442 154
443 -
      def dockerfile_path(file)
444 -
        config[:build_context] ? Pathname.new(file.path).relative_path_from(Pathname.pwd).to_s : file.path
155 +
      def container
156 +
        @container ||= if windows_os?
157 +
                         Kitchen::Docker::Container::Windows.new(config)
158 +
                       else
159 +
                         Kitchen::Docker::Container::Linux.new(config)
160 +
                       end
161 +
        @container
445 162
      end
446 -
447 163
    end
448 164
  end
449 165
end
Files Coverage
lib/kitchen 36.76%
Project Totals (10 files) 36.76%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading