kitchen-docker 2.6.0 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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