kitchen-docker 2.6.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Copyright (C) 2014, Sean Porter
4
3
  #
@@ -17,14 +16,14 @@
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
20
 
25
21
  require 'kitchen/driver/base'
26
22
 
27
- 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'
28
27
 
29
28
  module Kitchen
30
29
  module Driver
@@ -32,32 +31,37 @@ module Kitchen
32
31
  #
33
32
  # @author Sean Porter <portertech@gmail.com>
34
33
  class Docker < Kitchen::Driver::Base
34
+ include Kitchen::Docker::Helpers::CliHelper
35
+ include Kitchen::Docker::Helpers::ContainerHelper
35
36
  include ShellOut
36
37
 
37
38
  default_config :binary, 'docker'
38
- default_config :socket, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
39
- default_config :privileged, false
39
+ default_config :build_options, nil
40
40
  default_config :cap_add, nil
41
41
  default_config :cap_drop, nil
42
- default_config :security_opt, nil
43
- 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
44
50
  default_config :remove_images, false
45
- default_config :run_command, '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes ' +
46
- '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
47
- default_config :username, 'kitchen'
51
+ default_config :run_options, nil
52
+ default_config :security_opt, nil
48
53
  default_config :tls, false
49
- default_config :tls_verify, false
50
54
  default_config :tls_cacert, nil
51
55
  default_config :tls_cert, nil
52
56
  default_config :tls_key, nil
53
- default_config :publish_all, false
54
- default_config :wait_for_sshd, true
55
- default_config :private_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa')
56
- default_config :public_key, File.join(Dir.pwd, '.kitchen', 'docker_id_rsa.pub')
57
- default_config :build_options, nil
58
- default_config :run_options, nil
57
+ default_config :tls_verify, false
58
+ default_config :tty, false
59
+ default_config :use_cache, true
60
+ default_config :use_internal_docker_network, false
61
+ default_config :use_sudo, false
62
+ default_config :wait_for_transport, true
59
63
 
60
- default_config :use_sudo do |driver|
64
+ default_config :build_context do |driver|
61
65
  !driver.remote_socket?
62
66
  end
63
67
 
@@ -65,16 +69,6 @@ module Kitchen
65
69
  driver.default_image
66
70
  end
67
71
 
68
- default_config :platform do |driver|
69
- driver.default_platform
70
- end
71
-
72
- default_config :disable_upstart, true
73
-
74
- default_config :build_context do |driver|
75
- !driver.remote_socket?
76
- end
77
-
78
72
  default_config :instance_name do |driver|
79
73
  # Borrowed from kitchen-rackspace
80
74
  [
@@ -82,334 +76,90 @@ module Kitchen
82
76
  (Etc.getlogin || 'nologin').gsub(/\W/, ''),
83
77
  Socket.gethostname.gsub(/\W/, '')[0..20],
84
78
  Array.new(8) { rand(36).to_s(36) }.join
85
- ].join('-')
86
- end
87
-
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] = remote_socket? ? socket_uri.host : 'localhost'
125
- state[:port] = container_ssh_port(state)
126
- if config[:wait_for_sshd]
127
- instance.transport.connection(state) do |conn|
128
- conn.wait_until_ready
129
- end
130
- end
131
- end
132
-
133
- def destroy(state)
134
- rm_container(state) if container_exists?(state)
135
- if config[:remove_images] && state[:image_id]
136
- rm_image(state)
137
- end
79
+ ].join('-').downcase
138
80
  end
139
81
 
140
- def remote_socket?
141
- config[:socket] ? socket_uri.scheme == 'tcp' : false
142
- end
143
-
144
- protected
145
-
146
- def socket_uri
147
- URI.parse(config[:socket])
148
- end
149
-
150
- def docker_command(cmd, options={})
151
- docker = config[:binary].dup
152
- docker << " -H #{config[:socket]}" if config[:socket]
153
- docker << " --tls" if config[:tls]
154
- docker << " --tlsverify" if config[:tls_verify]
155
- docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
156
- docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
157
- docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
158
- run_command("#{docker} #{cmd}", options.merge({
159
- quiet: !logger.debug?,
160
- use_sudo: config[:use_sudo],
161
- log_subject: Thor::Util.snake_case(self.class.to_s),
162
- }))
82
+ default_config :platform do |driver|
83
+ driver.default_platform
163
84
  end
164
85
 
165
- def generate_keys
166
- MUTEX_FOR_SSH_KEYS.synchronize do
167
- if !File.exist?(config[:public_key]) || !File.exist?(config[:private_key])
168
- private_key = OpenSSL::PKey::RSA.new(2048)
169
- blobbed_key = Base64.encode64(private_key.to_blob).gsub("\n", '')
170
- public_key = "ssh-rsa #{blobbed_key} kitchen_docker_key"
171
- File.open(config[:private_key], 'w') do |file|
172
- file.write(private_key)
173
- file.chmod(0600)
174
- end
175
- File.open(config[:public_key], 'w') do |file|
176
- file.write(public_key)
177
- file.chmod(0600)
178
- 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'
179
94
  end
180
- end
181
- end
182
-
183
- def build_dockerfile
184
- from = "FROM #{config[:image]}"
185
-
186
- env_variables = ''
187
- if config[:http_proxy]
188
- env_variables << "ENV http_proxy #{config[:http_proxy]}\n"
189
- env_variables << "ENV HTTP_PROXY #{config[:http_proxy]}\n"
190
- end
191
-
192
- if config[:https_proxy]
193
- env_variables << "ENV https_proxy #{config[:https_proxy]}\n"
194
- env_variables << "ENV HTTPS_PROXY #{config[:https_proxy]}\n"
195
- end
196
-
197
- if config[:no_proxy]
198
- env_variables << "ENV no_proxy #{config[:no_proxy]}\n"
199
- env_variables << "ENV NO_PROXY #{config[:no_proxy]}\n"
200
- end
201
-
202
- platform = case config[:platform]
203
- when 'debian', 'ubuntu'
204
- disable_upstart = <<-eos
205
- RUN dpkg-divert --local --rename --add /sbin/initctl
206
- RUN ln -sf /bin/true /sbin/initctl
207
- eos
208
- packages = <<-eos
209
- ENV DEBIAN_FRONTEND noninteractive
210
- ENV container docker
211
- RUN apt-get update
212
- RUN apt-get install -y sudo openssh-server curl lsb-release
213
- eos
214
- config[:disable_upstart] ? disable_upstart + packages : packages
215
- when 'rhel', 'centos', 'fedora'
216
- <<-eos
217
- ENV container docker
218
- RUN yum clean all
219
- RUN yum install -y sudo openssh-server openssh-clients which curl
220
- RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
221
- RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
222
- eos
223
- when 'arch'
224
- # See https://bugs.archlinux.org/task/47052 for why we
225
- # blank out limits.conf.
226
- <<-eos
227
- RUN pacman --noconfirm -Sy archlinux-keyring
228
- RUN pacman-db-upgrade
229
- RUN pacman --noconfirm -Sy openssl openssh sudo curl
230
- RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
231
- RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
232
- RUN echo >/etc/security/limits.conf
233
- eos
234
- when 'gentoo'
235
- <<-eos
236
- RUN emerge sync
237
- RUN emerge net-misc/openssh app-admin/sudo
238
- RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
239
- RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
240
- eos
241
- when 'gentoo-paludis'
242
- <<-eos
243
- RUN cave sync
244
- RUN cave resolve -zx net-misc/openssh app-admin/sudo
245
- RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
246
- RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
247
- eos
248
95
  else
249
- raise ActionFailed,
250
- "Unknown platform '#{config[:platform]}'"
251
- end
252
-
253
- username = config[:username]
254
- public_key = IO.read(config[:public_key]).strip
255
- homedir = username == 'root' ? '/root' : "/home/#{username}"
256
-
257
- base = <<-eos
258
- RUN if ! getent passwd #{username}; then \
259
- useradd -d #{homedir} -m -s /bin/bash -p '*' #{username}; \
260
- fi
261
- RUN echo "#{username} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
262
- RUN echo "Defaults !requiretty" >> /etc/sudoers
263
- RUN mkdir -p #{homedir}/.ssh
264
- RUN chown -R #{username} #{homedir}/.ssh
265
- RUN chmod 0700 #{homedir}/.ssh
266
- RUN touch #{homedir}/.ssh/authorized_keys
267
- RUN chown #{username} #{homedir}/.ssh/authorized_keys
268
- RUN chmod 0600 #{homedir}/.ssh/authorized_keys
269
- eos
270
- custom = ''
271
- Array(config[:provision_command]).each do |cmd|
272
- custom << "RUN #{cmd}\n"
96
+ '/usr/sbin/sshd -D -o UseDNS=no -o UsePAM=no -o PasswordAuthentication=yes '\
97
+ '-o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid'
273
98
  end
274
- ssh_key = "RUN echo #{Shellwords.escape(public_key)} >> #{homedir}/.ssh/authorized_keys"
275
- # Empty string to ensure the file ends with a newline.
276
- [from, env_variables, platform, base, custom, ssh_key, ''].join("\n")
277
99
  end
278
100
 
279
- def dockerfile
280
- if config[:dockerfile]
281
- template = IO.read(File.expand_path(config[:dockerfile]))
282
- context = DockerERBContext.new(config.to_hash)
283
- ERB.new(template).result(context.get_binding)
284
- else
285
- build_dockerfile
286
- end
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
287
105
  end
288
106
 
289
- def parse_image_id(output)
290
- output.each_line do |line|
291
- if line =~ /image id|build successful|successfully built/i
292
- return line.split(/\s+/).last
293
- end
294
- end
295
- raise ActionFailed,
296
- 'Could not parse Docker build output for image ID'
297
- end
298
-
299
- def build_image(state)
300
- cmd = "build"
301
- cmd << " --no-cache" unless config[:use_cache]
302
- extra_build_options = config_to_options(config[:build_options])
303
- cmd << " #{extra_build_options}" unless extra_build_options.empty?
304
- dockerfile_contents = dockerfile
305
- build_context = config[:build_context] ? '.' : '-'
306
- file = Tempfile.new('Dockerfile-kitchen', Dir.pwd)
307
- output = begin
308
- file.write(dockerfile)
309
- file.close
310
- docker_command("#{cmd} -f #{Shellwords.escape(file.path)} #{build_context}", :input => dockerfile_contents)
311
- ensure
312
- file.close unless file.closed?
313
- file.unlink
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'
314
114
  end
315
- parse_image_id(output)
316
115
  end
317
116
 
318
- def parse_container_id(output)
319
- container_id = output.chomp
320
- unless [12, 64].include?(container_id.size)
321
- raise ActionFailed,
322
- 'Could not parse Docker run output for container ID'
323
- end
324
- container_id
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'
325
121
  end
326
122
 
327
- def build_run_command(image_id)
328
- cmd = "run -d -p 22"
329
- Array(config[:forward]).each {|port| cmd << " -p #{port}"}
330
- Array(config[:dns]).each {|dns| cmd << " --dns #{dns}"}
331
- Array(config[:add_host]).each {|host, ip| cmd << " --add-host=#{host}:#{ip}"}
332
- Array(config[:volume]).each {|volume| cmd << " -v #{volume}"}
333
- Array(config[:volumes_from]).each {|container| cmd << " --volumes-from #{container}"}
334
- Array(config[:links]).each {|link| cmd << " --link #{link}"}
335
- Array(config[:devices]).each {|device| cmd << " --device #{device}"}
336
- cmd << " --name #{config[:instance_name]}" if config[:instance_name]
337
- cmd << " -P" if config[:publish_all]
338
- cmd << " -h #{config[:hostname]}" if config[:hostname]
339
- cmd << " -m #{config[:memory]}" if config[:memory]
340
- cmd << " -c #{config[:cpu]}" if config[:cpu]
341
- cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
342
- cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
343
- cmd << " --privileged" if config[:privileged]
344
- Array(config[:cap_add]).each {|cap| cmd << " --cap-add=#{cap}"} if config[:cap_add]
345
- Array(config[:cap_drop]).each {|cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
346
- Array(config[:security_opt]).each {|opt| cmd << " --security-opt=#{opt}"} if config[:security_opt]
347
- extra_run_options = config_to_options(config[:run_options])
348
- cmd << " #{extra_run_options}" unless extra_run_options.empty?
349
- cmd << " #{image_id} #{config[:run_command]}"
350
- cmd
351
- end
123
+ def create(state)
124
+ container.create(state)
352
125
 
353
- def run_container(state)
354
- cmd = build_run_command(state[:image_id])
355
- output = docker_command(cmd)
356
- parse_container_id(output)
126
+ wait_for_transport(state)
357
127
  end
358
128
 
359
- def container_exists?(state)
360
- state[:container_id] && !!docker_command("top #{state[:container_id]}") rescue false
129
+ def destroy(state)
130
+ container.destroy(state)
361
131
  end
362
132
 
363
- def parse_container_ssh_port(output)
364
- begin
365
- _host, port = output.split(':')
366
- port.to_i
367
- rescue
368
- raise ActionFailed,
369
- 'Could not parse Docker port output for container SSH port'
133
+ def wait_for_transport(state)
134
+ if config[:wait_for_transport]
135
+ instance.transport.connection(state) do |conn|
136
+ conn.wait_until_ready
137
+ end
370
138
  end
371
139
  end
372
140
 
373
- def container_ssh_port(state)
374
- begin
375
- output = docker_command("port #{state[:container_id]} 22/tcp")
376
- parse_container_ssh_port(output)
377
- rescue
378
- raise ActionFailed,
379
- 'Docker reports container has no ssh port mapped'
141
+ def default_image
142
+ platform, release = instance.platform.name.split('-')
143
+ if platform == 'centos' && release
144
+ release = 'centos' + release.split('.').first
380
145
  end
146
+ release ? [platform, release].join(':') : platform
381
147
  end
382
148
 
383
- def rm_container(state)
384
- container_id = state[:container_id]
385
- docker_command("stop -t 0 #{container_id}")
386
- docker_command("rm #{container_id}")
149
+ def default_platform
150
+ instance.platform.name.split('-').first
387
151
  end
388
152
 
389
- def rm_image(state)
390
- image_id = state[:image_id]
391
- docker_command("rmi #{image_id}")
392
- end
153
+ protected
393
154
 
394
- # Convert the config input for `:build_options` or `:run_options` in to a
395
- # command line string for use with Docker.
396
- #
397
- # @since 2.5.0
398
- # @param config [nil, String, Array, Hash] Config data to convert.
399
- # @return [String]
400
- def config_to_options(config)
401
- case config
402
- when nil
403
- ''
404
- when String
405
- config
406
- when Array
407
- config.map {|c| config_to_options(c) }.join(' ')
408
- when Hash
409
- config.map {|k, v| Array(v).map {|c| "--#{k}=#{Shellwords.escape(c)}" }.join(' ') }.join(' ')
410
- end
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
411
162
  end
412
-
413
163
  end
414
164
  end
415
165
  end