kitchen-docker 2.8.0 → 2.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
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
+ begin
15
+ require 'docker'
16
+
17
+ # Override API_VERSION constant in docker-api gem to use version 1.24 of the Docker API
18
+ # This override is for the docker-api gem to communicate to the Docker engine on Windows
19
+ module Docker
20
+ VERSION = '0.0.0'
21
+ API_VERSION = '1.24'
22
+ end
23
+ rescue LoadError => e
24
+ logger.debug("[Docker] docker-api gem not found for InSpec verifier. #{e}")
25
+ end
@@ -0,0 +1,136 @@
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,85 @@
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,0 +1,75 @@
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
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Copyright (C) 2014, Sean Porter
4
3
  #
@@ -15,10 +14,8 @@
15
14
  # limitations under the License.
16
15
 
17
16
  module Kitchen
18
-
19
- module Driver
20
-
17
+ module Docker
21
18
  # Version string for Docker Kitchen driver
22
- DOCKER_VERSION = "2.8.0"
19
+ DOCKER_VERSION = "2.12.0"
23
20
  end
24
21
  end
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Copyright (C) 2014, Sean Porter
4
3
  #
@@ -17,10 +16,8 @@
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,172 @@
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
+ # Copied from kitchen because we need stderr
40
+ def run_command(cmd, options = {})
41
+ if options.fetch(:use_sudo, false)
42
+ cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
43
+ end
44
+ subject = "[#{options.fetch(:log_subject, "local")} command]"
45
+
46
+ debug("#{subject} BEGIN (#{cmd})")
47
+ sh = Mixlib::ShellOut.new(cmd, shell_opts(options))
48
+ sh.run_command
49
+ debug("#{subject} END #{Util.duration(sh.execution_time)}")
50
+ sh.error!
51
+ sh.stdout + sh.stderr
52
+ rescue Mixlib::ShellOut::ShellCommandFailed => ex
53
+ raise ShellCommandFailed, ex.message
54
+ rescue Exception => error # rubocop:disable Lint/RescueException
55
+ error.extend(Kitchen::Error)
56
+ raise
57
+ end
58
+
59
+ def build_run_command(image_id, transport_port = nil)
60
+ cmd = 'run -d'
61
+ cmd << ' -i' if config[:interactive]
62
+ cmd << ' -t' if config[:tty]
63
+ cmd << build_env_variable_args(config[:env_variables]) if config[:env_variables]
64
+ cmd << " -p #{transport_port}" unless transport_port.nil?
65
+ Array(config[:forward]).each { |port| cmd << " -p #{port}" }
66
+ Array(config[:dns]).each { |dns| cmd << " --dns #{dns}" }
67
+ Array(config[:add_host]).each { |host, ip| cmd << " --add-host=#{host}:#{ip}" }
68
+ Array(config[:volume]).each { |volume| cmd << " -v #{volume}" }
69
+ Array(config[:volumes_from]).each { |container| cmd << " --volumes-from #{container}" }
70
+ Array(config[:links]).each { |link| cmd << " --link #{link}" }
71
+ Array(config[:devices]).each { |device| cmd << " --device #{device}" }
72
+ Array(config[:mount]).each {|mount| cmd << " --mount #{mount}"}
73
+ Array(config[:tmpfs]).each {|tmpfs| cmd << " --tmpfs #{tmpfs}"}
74
+ cmd << " --name #{config[:instance_name]}" if config[:instance_name]
75
+ cmd << ' -P' if config[:publish_all]
76
+ cmd << " -h #{config[:hostname]}" if config[:hostname]
77
+ cmd << " -m #{config[:memory]}" if config[:memory]
78
+ cmd << " -c #{config[:cpu]}" if config[:cpu]
79
+ cmd << " --gpus #{config[:gpus]}" if config[:gpus]
80
+ cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
81
+ cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
82
+ cmd << ' --privileged' if config[:privileged]
83
+ cmd << " --isolation #{config[:isolation]}" if config[:isolation]
84
+ Array(config[:cap_add]).each { |cap| cmd << " --cap-add=#{cap}"} if config[:cap_add]
85
+ Array(config[:cap_drop]).each { |cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
86
+ Array(config[:security_opt]).each { |opt| cmd << " --security-opt=#{opt}"} if config[:security_opt]
87
+ cmd << " --platform=#{config[:docker_platform]}" if config[:docker_platform]
88
+ extra_run_options = config_to_options(config[:run_options])
89
+ cmd << " #{extra_run_options}" unless extra_run_options.empty?
90
+ cmd << " #{image_id} #{config[:run_command]}"
91
+ logger.debug("build_run_command: #{cmd}")
92
+ cmd
93
+ end
94
+
95
+ def build_exec_command(state, command)
96
+ cmd = 'exec'
97
+ cmd << ' -d' if config[:detach]
98
+ cmd << build_env_variable_args(config[:env_variables]) if config[:env_variables]
99
+ cmd << ' --privileged' if config[:privileged]
100
+ cmd << ' -t' if config[:tty]
101
+ cmd << ' -i' if config[:interactive]
102
+ cmd << " -u #{config[:username]}" if config[:username]
103
+ cmd << " -w #{config[:working_dir]}" if config[:working_dir]
104
+ cmd << " #{state[:container_id]}"
105
+ cmd << " #{command}"
106
+ logger.debug("build_exec_command: #{cmd}")
107
+ cmd
108
+ end
109
+
110
+ def build_copy_command(local_file, remote_file, opts = {})
111
+ cmd = 'cp'
112
+ cmd << ' -a' if opts[:archive]
113
+ cmd << " #{local_file} #{remote_file}"
114
+ cmd
115
+ end
116
+
117
+ def build_powershell_command(args)
118
+ cmd = 'powershell -ExecutionPolicy Bypass -NoLogo '
119
+ cmd << args
120
+ logger.debug("build_powershell_command: #{cmd}")
121
+ cmd
122
+ end
123
+
124
+ def build_env_variable_args(vars)
125
+ raise ActionFailed, 'Environment variables are not of a Hash type' unless vars.is_a?(Hash)
126
+
127
+ args = ''
128
+ vars.each do |k, v|
129
+ args << " -e #{k.to_s.strip}=\"#{v.to_s.strip}\""
130
+ end
131
+
132
+ args
133
+ end
134
+
135
+ def dev_null
136
+ case RbConfig::CONFIG['host_os']
137
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
138
+ 'NUL'
139
+ else
140
+ '/dev/null'
141
+ end
142
+ end
143
+
144
+ def docker_shell_opts(options = {})
145
+ options[:live_stream] = nil if options[:suppress_output]
146
+ options.delete(:suppress_output)
147
+
148
+ options
149
+ end
150
+
151
+ # Convert the config input for `:build_options` or `:run_options` in to a
152
+ # command line string for use with Docker.
153
+ #
154
+ # @since 2.5.0
155
+ # @param config [nil, String, Array, Hash] Config data to convert.
156
+ # @return [String]
157
+ def config_to_options(config)
158
+ case config
159
+ when nil
160
+ ''
161
+ when String
162
+ config
163
+ when Array
164
+ config.map { |c| config_to_options(c) }.join(' ')
165
+ when Hash
166
+ config.map { |k, v| Array(v).map { |c| "--#{k}=#{Shellwords.escape(c)}" }.join(' ') }.join(' ')
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end