kitchen-docker_ssh 0.0.14

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.
@@ -0,0 +1,405 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Peter Abbott (<pabbottnz@gmail.com>)
4
+ #
5
+ # Copyright (C) 2015, Peter Abbott
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'kitchen'
20
+ require 'json'
21
+ require 'uri'
22
+ require File.join(File.dirname(__FILE__), 'docker', 'erb')
23
+
24
+ module Kitchen
25
+
26
+ module Driver
27
+
28
+ # DockerSsh driver for Kitchen.
29
+ #
30
+ # @author peter.abbott <peter.abbott@akqa.com>
31
+ class DockerSsh < Kitchen::Driver::SSHBase
32
+ default_config :binary, 'docker'
33
+ default_config :socket, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
34
+
35
+ default_config :privileged, false
36
+ default_config :use_cache, true
37
+ default_config :remove_images, false
38
+ default_config :run_command, '/usr/sbin/sshd -D \
39
+ -o UseDNS=no \
40
+ -o UsePAM=no \
41
+ -o PasswordAuthentication=yes \
42
+ -o UsePrivilegeSeparation=no \
43
+ -o PidFile=/tmp/sshd.pid'
44
+ default_config :username, 'kitchen'
45
+ default_config :password, 'kitchen'
46
+ default_config :no_ssh_tcp_check, false
47
+ default_config :tls, false
48
+ default_config :tls_verify, false
49
+ default_config :tls_cacert, nil
50
+ default_config :tls_cert, nil
51
+ default_config :tls_key, nil
52
+ default_config :publish_all, false
53
+ default_config :cap_add, nil
54
+ default_config :cap_drop, nil
55
+
56
+ default_config :use_sudo do |driver|
57
+ !driver.remote_socket?
58
+ end
59
+
60
+ default_config :image do |driver|
61
+ driver.default_image
62
+ end
63
+
64
+ default_config :platform do |driver|
65
+ driver.default_platform
66
+ end
67
+
68
+ default_config :platform_version do |driver|
69
+ driver.default_platform_version
70
+ end
71
+
72
+ default_config :disable_upstart, true
73
+
74
+ def verify_dependencies
75
+ begin
76
+ run_command("#{config[:binary]} info #{Helper.env_error_redirect}", \
77
+ :quiet => true,\
78
+ :use_sudo => false)
79
+ rescue
80
+ unless ENV['CI']
81
+ raise UserError, \
82
+ 'You must first install the Docker CLI tool http://www.docker.io/gettingstarted/'
83
+ end
84
+ end
85
+ if config[:cpuset] && !version_above?('1.1.0')
86
+ raise UserError, \
87
+ 'The cpuset option is only supported on docker '\
88
+ 'version >= 1.1.0, either remove this option or upgarde docker'
89
+ end
90
+ end
91
+
92
+ def default_image
93
+ platform, release = instance.platform.name.split('-')
94
+ if platform == 'centos' && release
95
+ release = 'centos' + release.split('.').first
96
+ end
97
+ release ? [platform, release].join(':') : platform
98
+ end
99
+
100
+ def default_platform
101
+ instance.platform.name.split('-').first
102
+ end
103
+
104
+ def default_platform_version
105
+ instance.platform.name.split('-').last
106
+ end
107
+
108
+ def create(state)
109
+ state[:image_id] = build_image(state) unless state[:image_id]
110
+ state[:container_id] = run_container(state) unless state[:container_id]
111
+ state[:hostname] = remote_socket? ? socket_uri.host : 'localhost'
112
+ state[:port] = container_ssh_port(state)
113
+
114
+ if config[:no_ssh_tcp_check]
115
+ wait_for_container(state)
116
+ else
117
+ sleep 1
118
+ wait_for_sshd(state[:hostname], nil, :port => state[:port])
119
+ end
120
+ logger.info("Created Container: #{state[:container_id]}")
121
+ end
122
+
123
+ def wait_for_container(state)
124
+ logger.info("Waiting for #{state[:hostname]}:#{state[:port]}...") until
125
+ begin
126
+ container_exists?(state)
127
+ rescue
128
+ false
129
+ end
130
+ end
131
+
132
+ def destroy(state)
133
+ rm_container(state) if container_exists?(state)
134
+ if config[:remove_images] && state[:image_id]
135
+ rm_image(state)
136
+ end
137
+ end
138
+
139
+ def remote_socket?
140
+ config[:socket] ? socket_uri.scheme == 'tcp' : false
141
+ end
142
+
143
+ protected
144
+
145
+ def socket_uri
146
+ URI.parse(config[:socket])
147
+ end
148
+
149
+ def docker_command(cmd, options={})
150
+ docker = config[:binary].dup
151
+ docker << " -H #{config[:socket]}" if config[:socket]
152
+ docker << " --tls" if config[:tls]
153
+ docker << " --tlsverify" if config[:tls_verify]
154
+ docker << " --tlscacert=#{config[:tls_cacert]}" if config[:tls_cacert]
155
+ docker << " --tlscert=#{config[:tls_cert]}" if config[:tls_cert]
156
+ docker << " --tlskey=#{config[:tls_key]}" if config[:tls_key]
157
+
158
+ if options[:quiet] == true
159
+ options.merge!(:live_stream => nil)
160
+ else
161
+ options.merge!(:quiet => !logger.debug?)
162
+ end
163
+
164
+ run_command("#{docker} #{cmd}", options)
165
+ end
166
+
167
+ def build_dockerfile
168
+ from = "FROM #{config[:image]}"
169
+ platform = case config[:platform]
170
+ when 'debian', 'ubuntu'
171
+ disable_upstart = <<-eos
172
+ RUN dpkg-divert --local --rename --add /sbin/initctl
173
+ RUN ln -sf /bin/true /sbin/initctl
174
+ eos
175
+ packages = <<-eos
176
+ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y sudo openssh-server curl lsb-release
177
+ eos
178
+ config[:disable_upstart] ? disable_upstart + packages : packages
179
+ when 'rhel', 'centos', 'fedora'
180
+ <<-eos
181
+ RUN yum clean all
182
+ RUN yum install -y sudo openssh-server openssh-clients which curl
183
+ RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
184
+ RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ''
185
+ eos
186
+ when 'arch'
187
+ <<-eos
188
+ RUN pacman -Syu --noconfirm
189
+ RUN pacman-db-upgrade
190
+ RUN pacman -S --noconfirm openssh sudo curl
191
+ RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
192
+ RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
193
+ eos
194
+ when 'gentoo'
195
+ <<-eos
196
+ RUN emerge sync
197
+ RUN emerge net-misc/openssh app-admin/sudo
198
+ RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
199
+ RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
200
+ eos
201
+ when 'gentoo-paludis'
202
+ <<-eos
203
+ RUN cave sync
204
+ RUN cave resolve -zx net-misc/openssh app-admin/sudo
205
+ RUN ssh-keygen -A -t rsa -f /etc/ssh/ssh_host_rsa_key
206
+ RUN ssh-keygen -A -t dsa -f /etc/ssh/ssh_host_dsa_key
207
+ eos
208
+ else
209
+ raise ActionFailed,
210
+ "Unknown platform '#{config[:platform]}'"
211
+ end
212
+ username = config[:username]
213
+ password = config[:password]
214
+
215
+ base = <<-eos
216
+ RUN useradd -d /home/#{username} -m -s /bin/bash #{username}
217
+ RUN echo #{username}:#{password} | chpasswd
218
+ RUN echo '#{username} ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
219
+ eos
220
+
221
+ sudoers = if supports_sudoers_d
222
+ ssh = <<-eos
223
+ RUN mkdir -p /etc/sudoers.d
224
+ RUN echo '#{username} ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/#{username}
225
+ RUN chmod 0440 /etc/sudoers.d/#{username}
226
+ eos
227
+ else '' end
228
+ custom = ''
229
+ Array(config[:provision_command]).each do |cmd|
230
+ custom << "RUN #{cmd}\n"
231
+ end
232
+ [from, platform, base, sudoers, custom].join("\n")
233
+ end
234
+
235
+ def supports_sudoers_d
236
+ case config[:platform]
237
+ when 'rhel', 'centos', 'fedora'
238
+ config[:platform_version].to_i >= 6
239
+ else
240
+ true
241
+ end
242
+ end
243
+
244
+ def dockerfile
245
+ if config[:dockerfile]
246
+ template = IO.read(File.expand_path(config[:dockerfile]))
247
+ context = DockerERBContext.new(config.to_hash)
248
+ ERB.new(template).result(context.get_binding)
249
+ else
250
+ build_dockerfile
251
+ end
252
+ end
253
+
254
+ def parse_image_id(output)
255
+ output.each_line do |line|
256
+ if line =~ /image id|build successful|successfully built/i
257
+ return line.split(/\s+/).last
258
+ end
259
+ end
260
+ raise ActionFailed,
261
+ 'Could not parse Docker build output for image ID'
262
+ end
263
+
264
+ def build_image(state)
265
+ cmd = "build"
266
+ cmd << " --no-cache" unless config[:use_cache]
267
+ output = docker_command("#{cmd} -", :input => dockerfile)
268
+ parse_image_id(output)
269
+ end
270
+
271
+ def parse_container_id(output)
272
+ container_id = output.chomp
273
+ unless [12, 64].include?(container_id.size)
274
+ raise ActionFailed,
275
+ 'Could not parse Docker run output for container ID'
276
+ end
277
+ container_id
278
+ end
279
+
280
+ def build_run_command(image_id)
281
+ cmd = "run -d -p 22"
282
+ Array(config[:forward]).each {|port| cmd << " -p #{port}"}
283
+ Array(config[:dns]).each {|dns| cmd << " --dns #{dns}"}
284
+ Array(config[:volume]).each {|volume| cmd << " -v #{volume}"}
285
+ Array(config[:volumes_from]).each {|container| cmd << " --volumes-from #{container}"}
286
+ Array(config[:links]).each {|link| cmd << " --link #{link}"}
287
+ cmd << " --name #{config[:instance_name]}" if config[:instance_name]
288
+ cmd << " -P" if config[:publish_all]
289
+ cmd << " -h #{config[:hostname]}" if config[:hostname]
290
+ cmd << " -m #{config[:memory]}" if config[:memory]
291
+ cmd << " -c #{config[:cpu]}" if config[:cpu]
292
+ cmd << " --cpuset=\"#{config[:cpuset]}\"" if config[:cpuset]
293
+ cmd << " --privileged" if config[:privileged]
294
+ cmd << " -e http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
295
+ cmd << " -e https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
296
+ if version_above?('1.2.0')
297
+ Array(config[:cap_add]).each { |cap| cmd << " --cap-add=#{cap}" } if config[:cap_add]
298
+ Array(config[:cap_drop]).each { |cap| cmd << " --cap-drop=#{cap}"} if config[:cap_drop]
299
+ end
300
+ cmd << " #{image_id} #{config[:run_command]}"
301
+ cmd
302
+ end
303
+
304
+ def run_container(state)
305
+ cmd = build_run_command(state[:image_id])
306
+ output = docker_command(cmd, :quiet => true)
307
+ parse_container_id(output)
308
+ end
309
+
310
+ def inspect_container(state)
311
+ container_id = state[:container_id]
312
+ unless container_id.nil?
313
+ begin
314
+ docker_command("inspect #{container_id}", :quiet => true)
315
+ rescue
316
+ logger.warn("Container #{container_id} no longer exists")
317
+ end
318
+ end
319
+ end
320
+
321
+ def container_exists?(state)
322
+ begin
323
+ state[:container_id] && docker_command("ps -q --no-trunc #{state[:container_id]}", :quiet => true).strip! == state[:container_id]
324
+ rescue
325
+ false
326
+ end
327
+ state[:container_id]
328
+ end
329
+
330
+ def parse_container_ssh_port(output)
331
+ begin
332
+ info = Array(::JSON.parse(output)).first
333
+ ports = info['NetworkSettings']['Ports'] || info['HostConfig']['PortBindings']
334
+ ssh_port = ports['22/tcp'].detect{|port| port['HostIp'] == '0.0.0.0'}
335
+ ssh_port['HostPort'].to_i
336
+ rescue
337
+ raise ActionFailed,
338
+ 'Could not parse Docker inspect output for container SSH port'
339
+ end
340
+ end
341
+
342
+ def container_ssh_port(state)
343
+ output = inspect_container(state)
344
+ parse_container_ssh_port(output)
345
+ end
346
+
347
+ def rm_container(state)
348
+ container_id = state[:container_id]
349
+
350
+ if container_exists?(state)
351
+ begin
352
+ docker_command("rm -f -v #{container_id}", :quiet => true)
353
+ rescue
354
+ logger.info("problem removing the container #{container_id}, may have already gone")
355
+ end
356
+ end
357
+ logger.info("Destroyed Container: #{state[:container_id]}")
358
+ end
359
+
360
+ def rm_image(state)
361
+ image_id = state[:image_id]
362
+ begin
363
+ docker_command("rmi #{image_id}")
364
+ rescue
365
+ logger.info('Unable to remove image #{image_id}, going to ignore')
366
+ end
367
+ end
368
+
369
+ def version_above?(version)
370
+ docker_version = docker_command('--version').split(',').first.scan(/\d+/).join('.')
371
+ Gem::Version.new(docker_version) >= Gem::Version.new(version)
372
+ end
373
+ end
374
+
375
+ # helper class
376
+ class Helper
377
+
378
+ def self.os
379
+ @os ||= (
380
+ host_os = RbConfig::CONFIG['host_os']
381
+ case host_os
382
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
383
+ :windows
384
+ when /darwin|mac os/
385
+ :macosx
386
+ when /linux/
387
+ :linux
388
+ when /solaris|bsd/
389
+ :unix
390
+ else
391
+ raise UserError, "unknown os: #{host_os.inspect}"
392
+ end
393
+ )
394
+ end
395
+
396
+ def self.env_error_redirect
397
+ if os == :windows
398
+ "2> NUL"
399
+ else
400
+ "2> /dev/null"
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Peter Abbott (pabbottnz@gmail.com)
4
+ #
5
+ # Copyright (C) 2015, Peter Abbott
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Kitchen
20
+
21
+ module Driver
22
+
23
+ # Version string for DockerSsh Kitchen driver
24
+ DOCKER_SSH_VERSION = IO.read("VERSION") rescue '0.0.1'
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ require 'kitchen/driver/docker'
2
+ #require 'kitchen/provisioner/dummy'
3
+
4
+ describe Kitchen::Driver::Docker do
5
+ let(:driver_object) { Kitchen::Driver::Docker.new(config) }
6
+
7
+ let(:driver) do
8
+ d = driver_object
9
+ instance
10
+ d
11
+ end
12
+ let(:instance) do
13
+ Kitchen::Instance.new(
14
+ :platform => double(:name => "centos-6.4"),
15
+ :suite => double(:name => "default"),
16
+ :driver => driver,
17
+ :provisioner => Kitchen::Provisioner::Dummy.new({}),
18
+ :busser => double("busser"),
19
+ :state_file => double("state_file")
20
+ )
21
+ end
22
+ describe "configuration" do
23
+ it "dummy" do
24
+ expect(instance[:platform]).to eq("ubuntu:12.04")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ ---
2
+ driver:
3
+ name: docker_ssh
4
+ provision_command: curl -L http://www.chef.io/chef/install.sh | bash
5
+ provisioner:
6
+ name: dummy
7
+ platforms:
8
+ - name: ubuntu-12.04
9
+ - name: ubuntu-14.04
10
+ - name: centos-5
11
+ - name: centos-6.4
12
+ - name: centos-7
13
+ - name: centos-6
14
+ - name: centos-5
15
+ - name: debian
16
+ - name: arch
17
+ driver:
18
+ image: base/arch
19
+ - name: dockerfile
20
+ driver:
21
+ username: dockerfile
22
+ password: dockerfile
23
+ use_cache: false
24
+ dockerfile: test/Dockerfile
25
+ run_command: /sbin/init
26
+ - name: database
27
+ driver:
28
+ image: ubuntu:14.04
29
+ platform: ubuntu
30
+ instance_name: db
31
+ publish_all: true
32
+ - name: linked
33
+ driver:
34
+ image: ubuntu:14.04
35
+ platform: ubuntu
36
+ links: "db:db"
37
+ suites:
38
+ - name: default
39
+ - name: concurrency
40
+ - name: capabilities
41
+ excludes: [unknown,centos-7,centos-6.4,dockerfile]
42
+ driver:
43
+ provision_command:
44
+ - curl -L http://www.chef.io/chef/install.sh | bash
45
+ - apt-get install -y net-tools
46
+ username: kitchen
47
+ password: kitchen
48
+ cap_drop:
49
+ - NET_ADMIN
data/test/.kitchen.yml ADDED
@@ -0,0 +1,29 @@
1
+ ---
2
+ driver:
3
+ name: docker_ssh
4
+ provision_command: curl -L http://www.chef.io/chef/install.sh | bash
5
+ provisioner:
6
+ name: dummy
7
+ platforms:
8
+ - name: ubuntu-12.04
9
+ driver:
10
+ provision_command:
11
+ - curl -L http://www.chef.io/chef/install.sh | bash
12
+ - apt-get install -y net-tools
13
+ - name: ubuntu-14.04
14
+ driver:
15
+ provision_command:
16
+ - curl -L http://www.chef.io/chef/install.sh | bash
17
+ - apt-get install -y net-tools
18
+ - name: centos-5
19
+ - name: centos-6.6
20
+ - name: centos-7
21
+ - name: centos-5
22
+ - name: debian
23
+ suites:
24
+ - name: default
25
+ - name: capabilities
26
+ excludes: [ centos-7, debian ]
27
+ driver:
28
+ cap_drop:
29
+ - NET_ADMIN
@@ -0,0 +1,8 @@
1
+ require 'serverspec'
2
+
3
+ set :backend, :exec
4
+
5
+ describe command('sudo /sbin/ifconfig eth0 multicast') do
6
+ its(:exit_status) { should_not eq 0 }
7
+ its(:stdout) { should match /Operation not permitted/ }
8
+ end
@@ -0,0 +1,6 @@
1
+ require 'serverspec'
2
+ set :backend, :exec
3
+
4
+ describe file('/etc/passwd') do
5
+ it { should be_file }
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'serverspec'
2
+ set :backend, :exec
3
+
4
+ describe file('/etc/passwd') do
5
+ it { should be_file }
6
+ end