beaker-docker 0.7.0 → 0.8.3
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/test.yml +105 -0
- data/Gemfile.local +5 -0
- data/README.md +92 -28
- data/Rakefile +8 -4
- data/acceptance/config/nodes/hosts.yaml +21 -14
- data/acceptance/tests/00_default_spec.rb +10 -0
- data/beaker-docker.gemspec +14 -14
- data/lib/beaker-docker/version.rb +1 -1
- data/lib/beaker/hypervisor/docker.rb +157 -51
- data/spec/beaker/hypervisor/docker_spec.rb +509 -479
- metadata +31 -23
@@ -19,19 +19,25 @@ module Beaker
|
|
19
19
|
default_docker_options = { :write_timeout => 300, :read_timeout => 300 }.merge(::Docker.options || {})
|
20
20
|
# Merge docker options from the entry in hosts file
|
21
21
|
::Docker.options = default_docker_options.merge(@options[:docker_options] || {})
|
22
|
-
|
23
|
-
#
|
22
|
+
|
23
|
+
# Ensure that we can correctly communicate with the docker API
|
24
24
|
begin
|
25
|
-
::Docker.
|
25
|
+
@docker_version = ::Docker.version
|
26
26
|
rescue Excon::Errors::SocketError => e
|
27
|
-
raise
|
27
|
+
raise <<~ERRMSG
|
28
|
+
Docker instance not connectable
|
29
|
+
Error was: #{e}
|
30
|
+
* Check your DOCKER_HOST variable has been set
|
31
|
+
* If you are on OSX or Windows, you might not have Docker Machine setup correctly: https://docs.docker.com/machine/
|
32
|
+
* If you are using rootless podman, you might need to set up your local socket and service
|
33
|
+
ERRMSG
|
28
34
|
end
|
29
35
|
|
30
36
|
# Pass on all the logging from docker-api to the beaker logger instance
|
31
37
|
::Docker.logger = @logger
|
32
38
|
|
33
39
|
# Find out what kind of remote instance we are talking against
|
34
|
-
if
|
40
|
+
if @docker_version['Version'] =~ /swarm/
|
35
41
|
@docker_type = 'swarm'
|
36
42
|
unless ENV['DOCKER_REGISTRY']
|
37
43
|
raise "Using Swarm with beaker requires a private registry. Please setup the private registry and set the 'DOCKER_REGISTRY' env var"
|
@@ -41,10 +47,21 @@ module Beaker
|
|
41
47
|
else
|
42
48
|
@docker_type = 'docker'
|
43
49
|
end
|
44
|
-
|
45
50
|
end
|
46
51
|
|
47
52
|
def install_and_run_ssh(host)
|
53
|
+
def host.enable_root_login(host,opts)
|
54
|
+
logger.debug("Root login already enabled for #{host}")
|
55
|
+
end
|
56
|
+
|
57
|
+
# If the container is running ssh as its init process then this method
|
58
|
+
# will cause issues.
|
59
|
+
if host[:docker_cmd] =~ /sshd/
|
60
|
+
def host.ssh_service_restart
|
61
|
+
self[:docker_container].exec(%w(kill -1 1))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
48
65
|
host['dockerfile'] || host['use_image_entry_point']
|
49
66
|
end
|
50
67
|
|
@@ -62,7 +79,6 @@ module Beaker
|
|
62
79
|
'22/tcp' => [{ 'HostPort' => rand.to_s[2..5], 'HostIp' => '0.0.0.0'}]
|
63
80
|
},
|
64
81
|
'PublishAllPorts' => true,
|
65
|
-
'Privileged' => true,
|
66
82
|
'RestartPolicy' => {
|
67
83
|
'Name' => 'always'
|
68
84
|
}
|
@@ -109,6 +125,48 @@ module Beaker
|
|
109
125
|
{ rm: true, buildargs: buildargs_for(host) })
|
110
126
|
end
|
111
127
|
|
128
|
+
# Find out where the ssh port is from the container
|
129
|
+
# When running on swarm DOCKER_HOST points to the swarm manager so we have to get the
|
130
|
+
# IP of the swarm slave via the container data
|
131
|
+
# When we are talking to a normal docker instance DOCKER_HOST can point to a remote docker instance.
|
132
|
+
def get_ssh_connection_info(container)
|
133
|
+
ssh_connection_info = {
|
134
|
+
ip: nil,
|
135
|
+
port: nil
|
136
|
+
}
|
137
|
+
|
138
|
+
container_json = container.json
|
139
|
+
network_settings = container_json['NetworkSettings']
|
140
|
+
host_config = container_json['HostConfig']
|
141
|
+
|
142
|
+
ip = nil
|
143
|
+
port = nil
|
144
|
+
# Talking against a remote docker host which is a normal docker host
|
145
|
+
if @docker_type == 'docker' && ENV['DOCKER_HOST'] && !ENV.fetch('DOCKER_HOST','').include?(':///')
|
146
|
+
ip = URI.parse(ENV['DOCKER_HOST']).host
|
147
|
+
else
|
148
|
+
# Swarm or local docker host
|
149
|
+
if in_container?
|
150
|
+
gw = network_settings['Gateway']
|
151
|
+
ip = gw unless (gw.nil? || gw.empty?)
|
152
|
+
else
|
153
|
+
port22 = network_settings.dig('Ports','22/tcp')
|
154
|
+
ip = port22[0]["HostIp"] if port22
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if host_config['NetworkMode'] != 'slirp4netns' && network_settings['IPAddress'] && !network_settings['IPAddress'].empty?
|
159
|
+
ip = network_settings['IPAddress']
|
160
|
+
else
|
161
|
+
port22 = network_settings.dig('Ports','22/tcp')
|
162
|
+
port = port22[0]['HostPort'] if port22
|
163
|
+
end
|
164
|
+
|
165
|
+
ssh_connection_info[:ip] = (ip == '0.0.0.0') ? '127.0.0.1' : ip
|
166
|
+
ssh_connection_info[:port] = port || '22'
|
167
|
+
ssh_connection_info
|
168
|
+
end
|
169
|
+
|
112
170
|
def provision
|
113
171
|
@logger.notify "Provisioning docker"
|
114
172
|
|
@@ -134,6 +192,8 @@ module Beaker
|
|
134
192
|
image_name = image.id
|
135
193
|
end
|
136
194
|
|
195
|
+
### BEGIN CONTAINER OPTIONS MANGLING ###
|
196
|
+
|
137
197
|
container_opts = get_container_opts(host, image_name)
|
138
198
|
if host['dockeropts'] || @options[:dockeropts]
|
139
199
|
dockeropts = host['dockeropts'] ? host['dockeropts'] : @options[:dockeropts]
|
@@ -156,7 +216,12 @@ module Beaker
|
|
156
216
|
host_path = "/" + host_path.gsub(/^.\:/, host_path[/^(.)/].downcase)
|
157
217
|
end
|
158
218
|
a = [ host_path, mount['container_path'] ]
|
159
|
-
|
219
|
+
if mount.has_key?('opts')
|
220
|
+
a << mount['opts'] if mount.has_key?('opts')
|
221
|
+
else
|
222
|
+
a << mount['opts'] = 'z'
|
223
|
+
end
|
224
|
+
|
160
225
|
a.join(':')
|
161
226
|
end
|
162
227
|
end
|
@@ -165,16 +230,46 @@ module Beaker
|
|
165
230
|
container_opts['Env'] = host['docker_env']
|
166
231
|
end
|
167
232
|
|
233
|
+
# Fixup privileges
|
234
|
+
#
|
235
|
+
# If the user has specified CAPs, then we cannot be privileged
|
236
|
+
#
|
237
|
+
# If the user has not specified CAPs, we will default to privileged for
|
238
|
+
# compatibility with worst practice
|
168
239
|
if host['docker_cap_add']
|
169
240
|
container_opts['HostConfig']['CapAdd'] = host['docker_cap_add']
|
241
|
+
container_opts['HostConfig'].delete('Privileged')
|
242
|
+
else
|
243
|
+
container_opts['HostConfig']['Privileged'] = container_opts['HostConfig']['Privileged'].nil? ? true : container_opts['HostConfig']['Privileged']
|
170
244
|
end
|
171
245
|
|
172
246
|
if host['docker_container_name']
|
173
247
|
container_opts['name'] = host['docker_container_name']
|
248
|
+
else
|
249
|
+
container_opts['name'] = ['beaker', host.name, SecureRandom.uuid.split('-').last].join('-')
|
174
250
|
end
|
175
251
|
|
252
|
+
### END CONTAINER OPTIONS MANGLING ###
|
253
|
+
|
176
254
|
@logger.debug("Creating container from image #{image_name}")
|
177
|
-
|
255
|
+
|
256
|
+
ok=false
|
257
|
+
retries=0
|
258
|
+
while(!ok && (retries < 5))
|
259
|
+
container = ::Docker::Container.create(container_opts)
|
260
|
+
|
261
|
+
ssh_info = get_ssh_connection_info(container)
|
262
|
+
if ssh_info[:ip] == '127.0.0.1' && (ssh_info[:port].to_i < 1024) && (Process.uid != 0)
|
263
|
+
@logger.debug("#{host} was given a port less than 1024 but you are not running as root, retrying")
|
264
|
+
|
265
|
+
container.delete
|
266
|
+
|
267
|
+
retries+=1
|
268
|
+
next
|
269
|
+
end
|
270
|
+
|
271
|
+
ok=true
|
272
|
+
end
|
178
273
|
else
|
179
274
|
host['use_existing_container'] = true
|
180
275
|
end
|
@@ -189,36 +284,36 @@ module Beaker
|
|
189
284
|
@logger.debug("Starting container #{container.id}")
|
190
285
|
container.start
|
191
286
|
|
287
|
+
begin
|
288
|
+
container.stats
|
289
|
+
rescue StandardError => e
|
290
|
+
container.delete
|
291
|
+
raise "Container '#{container.id}' in a bad state: #{e}"
|
292
|
+
end
|
293
|
+
|
294
|
+
# Preserve the ability to talk directly to the underlying API
|
295
|
+
#
|
296
|
+
# You can use any method defined by the docker-api gem on this object
|
297
|
+
# https://github.com/swipely/docker-api
|
298
|
+
host[:docker_container] = container
|
299
|
+
|
192
300
|
if install_and_run_ssh(host)
|
193
301
|
@logger.notify("Installing ssh components and starting ssh daemon in #{host} container")
|
194
302
|
install_ssh_components(container, host)
|
195
303
|
# run fixssh to configure and start the ssh service
|
196
304
|
fix_ssh(container, host)
|
197
305
|
end
|
198
|
-
# Find out where the ssh port is from the container
|
199
|
-
# When running on swarm DOCKER_HOST points to the swarm manager so we have to get the
|
200
|
-
# IP of the swarm slave via the container data
|
201
|
-
# When we are talking to a normal docker instance DOCKER_HOST can point to a remote docker instance.
|
202
|
-
|
203
|
-
# Talking against a remote docker host which is a normal docker host
|
204
|
-
if @docker_type == 'docker' && ENV['DOCKER_HOST']
|
205
|
-
ip = URI.parse(ENV['DOCKER_HOST']).host
|
206
|
-
else
|
207
|
-
# Swarm or local docker host
|
208
|
-
if in_container?
|
209
|
-
ip = container.json["NetworkSettings"]["Gateway"]
|
210
|
-
else
|
211
|
-
ip = container.json["NetworkSettings"]["Ports"]["22/tcp"][0]["HostIp"]
|
212
|
-
end
|
213
|
-
end
|
214
306
|
|
215
|
-
|
216
|
-
|
307
|
+
ssh_connection_info = get_ssh_connection_info(container)
|
308
|
+
|
309
|
+
ip = ssh_connection_info[:ip]
|
310
|
+
port = ssh_connection_info[:port]
|
311
|
+
|
312
|
+
@logger.info("Using container connection at #{ip}:#{port}")
|
217
313
|
|
218
314
|
forward_ssh_agent = @options[:forward_ssh_agent] || false
|
219
315
|
|
220
|
-
|
221
|
-
host['ip'] = ip
|
316
|
+
host['ip'] = ip
|
222
317
|
host['port'] = port
|
223
318
|
host['ssh'] = {
|
224
319
|
:password => root_password,
|
@@ -227,15 +322,17 @@ module Beaker
|
|
227
322
|
:auth_methods => ['password', 'publickey', 'hostbased', 'keyboard-interactive']
|
228
323
|
}
|
229
324
|
|
230
|
-
@logger.debug("node available as
|
325
|
+
@logger.debug("node available as ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@#{ip} -p #{port}")
|
231
326
|
host['docker_container_id'] = container.id
|
232
327
|
host['docker_image_id'] = image.id
|
233
328
|
host['vm_ip'] = container.json["NetworkSettings"]["IPAddress"].to_s
|
234
329
|
|
330
|
+
def host.reboot
|
331
|
+
@logger.warn("Rebooting containers is ineffective...ignoring")
|
332
|
+
end
|
235
333
|
end
|
236
334
|
|
237
335
|
hack_etc_hosts @hosts, @options
|
238
|
-
|
239
336
|
end
|
240
337
|
|
241
338
|
# This sideloads sshd after a container starts
|
@@ -244,19 +341,23 @@ module Beaker
|
|
244
341
|
when /ubuntu/, /debian/
|
245
342
|
container.exec(%w(apt-get update))
|
246
343
|
container.exec(%w(apt-get install -y openssh-server openssh-client))
|
344
|
+
container.exec(%w(sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*))
|
247
345
|
when /cumulus/
|
248
346
|
container.exec(%w(apt-get update))
|
249
347
|
container.exec(%w(apt-get install -y openssh-server openssh-client))
|
348
|
+
container.exec(%w(sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*))
|
250
349
|
when /fedora-(2[2-9])/
|
251
350
|
container.exec(%w(dnf clean all))
|
252
351
|
container.exec(%w(dnf install -y sudo openssh-server openssh-clients))
|
253
352
|
container.exec(%w(ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key))
|
254
353
|
container.exec(%w(ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key))
|
354
|
+
container.exec(%w(sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*))
|
255
355
|
when /^el-/, /centos/, /fedora/, /redhat/, /eos/
|
256
356
|
container.exec(%w(yum clean all))
|
257
357
|
container.exec(%w(yum install -y sudo openssh-server openssh-clients))
|
258
358
|
container.exec(%w(ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key))
|
259
359
|
container.exec(%w(ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key))
|
360
|
+
container.exec(%w(sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*))
|
260
361
|
when /opensuse/, /sles/
|
261
362
|
container.exec(%w(zypper -n in openssh))
|
262
363
|
container.exec(%w(ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key))
|
@@ -372,71 +473,76 @@ module Beaker
|
|
372
473
|
case host['platform']
|
373
474
|
when /ubuntu/, /debian/
|
374
475
|
service_name = "ssh"
|
375
|
-
dockerfile +=
|
476
|
+
dockerfile += <<~EOF
|
376
477
|
RUN apt-get update
|
377
478
|
RUN apt-get install -y openssh-server openssh-client #{Beaker::HostPrebuiltSteps::DEBIAN_PACKAGES.join(' ')}
|
378
|
-
|
479
|
+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*
|
480
|
+
EOF
|
379
481
|
when /cumulus/
|
380
|
-
dockerfile +=
|
482
|
+
dockerfile += <<~EOF
|
381
483
|
RUN apt-get update
|
382
484
|
RUN apt-get install -y openssh-server openssh-client #{Beaker::HostPrebuiltSteps::CUMULUS_PACKAGES.join(' ')}
|
383
|
-
|
485
|
+
EOF
|
384
486
|
when /fedora-(2[2-9])/
|
385
|
-
dockerfile +=
|
487
|
+
dockerfile += <<~EOF
|
386
488
|
RUN dnf clean all
|
387
489
|
RUN dnf install -y sudo openssh-server openssh-clients #{Beaker::HostPrebuiltSteps::UNIX_PACKAGES.join(' ')}
|
388
490
|
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
389
491
|
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
390
|
-
|
492
|
+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*
|
493
|
+
EOF
|
391
494
|
when /el-8/
|
392
|
-
dockerfile +=
|
393
|
-
RUN
|
394
|
-
RUN
|
495
|
+
dockerfile += <<~EOF
|
496
|
+
RUN dnf clean all
|
497
|
+
RUN dnf install -y sudo openssh-server openssh-clients #{Beaker::HostPrebuiltSteps::RHEL8_PACKAGES.join(' ')}
|
395
498
|
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
396
499
|
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
397
|
-
|
500
|
+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*
|
501
|
+
EOF
|
398
502
|
when /^el-/, /centos/, /fedora/, /redhat/, /eos/
|
399
|
-
dockerfile +=
|
503
|
+
dockerfile += <<~EOF
|
400
504
|
RUN yum clean all
|
401
505
|
RUN yum install -y sudo openssh-server openssh-clients #{Beaker::HostPrebuiltSteps::UNIX_PACKAGES.join(' ')}
|
402
506
|
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
403
507
|
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
404
|
-
|
508
|
+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*
|
509
|
+
EOF
|
405
510
|
when /opensuse/, /sles/
|
406
|
-
dockerfile +=
|
511
|
+
dockerfile += <<~EOF
|
407
512
|
RUN zypper -n in openssh #{Beaker::HostPrebuiltSteps::SLES_PACKAGES.join(' ')}
|
408
513
|
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
409
514
|
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
410
515
|
RUN sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config
|
411
|
-
|
516
|
+
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*
|
517
|
+
EOF
|
412
518
|
when /archlinux/
|
413
|
-
dockerfile +=
|
519
|
+
dockerfile += <<~EOF
|
414
520
|
RUN pacman --noconfirm -Sy archlinux-keyring
|
415
521
|
RUN pacman --noconfirm -Syu
|
416
522
|
RUN pacman -S --noconfirm openssh #{Beaker::HostPrebuiltSteps::ARCHLINUX_PACKAGES.join(' ')}
|
417
523
|
RUN ssh-keygen -A
|
418
524
|
RUN sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config
|
419
525
|
RUN systemctl enable sshd
|
420
|
-
|
526
|
+
EOF
|
421
527
|
else
|
422
528
|
# TODO add more platform steps here
|
423
529
|
raise "platform #{host['platform']} not yet supported on docker"
|
424
530
|
end
|
425
531
|
|
426
532
|
# Make sshd directory, set root password
|
427
|
-
dockerfile +=
|
533
|
+
dockerfile += <<~EOF
|
428
534
|
RUN mkdir -p /var/run/sshd
|
429
535
|
RUN echo root:#{root_password} | chpasswd
|
430
|
-
|
536
|
+
EOF
|
431
537
|
|
432
538
|
# Configure sshd service to allowroot login using password
|
433
539
|
# Also, disable reverse DNS lookups to prevent every. single. ssh
|
434
540
|
# operation taking 30 seconds while the lookup times out.
|
435
|
-
dockerfile +=
|
541
|
+
dockerfile += <<~EOF
|
436
542
|
RUN sed -ri 's/^#?PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
437
543
|
RUN sed -ri 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
438
544
|
RUN sed -ri 's/^#?UseDNS .*/UseDNS no/' /etc/ssh/sshd_config
|
439
|
-
|
545
|
+
EOF
|
440
546
|
|
441
547
|
|
442
548
|
# Any extra commands specified for the host
|
@@ -1,14 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'fakefs/spec_helpers'
|
3
3
|
|
4
|
-
# fake the docker-api
|
5
|
-
module Docker
|
6
|
-
class Image
|
7
|
-
end
|
8
|
-
class Container
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
4
|
module Beaker
|
13
5
|
platforms = [
|
14
6
|
"ubuntu-14.04-x86_64",
|
@@ -20,6 +12,8 @@ module Beaker
|
|
20
12
|
]
|
21
13
|
|
22
14
|
describe Docker do
|
15
|
+
require 'docker'
|
16
|
+
|
23
17
|
let(:hosts) {
|
24
18
|
the_hosts = make_hosts
|
25
19
|
the_hosts[2]['dockeropts'] = {
|
@@ -60,14 +54,15 @@ module Beaker
|
|
60
54
|
image
|
61
55
|
end
|
62
56
|
|
63
|
-
let(:
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
57
|
+
let(:container_mode) do
|
58
|
+
'rootless'
|
59
|
+
end
|
60
|
+
|
61
|
+
let(:container_config) do
|
62
|
+
conf = {
|
63
|
+
'HostConfig' => {
|
64
|
+
'NetworkMode' => 'slirp4netns'
|
65
|
+
},
|
71
66
|
'NetworkSettings' => {
|
72
67
|
'IPAddress' => '192.0.2.1',
|
73
68
|
'Ports' => {
|
@@ -80,7 +75,24 @@ module Beaker
|
|
80
75
|
},
|
81
76
|
'Gateway' => '192.0.2.254'
|
82
77
|
}
|
83
|
-
}
|
78
|
+
}
|
79
|
+
|
80
|
+
unless container_mode == 'rootless'
|
81
|
+
conf['HostConfig']['NetworkMode'] = 'bridge'
|
82
|
+
end
|
83
|
+
|
84
|
+
conf
|
85
|
+
end
|
86
|
+
|
87
|
+
let(:container) do
|
88
|
+
container = double('Docker::Container')
|
89
|
+
allow( container ).to receive(:id).and_return('abcdef')
|
90
|
+
allow( container ).to receive(:start)
|
91
|
+
allow( container ).to receive(:stats)
|
92
|
+
allow( container ).to receive(:info).and_return(
|
93
|
+
*(0..2).map { |index| { 'Names' => ["/spec-container-#{index}"] } }
|
94
|
+
)
|
95
|
+
allow( container ).to receive(:json).and_return(container_config)
|
84
96
|
allow( container ).to receive(:kill)
|
85
97
|
allow( container ).to receive(:delete)
|
86
98
|
allow( container ).to receive(:exec)
|
@@ -88,603 +100,621 @@ module Beaker
|
|
88
100
|
end
|
89
101
|
|
90
102
|
let (:docker) { ::Beaker::Docker.new( hosts, options ) }
|
103
|
+
|
91
104
|
let(:docker_options) { nil }
|
105
|
+
|
92
106
|
let (:version) { {"ApiVersion"=>"1.18", "Arch"=>"amd64", "GitCommit"=>"4749651", "GoVersion"=>"go1.4.2", "KernelVersion"=>"3.16.0-37-generic", "Os"=>"linux", "Version"=>"1.6.0"} }
|
93
107
|
|
94
|
-
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
allow( ::Docker ).to receive(:logger=)
|
101
|
-
allow( ::Docker ).to receive(:version).and_return(version)
|
102
|
-
allow( ::Docker::Image ).to receive(:build).and_return(image)
|
103
|
-
allow( ::Docker::Image ).to receive(:create).and_return(image)
|
104
|
-
allow( ::Docker::Container ).to receive(:create).and_return(container)
|
105
|
-
allow_any_instance_of( ::Docker::Container ).to receive(:start)
|
106
|
-
end
|
108
|
+
context 'with connection failure' do
|
109
|
+
describe '#initialize' do
|
110
|
+
before :each do
|
111
|
+
require 'excon'
|
112
|
+
expect( ::Docker ).to receive(:version).and_raise(Excon::Errors::SocketError.new( StandardError.new('oops') )).exactly(4).times
|
113
|
+
end
|
107
114
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
expect { docker }.to raise_error(RuntimeError, /Docker instance not connectable./)
|
115
|
-
expect { docker }.to raise_error(RuntimeError, /Check your DOCKER_HOST variable has been set/)
|
116
|
-
expect { docker }.to raise_error(RuntimeError, /If you are on OSX or Windows, you might not have Docker Machine setup correctly/)
|
117
|
-
expect { docker }.to raise_error(RuntimeError, /Error was: oops/)
|
115
|
+
it 'should fail when docker not present' do
|
116
|
+
expect { docker }.to raise_error(RuntimeError, /Docker instance not connectable/)
|
117
|
+
expect { docker }.to raise_error(RuntimeError, /Check your DOCKER_HOST variable has been set/)
|
118
|
+
expect { docker }.to raise_error(RuntimeError, /If you are on OSX or Windows, you might not have Docker Machine setup correctly/)
|
119
|
+
expect { docker }.to raise_error(RuntimeError, /Error was: oops/)
|
120
|
+
end
|
118
121
|
end
|
119
122
|
end
|
120
123
|
|
121
|
-
|
124
|
+
|
125
|
+
context 'with a working connection' do
|
122
126
|
before :each do
|
123
|
-
|
124
|
-
|
127
|
+
# Stub out all of the docker-api gem. we should never really call it
|
128
|
+
# from these tests
|
129
|
+
allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker')
|
130
|
+
allow( ::Docker ).to receive(:options).and_return(docker_options)
|
131
|
+
allow( ::Docker ).to receive(:options=)
|
132
|
+
allow( ::Docker ).to receive(:logger=)
|
133
|
+
allow( ::Docker ).to receive(:version).and_return(version)
|
134
|
+
allow( ::Docker::Image ).to receive(:build).and_return(image)
|
135
|
+
allow( ::Docker::Image ).to receive(:create).and_return(image)
|
136
|
+
allow( ::Docker::Container ).to receive(:create).and_return(container)
|
137
|
+
allow_any_instance_of( ::Docker::Container ).to receive(:start)
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '#initialize' do
|
141
|
+
it 'should require the docker gem' do
|
142
|
+
expect_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').once
|
125
143
|
|
126
|
-
|
127
|
-
|
144
|
+
docker
|
145
|
+
end
|
128
146
|
|
129
|
-
|
130
|
-
|
147
|
+
it 'should fail when the gem is absent' do
|
148
|
+
allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').and_raise(LoadError)
|
149
|
+
expect { docker }.to raise_error(LoadError)
|
150
|
+
end
|
131
151
|
|
132
|
-
|
133
|
-
|
134
|
-
expect { docker }.to raise_error(LoadError)
|
135
|
-
end
|
152
|
+
it 'should set Docker options' do
|
153
|
+
expect( ::Docker ).to receive(:options=).with({:write_timeout => 300, :read_timeout => 300}).once
|
136
154
|
|
137
|
-
|
138
|
-
|
155
|
+
docker
|
156
|
+
end
|
139
157
|
|
140
|
-
|
141
|
-
|
158
|
+
context 'when Docker options are already set' do
|
159
|
+
let(:docker_options) {{:write_timeout => 600, :foo => :bar}}
|
142
160
|
|
143
|
-
|
144
|
-
|
161
|
+
it 'should not override Docker options' do
|
162
|
+
expect( ::Docker ).to receive(:options=).with({:write_timeout => 600, :read_timeout => 300, :foo => :bar}).once
|
145
163
|
|
146
|
-
|
147
|
-
|
164
|
+
docker
|
165
|
+
end
|
166
|
+
end
|
148
167
|
|
168
|
+
it 'should check the Docker gem can work with the api' do
|
149
169
|
docker
|
150
170
|
end
|
151
|
-
end
|
152
171
|
|
153
|
-
|
154
|
-
|
172
|
+
it 'should hook the Beaker logger into the Docker one' do
|
173
|
+
expect( ::Docker ).to receive(:logger=).with(logger)
|
155
174
|
|
156
|
-
|
175
|
+
docker
|
176
|
+
end
|
157
177
|
end
|
158
178
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
179
|
+
describe '#install_ssh_components' do
|
180
|
+
let(:test_container) { double('container') }
|
181
|
+
let(:host) {hosts[0]}
|
182
|
+
before :each do
|
183
|
+
allow( docker ).to receive(:dockerfile_for)
|
184
|
+
end
|
165
185
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
186
|
+
platforms.each do |platform|
|
187
|
+
it 'should call exec at least twice' do
|
188
|
+
host['platform'] = platform
|
189
|
+
expect(test_container).to receive(:exec).at_least(:twice)
|
190
|
+
docker.install_ssh_components(test_container, host)
|
191
|
+
end
|
192
|
+
end
|
173
193
|
|
174
|
-
|
175
|
-
|
176
|
-
host['platform'] = platform
|
194
|
+
it 'should accept alpine as valid platform' do
|
195
|
+
host['platform'] = 'alpine-3.8-x86_64'
|
177
196
|
expect(test_container).to receive(:exec).at_least(:twice)
|
178
197
|
docker.install_ssh_components(test_container, host)
|
179
198
|
end
|
180
|
-
end
|
181
199
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
200
|
+
it 'should raise an error with an unsupported platform' do
|
201
|
+
host['platform'] = 'boogeyman-2000-x86_64'
|
202
|
+
expect{docker.install_ssh_components(test_container, host)}.to raise_error(RuntimeError, /boogeyman/)
|
203
|
+
end
|
186
204
|
end
|
187
205
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
end
|
206
|
+
describe '#provision' do
|
207
|
+
before :each do
|
208
|
+
allow( docker ).to receive(:dockerfile_for)
|
209
|
+
end
|
193
210
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
211
|
+
context 'when the host has "tag" defined' do
|
212
|
+
before :each do
|
213
|
+
hosts.each do |host|
|
214
|
+
host['tag'] = 'my_tag'
|
215
|
+
end
|
216
|
+
end
|
199
217
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
host['tag'] = 'my_tag'
|
218
|
+
it 'will tag the image with the value of the tag' do
|
219
|
+
expect( image ).to receive(:tag).with({:repo => 'my_tag'}).exactly(3).times
|
220
|
+
docker.provision
|
204
221
|
end
|
205
222
|
end
|
206
223
|
|
207
|
-
|
208
|
-
expect( image ).to receive(:tag).with({:repo => 'my_tag'}).exactly(3).times
|
209
|
-
docker.provision
|
210
|
-
end
|
211
|
-
end
|
224
|
+
context 'when the host has "use_image_entry_point" set to true on the host' do
|
212
225
|
|
213
|
-
|
226
|
+
before :each do
|
227
|
+
hosts.each do |host|
|
228
|
+
host['use_image_entry_point'] = true
|
229
|
+
end
|
230
|
+
end
|
214
231
|
|
215
|
-
|
216
|
-
|
217
|
-
|
232
|
+
it 'should not call #dockerfile_for but run methods necessary for ssh installation' do
|
233
|
+
expect( docker ).not_to receive(:dockerfile_for)
|
234
|
+
expect( docker ).to receive(:install_ssh_components).exactly(3).times #once per host
|
235
|
+
expect( docker ).to receive(:fix_ssh).exactly(3).times #once per host
|
236
|
+
docker.provision
|
218
237
|
end
|
219
238
|
end
|
220
239
|
|
221
|
-
|
222
|
-
expect( docker ).not_to receive(:dockerfile_for)
|
223
|
-
expect( docker ).to receive(:install_ssh_components).exactly(3).times #once per host
|
224
|
-
expect( docker ).to receive(:fix_ssh).exactly(3).times #once per host
|
225
|
-
docker.provision
|
226
|
-
end
|
227
|
-
end
|
240
|
+
context 'when the host has a "dockerfile" for the host' do
|
228
241
|
|
229
|
-
|
242
|
+
before :each do
|
243
|
+
allow( docker ).to receive(:buildargs_for).and_return('buildargs')
|
244
|
+
hosts.each do |host|
|
245
|
+
host['dockerfile'] = 'mydockerfile'
|
246
|
+
end
|
247
|
+
end
|
230
248
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
249
|
+
it 'should not call #dockerfile_for but run methods necessary for ssh installation' do
|
250
|
+
allow( File ).to receive(:exist?).with('mydockerfile').and_return(true)
|
251
|
+
allow( ::Docker::Image ).to receive(:build_from_dir).with("/", hash_including(:rm => true, :buildargs => 'buildargs')).and_return(image)
|
252
|
+
expect( docker ).not_to receive(:dockerfile_for)
|
253
|
+
expect( docker ).to receive(:install_ssh_components).exactly(3).times #once per host
|
254
|
+
expect( docker ).to receive(:fix_ssh).exactly(3).times #once per host
|
255
|
+
docker.provision
|
235
256
|
end
|
236
257
|
end
|
237
258
|
|
238
|
-
it 'should
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
259
|
+
it 'should call image create for hosts when use_image_as_is is defined' do
|
260
|
+
hosts.each do |host|
|
261
|
+
host['use_image_as_is'] = true
|
262
|
+
expect( docker ).not_to receive(:install_ssh_components)
|
263
|
+
expect( docker ).not_to receive(:fix_ssh)
|
264
|
+
expect( ::Docker::Image ).to receive(:create).with('fromImage' => host['image']) #once per host
|
265
|
+
expect( ::Docker::Image ).not_to receive(:build)
|
266
|
+
expect( ::Docker::Image ).not_to receive(:build_from_dir)
|
267
|
+
end
|
247
268
|
|
248
|
-
|
249
|
-
hosts.each do |host|
|
250
|
-
host['use_image_as_is'] = true
|
251
|
-
expect( docker ).not_to receive(:install_ssh_components)
|
252
|
-
expect( docker ).not_to receive(:fix_ssh)
|
253
|
-
expect( ::Docker::Image ).to receive(:create).with('fromImage' => host['image']) #once per host
|
254
|
-
expect( ::Docker::Image ).not_to receive(:build)
|
255
|
-
expect( ::Docker::Image ).not_to receive(:build_from_dir)
|
269
|
+
docker.provision
|
256
270
|
end
|
257
271
|
|
258
|
-
|
259
|
-
|
272
|
+
it 'should call dockerfile_for with all the hosts' do
|
273
|
+
hosts.each do |host|
|
274
|
+
expect( docker ).not_to receive(:install_ssh_components)
|
275
|
+
expect( docker ).not_to receive(:fix_ssh)
|
276
|
+
expect( docker ).to receive(:dockerfile_for).with(host).and_return('')
|
277
|
+
end
|
260
278
|
|
261
|
-
|
262
|
-
hosts.each do |host|
|
263
|
-
expect( docker ).not_to receive(:install_ssh_components)
|
264
|
-
expect( docker ).not_to receive(:fix_ssh)
|
265
|
-
expect( docker ).to receive(:dockerfile_for).with(host).and_return('')
|
279
|
+
docker.provision
|
266
280
|
end
|
267
281
|
|
268
|
-
|
269
|
-
|
282
|
+
it 'should pass the Dockerfile on to Docker::Image.create' do
|
283
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
284
|
+
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => '{}' })
|
270
285
|
|
271
|
-
|
272
|
-
|
273
|
-
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => '{}' })
|
274
|
-
|
275
|
-
docker.provision
|
276
|
-
end
|
286
|
+
docker.provision
|
287
|
+
end
|
277
288
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
289
|
+
it 'should pass the buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do
|
290
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
291
|
+
ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128'
|
292
|
+
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => "{\"HTTP_PROXY\":\"http://1.1.1.1:3128\"}" })
|
282
293
|
|
283
|
-
|
284
|
-
|
294
|
+
docker.provision
|
295
|
+
end
|
285
296
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
297
|
+
it 'should create a container based on the Image (identified by image.id)' do
|
298
|
+
hosts.each_with_index do |host,index|
|
299
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
300
|
+
'Image' => image.id,
|
301
|
+
'Hostname' => host.name,
|
302
|
+
'HostConfig' => {
|
303
|
+
'PortBindings' => {
|
304
|
+
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
305
|
+
},
|
306
|
+
'Privileged' => true,
|
307
|
+
'PublishAllPorts' => true,
|
308
|
+
'RestartPolicy' => {
|
309
|
+
'Name' => 'always'
|
310
|
+
}
|
294
311
|
},
|
295
|
-
'
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
}
|
301
|
-
|
302
|
-
'one' => 1,
|
303
|
-
'two' => 2,
|
304
|
-
},
|
305
|
-
}).with(hash_excluding('name'))
|
306
|
-
end
|
312
|
+
'Labels' => {
|
313
|
+
'one' => (index == 2 ? 3 : 1),
|
314
|
+
'two' => (index == 2 ? 4 : 2),
|
315
|
+
},
|
316
|
+
'name' => /\Abeaker-/
|
317
|
+
})
|
318
|
+
end
|
307
319
|
|
308
|
-
|
309
|
-
|
320
|
+
docker.provision
|
321
|
+
end
|
310
322
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
323
|
+
it 'should pass the multiple buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do
|
324
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
325
|
+
ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128 HTTPS_PROXY=https://1.1.1.1:3129'
|
326
|
+
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => "{\"HTTP_PROXY\":\"http://1.1.1.1:3128\",\"HTTPS_PROXY\":\"https://1.1.1.1:3129\"}" })
|
315
327
|
|
316
|
-
|
317
|
-
|
328
|
+
docker.provision
|
329
|
+
end
|
318
330
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
331
|
+
it 'should create a container based on the Image (identified by image.id)' do
|
332
|
+
hosts.each_with_index do |host,index|
|
333
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
334
|
+
'Image' => image.id,
|
335
|
+
'Hostname' => host.name,
|
336
|
+
'HostConfig' => {
|
337
|
+
'PortBindings' => {
|
338
|
+
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
339
|
+
},
|
340
|
+
'PublishAllPorts' => true,
|
341
|
+
'Privileged' => true,
|
342
|
+
'RestartPolicy' => {
|
343
|
+
'Name' => 'always'
|
344
|
+
}
|
327
345
|
},
|
328
|
-
'
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
}
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
},
|
338
|
-
}).with(hash_excluding('name'))
|
346
|
+
'Labels' => {
|
347
|
+
'one' => (index == 2 ? 3 : 1),
|
348
|
+
'two' => (index == 2 ? 4 : 2),
|
349
|
+
},
|
350
|
+
'name' => /\Abeaker-/
|
351
|
+
})
|
352
|
+
end
|
353
|
+
|
354
|
+
docker.provision
|
339
355
|
end
|
340
356
|
|
341
|
-
|
342
|
-
|
357
|
+
it 'should create a named container based on the Image (identified by image.id)' do
|
358
|
+
hosts.each_with_index do |host, index|
|
359
|
+
container_name = "spec-container-#{index}"
|
360
|
+
host['docker_container_name'] = container_name
|
343
361
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
362
|
+
allow(::Docker::Container).to receive(:all).and_return([])
|
363
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
364
|
+
'Image' => image.id,
|
365
|
+
'Hostname' => host.name,
|
366
|
+
'name' => container_name,
|
367
|
+
'HostConfig' => {
|
368
|
+
'PortBindings' => {
|
369
|
+
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
370
|
+
},
|
371
|
+
'PublishAllPorts' => true,
|
372
|
+
'Privileged' => true,
|
373
|
+
'RestartPolicy' => {
|
374
|
+
'Name' => 'always'
|
375
|
+
}
|
357
376
|
},
|
358
|
-
'
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
'Labels' => {
|
365
|
-
'one' => (index == 2 ? 3 : 1),
|
366
|
-
'two' => (index == 2 ? 4 : 2),
|
367
|
-
},
|
368
|
-
})
|
369
|
-
end
|
377
|
+
'Labels' => {
|
378
|
+
'one' => (index == 2 ? 3 : 1),
|
379
|
+
'two' => (index == 2 ? 4 : 2),
|
380
|
+
},
|
381
|
+
})
|
382
|
+
end
|
370
383
|
|
371
|
-
|
372
|
-
|
384
|
+
docker.provision
|
385
|
+
end
|
373
386
|
|
374
387
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
},
|
387
|
-
'mount3' => {
|
388
|
-
'host_path' => '/different_folder',
|
389
|
-
'container_path' => '/different_mount',
|
390
|
-
'opts' => 'rw',
|
391
|
-
},
|
392
|
-
'mount4' => {
|
393
|
-
'host_path' => './',
|
394
|
-
'container_path' => '/relative_mount',
|
395
|
-
},
|
396
|
-
'mount5' => {
|
397
|
-
'host_path' => 'local_folder',
|
398
|
-
'container_path' => '/another_relative_mount',
|
399
|
-
}
|
400
|
-
}
|
401
|
-
|
402
|
-
expect( ::Docker::Container ).to receive(:create).with({
|
403
|
-
'Image' => image.id,
|
404
|
-
'Hostname' => host.name,
|
405
|
-
'HostConfig' => {
|
406
|
-
'Binds' => [
|
407
|
-
'/source_folder:/mount_point',
|
408
|
-
'/another_folder:/another_mount:ro',
|
409
|
-
'/different_folder:/different_mount:rw',
|
410
|
-
"#{File.expand_path('./')}:/relative_mount",
|
411
|
-
"#{File.expand_path('local_folder')}:/another_relative_mount",
|
412
|
-
],
|
413
|
-
'PortBindings' => {
|
414
|
-
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
388
|
+
it 'should create a container with volumes bound' do
|
389
|
+
hosts.each_with_index do |host, index|
|
390
|
+
host['mount_folders'] = {
|
391
|
+
'mount1' => {
|
392
|
+
'host_path' => '/source_folder',
|
393
|
+
'container_path' => '/mount_point',
|
394
|
+
},
|
395
|
+
'mount2' => {
|
396
|
+
'host_path' => '/another_folder',
|
397
|
+
'container_path' => '/another_mount',
|
398
|
+
'opts' => 'ro',
|
415
399
|
},
|
416
|
-
'
|
417
|
-
|
418
|
-
|
419
|
-
'
|
400
|
+
'mount3' => {
|
401
|
+
'host_path' => '/different_folder',
|
402
|
+
'container_path' => '/different_mount',
|
403
|
+
'opts' => 'rw',
|
404
|
+
},
|
405
|
+
'mount4' => {
|
406
|
+
'host_path' => './',
|
407
|
+
'container_path' => '/relative_mount',
|
408
|
+
},
|
409
|
+
'mount5' => {
|
410
|
+
'host_path' => 'local_folder',
|
411
|
+
'container_path' => '/another_relative_mount',
|
420
412
|
}
|
421
|
-
}
|
422
|
-
'Labels' => {
|
423
|
-
'one' => (index == 2 ? 3 : 1),
|
424
|
-
'two' => (index == 2 ? 4 : 2),
|
425
|
-
},
|
426
|
-
})
|
427
|
-
end
|
413
|
+
}
|
428
414
|
|
429
|
-
|
430
|
-
|
415
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
416
|
+
'Image' => image.id,
|
417
|
+
'Hostname' => host.name,
|
418
|
+
'HostConfig' => {
|
419
|
+
'Binds' => [
|
420
|
+
'/source_folder:/mount_point:z',
|
421
|
+
'/another_folder:/another_mount:ro',
|
422
|
+
'/different_folder:/different_mount:rw',
|
423
|
+
"#{File.expand_path('./')}:/relative_mount:z",
|
424
|
+
"#{File.expand_path('local_folder')}:/another_relative_mount:z",
|
425
|
+
],
|
426
|
+
'PortBindings' => {
|
427
|
+
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
428
|
+
},
|
429
|
+
'PublishAllPorts' => true,
|
430
|
+
'Privileged' => true,
|
431
|
+
'RestartPolicy' => {
|
432
|
+
'Name' => 'always'
|
433
|
+
}
|
434
|
+
},
|
435
|
+
'Labels' => {
|
436
|
+
'one' => (index == 2 ? 3 : 1),
|
437
|
+
'two' => (index == 2 ? 4 : 2),
|
438
|
+
},
|
439
|
+
'name' => /\Abeaker-/
|
440
|
+
})
|
441
|
+
end
|
431
442
|
|
432
|
-
|
433
|
-
|
434
|
-
host['docker_cap_add'] = ['NET_ADMIN', 'SYS_ADMIN']
|
443
|
+
docker.provision
|
444
|
+
end
|
435
445
|
|
436
|
-
|
437
|
-
|
438
|
-
'
|
439
|
-
|
440
|
-
|
441
|
-
|
446
|
+
it 'should create a container with capabilities added' do
|
447
|
+
hosts.each_with_index do |host, index|
|
448
|
+
host['docker_cap_add'] = ['NET_ADMIN', 'SYS_ADMIN']
|
449
|
+
|
450
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
451
|
+
'Image' => image.id,
|
452
|
+
'Hostname' => host.name,
|
453
|
+
'HostConfig' => {
|
454
|
+
'PortBindings' => {
|
455
|
+
'22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}]
|
456
|
+
},
|
457
|
+
'PublishAllPorts' => true,
|
458
|
+
'RestartPolicy' => {
|
459
|
+
'Name' => 'always'
|
460
|
+
},
|
461
|
+
'CapAdd' => ['NET_ADMIN', 'SYS_ADMIN']
|
442
462
|
},
|
443
|
-
'
|
444
|
-
|
445
|
-
|
446
|
-
'Name' => 'always'
|
463
|
+
'Labels' => {
|
464
|
+
'one' => (index == 2 ? 3 : 1),
|
465
|
+
'two' => (index == 2 ? 4 : 2),
|
447
466
|
},
|
448
|
-
'
|
449
|
-
}
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
},
|
454
|
-
})
|
467
|
+
'name' => /\Abeaker-/
|
468
|
+
})
|
469
|
+
end
|
470
|
+
|
471
|
+
docker.provision
|
455
472
|
end
|
456
473
|
|
457
|
-
|
458
|
-
|
474
|
+
it 'should start the container' do
|
475
|
+
expect( container ).to receive(:start)
|
459
476
|
|
460
|
-
|
461
|
-
|
477
|
+
docker.provision
|
478
|
+
end
|
462
479
|
|
463
|
-
|
464
|
-
|
480
|
+
context "connecting to ssh" do
|
481
|
+
context "rootless" do
|
482
|
+
before { @docker_host = ENV['DOCKER_HOST'] }
|
483
|
+
after { ENV['DOCKER_HOST'] = @docker_host }
|
484
|
+
|
485
|
+
it 'should expose port 22 to beaker' do
|
486
|
+
ENV['DOCKER_HOST'] = nil
|
487
|
+
docker.provision
|
488
|
+
|
489
|
+
expect( hosts[0]['ip'] ).to be === '127.0.1.1'
|
490
|
+
expect( hosts[0]['port'] ).to be === 8022
|
491
|
+
end
|
492
|
+
|
493
|
+
it 'should expose port 22 to beaker when using DOCKER_HOST' do
|
494
|
+
ENV['DOCKER_HOST'] = "tcp://192.0.2.2:2375"
|
495
|
+
docker.provision
|
496
|
+
|
497
|
+
expect( hosts[0]['ip'] ).to be === '192.0.2.2'
|
498
|
+
expect( hosts[0]['port'] ).to be === 8022
|
499
|
+
end
|
500
|
+
|
501
|
+
it 'should have ssh agent forwarding enabled' do
|
502
|
+
ENV['DOCKER_HOST'] = nil
|
503
|
+
docker.provision
|
504
|
+
|
505
|
+
expect( hosts[0]['ip'] ).to be === '127.0.1.1'
|
506
|
+
expect( hosts[0]['port'] ).to be === 8022
|
507
|
+
expect( hosts[0]['ssh'][:password] ).to be === 'root'
|
508
|
+
expect( hosts[0]['ssh'][:port] ).to be === 8022
|
509
|
+
expect( hosts[0]['ssh'][:forward_agent] ).to be === true
|
510
|
+
end
|
511
|
+
|
512
|
+
it 'should connect to gateway ip' do
|
513
|
+
FakeFS do
|
514
|
+
File.open('/.dockerenv', 'w') { }
|
515
|
+
docker.provision
|
516
|
+
|
517
|
+
expect( hosts[0]['ip'] ).to be === '192.0.2.254'
|
518
|
+
expect( hosts[0]['port'] ).to be === 8022
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
465
522
|
|
466
|
-
|
467
|
-
|
468
|
-
|
523
|
+
context 'rootful' do
|
524
|
+
before { @docker_host = ENV['DOCKER_HOST'] }
|
525
|
+
after { ENV['DOCKER_HOST'] = @docker_host }
|
469
526
|
|
470
|
-
|
471
|
-
|
472
|
-
|
527
|
+
let(:container_mode) do
|
528
|
+
'rootful'
|
529
|
+
end
|
473
530
|
|
474
|
-
|
475
|
-
|
476
|
-
|
531
|
+
it 'should expose port 22 to beaker' do
|
532
|
+
ENV['DOCKER_HOST'] = nil
|
533
|
+
docker.provision
|
477
534
|
|
478
|
-
|
479
|
-
|
480
|
-
|
535
|
+
expect( hosts[0]['ip'] ).to be === '192.0.2.1'
|
536
|
+
expect( hosts[0]['port'] ).to be === '22'
|
537
|
+
end
|
538
|
+
end
|
481
539
|
|
482
|
-
expect( hosts[0]['ip'] ).to be === '192.0.2.2'
|
483
|
-
expect( hosts[0]['port'] ).to be === 8022
|
484
540
|
end
|
485
541
|
|
486
|
-
it
|
542
|
+
it "should generate a new /etc/hosts file referencing each host" do
|
487
543
|
ENV['DOCKER_HOST'] = nil
|
488
544
|
docker.provision
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
expect( hosts[0]['ssh'][:password] ).to be === 'root'
|
493
|
-
expect( hosts[0]['ssh'][:port] ).to be === 8022
|
494
|
-
expect( hosts[0]['ssh'][:forward_agent] ).to be === true
|
495
|
-
end
|
496
|
-
|
497
|
-
it 'should connect to gateway ip' do
|
498
|
-
FakeFS do
|
499
|
-
File.open('/.dockerenv', 'w') { }
|
500
|
-
docker.provision
|
501
|
-
|
502
|
-
expect( hosts[0]['ip'] ).to be === '192.0.2.254'
|
503
|
-
expect( hosts[0]['port'] ).to be === 8022
|
545
|
+
hosts.each do |host|
|
546
|
+
expect( docker ).to receive( :get_domain_name ).with( host ).and_return( 'labs.lan' )
|
547
|
+
expect( docker ).to receive( :set_etc_hosts ).with( host, "127.0.0.1\tlocalhost localhost.localdomain\n192.0.2.1\tvm1.labs.lan vm1\n192.0.2.1\tvm2.labs.lan vm2\n192.0.2.1\tvm3.labs.lan vm3\n" ).once
|
504
548
|
end
|
549
|
+
docker.hack_etc_hosts( hosts, options )
|
505
550
|
end
|
506
551
|
|
507
|
-
|
552
|
+
it 'should record the image and container for later' do
|
553
|
+
docker.provision
|
508
554
|
|
509
|
-
|
510
|
-
|
511
|
-
docker.provision
|
512
|
-
hosts.each do |host|
|
513
|
-
expect( docker ).to receive( :get_domain_name ).with( host ).and_return( 'labs.lan' )
|
514
|
-
expect( docker ).to receive( :set_etc_hosts ).with( host, "127.0.0.1\tlocalhost localhost.localdomain\n192.0.2.1\tvm1.labs.lan vm1\n192.0.2.1\tvm2.labs.lan vm2\n192.0.2.1\tvm3.labs.lan vm3\n" ).once
|
555
|
+
expect( hosts[0]['docker_image_id'] ).to be === image.id
|
556
|
+
expect( hosts[0]['docker_container_id'] ).to be === container.id
|
515
557
|
end
|
516
|
-
docker.hack_etc_hosts( hosts, options )
|
517
|
-
end
|
518
|
-
|
519
|
-
it 'should record the image and container for later' do
|
520
|
-
docker.provision
|
521
558
|
|
522
|
-
|
523
|
-
|
524
|
-
|
559
|
+
context 'provision=false' do
|
560
|
+
let(:options) {{
|
561
|
+
:logger => logger,
|
562
|
+
:forward_ssh_agent => true,
|
563
|
+
:provision => false
|
564
|
+
}}
|
525
565
|
|
526
|
-
context 'provision=false' do
|
527
|
-
let(:options) {{
|
528
|
-
:logger => logger,
|
529
|
-
:forward_ssh_agent => true,
|
530
|
-
:provision => false
|
531
|
-
}}
|
532
566
|
|
567
|
+
it 'should fix ssh' do
|
568
|
+
hosts.each_with_index do |host, index|
|
569
|
+
container_name = "spec-container-#{index}"
|
570
|
+
host['docker_container_name'] = container_name
|
533
571
|
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
expect( ::Docker::Container ).to receive(:all).and_return([container])
|
540
|
-
expect(docker).to receive(:fix_ssh).exactly(1).times
|
572
|
+
expect( ::Docker::Container ).to receive(:all).and_return([container])
|
573
|
+
expect(docker).to receive(:fix_ssh).exactly(1).times
|
574
|
+
end
|
575
|
+
docker.provision
|
541
576
|
end
|
542
|
-
docker.provision
|
543
|
-
end
|
544
577
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
578
|
+
it 'should not create a container if a named one already exists' do
|
579
|
+
hosts.each_with_index do |host, index|
|
580
|
+
container_name = "spec-container-#{index}"
|
581
|
+
host['docker_container_name'] = container_name
|
549
582
|
|
550
|
-
|
551
|
-
|
552
|
-
|
583
|
+
expect( ::Docker::Container ).to receive(:all).and_return([container])
|
584
|
+
expect( ::Docker::Container ).not_to receive(:create)
|
585
|
+
end
|
553
586
|
|
554
|
-
|
587
|
+
docker.provision
|
588
|
+
end
|
555
589
|
end
|
556
590
|
end
|
557
|
-
end
|
558
591
|
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
end
|
568
|
-
|
569
|
-
it 'should stop the containers' do
|
570
|
-
allow( docker ).to receive( :sleep ).and_return(true)
|
571
|
-
expect( container ).to receive(:kill)
|
572
|
-
docker.cleanup
|
573
|
-
end
|
592
|
+
describe '#cleanup' do
|
593
|
+
before :each do
|
594
|
+
# get into a state where there's something to clean
|
595
|
+
allow( ::Docker::Container ).to receive(:all).and_return([container])
|
596
|
+
allow( ::Docker::Image ).to receive(:remove).with(image.id)
|
597
|
+
allow( docker ).to receive(:dockerfile_for)
|
598
|
+
docker.provision
|
599
|
+
end
|
574
600
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
601
|
+
it 'should stop the containers' do
|
602
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
603
|
+
expect( container ).to receive(:kill)
|
604
|
+
docker.cleanup
|
605
|
+
end
|
580
606
|
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
607
|
+
it 'should delete the containers' do
|
608
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
609
|
+
expect( container ).to receive(:delete)
|
610
|
+
docker.cleanup
|
611
|
+
end
|
586
612
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
613
|
+
it 'should delete the images' do
|
614
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
615
|
+
expect( ::Docker::Image ).to receive(:remove).with(image.id)
|
616
|
+
docker.cleanup
|
591
617
|
end
|
592
|
-
expect( ::Docker::Image ).to_not receive(:remove)
|
593
|
-
docker.cleanup
|
594
|
-
end
|
595
618
|
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
619
|
+
it 'should not delete the image if docker_preserve_image is set to true' do
|
620
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
621
|
+
hosts.each do |host|
|
622
|
+
host['docker_preserve_image']=true
|
623
|
+
end
|
624
|
+
expect( ::Docker::Image ).to_not receive(:remove)
|
625
|
+
docker.cleanup
|
600
626
|
end
|
601
|
-
expect( ::Docker::Image ).to receive(:remove).with(image.id)
|
602
|
-
docker.cleanup
|
603
|
-
end
|
604
627
|
|
605
|
-
|
628
|
+
it 'should delete the image if docker_preserve_image is set to false' do
|
629
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
630
|
+
hosts.each do |host|
|
631
|
+
host['docker_preserve_image']=false
|
632
|
+
end
|
633
|
+
expect( ::Docker::Image ).to receive(:remove).with(image.id)
|
634
|
+
docker.cleanup
|
635
|
+
end
|
606
636
|
|
607
|
-
describe '#dockerfile_for' do
|
608
|
-
FakeFS.deactivate!
|
609
|
-
before :each do
|
610
|
-
allow( ::Docker ).to receive(:validate_version!)
|
611
|
-
end
|
612
|
-
it 'should raise on an unsupported platform' do
|
613
|
-
expect { docker.send(:dockerfile_for, {'platform' => 'a_sidewalk', 'image' => 'foobar' }) }.to raise_error(/platform a_sidewalk not yet supported/)
|
614
637
|
end
|
615
638
|
|
616
|
-
|
639
|
+
describe '#dockerfile_for' do
|
617
640
|
FakeFS.deactivate!
|
618
|
-
|
619
|
-
|
620
|
-
'platform' => platform,
|
621
|
-
'image' => 'foobar',
|
622
|
-
})
|
623
|
-
expect( dockerfile ).to be =~ /ENV container docker/
|
641
|
+
it 'should raise on an unsupported platform' do
|
642
|
+
expect { docker.send(:dockerfile_for, {'platform' => 'a_sidewalk', 'image' => 'foobar' }) }.to raise_error(/platform a_sidewalk not yet supported/)
|
624
643
|
end
|
625
|
-
end
|
626
644
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
]
|
638
|
-
})
|
645
|
+
it 'should set "ENV container docker"' do
|
646
|
+
FakeFS.deactivate!
|
647
|
+
platforms.each do |platform|
|
648
|
+
dockerfile = docker.send(:dockerfile_for, {
|
649
|
+
'platform' => platform,
|
650
|
+
'image' => 'foobar',
|
651
|
+
})
|
652
|
+
expect( dockerfile ).to be =~ /ENV container docker/
|
653
|
+
end
|
654
|
+
end
|
639
655
|
|
640
|
-
|
656
|
+
it 'should add docker_image_commands as RUN statements' do
|
657
|
+
FakeFS.deactivate!
|
658
|
+
platforms.each do |platform|
|
659
|
+
dockerfile = docker.send(:dockerfile_for, {
|
660
|
+
'platform' => platform,
|
661
|
+
'image' => 'foobar',
|
662
|
+
'docker_image_commands' => [
|
663
|
+
'special one',
|
664
|
+
'special two',
|
665
|
+
'special three',
|
666
|
+
]
|
667
|
+
})
|
668
|
+
|
669
|
+
expect( dockerfile ).to be =~ /RUN special one\nRUN special two\nRUN special three/
|
670
|
+
end
|
641
671
|
end
|
642
|
-
end
|
643
672
|
|
644
|
-
|
645
|
-
|
646
|
-
|
673
|
+
it 'should add docker_image_entrypoint' do
|
674
|
+
FakeFS.deactivate!
|
675
|
+
platforms.each do |platform|
|
676
|
+
dockerfile = docker.send(:dockerfile_for, {
|
677
|
+
'platform' => platform,
|
678
|
+
'image' => 'foobar',
|
679
|
+
'docker_image_entrypoint' => '/bin/bash'
|
680
|
+
})
|
681
|
+
|
682
|
+
expect( dockerfile ).to be =~ %r{ENTRYPOINT /bin/bash}
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
it 'should use zypper on sles' do
|
687
|
+
FakeFS.deactivate!
|
647
688
|
dockerfile = docker.send(:dockerfile_for, {
|
648
|
-
'platform' =>
|
689
|
+
'platform' => 'sles-12-x86_64',
|
649
690
|
'image' => 'foobar',
|
650
|
-
'docker_image_entrypoint' => '/bin/bash'
|
651
691
|
})
|
652
692
|
|
653
|
-
expect( dockerfile ).to be =~
|
693
|
+
expect( dockerfile ).to be =~ /RUN zypper -n in openssh/
|
654
694
|
end
|
655
|
-
end
|
656
695
|
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
696
|
+
(22..29).to_a.each do | fedora_release |
|
697
|
+
it "should use dnf on fedora #{fedora_release}" do
|
698
|
+
FakeFS.deactivate!
|
699
|
+
dockerfile = docker.send(:dockerfile_for, {
|
700
|
+
'platform' => "fedora-#{fedora_release}-x86_64",
|
701
|
+
'image' => 'foobar',
|
702
|
+
})
|
663
703
|
|
664
|
-
|
665
|
-
|
704
|
+
expect( dockerfile ).to be =~ /RUN dnf install -y sudo/
|
705
|
+
end
|
706
|
+
end
|
666
707
|
|
667
|
-
|
668
|
-
it "should use dnf on fedora #{fedora_release}" do
|
708
|
+
it 'should use pacman on archlinux' do
|
669
709
|
FakeFS.deactivate!
|
670
710
|
dockerfile = docker.send(:dockerfile_for, {
|
671
|
-
'platform' =>
|
711
|
+
'platform' => 'archlinux-current-x86_64',
|
672
712
|
'image' => 'foobar',
|
673
713
|
})
|
674
714
|
|
675
|
-
expect( dockerfile ).to be =~ /RUN
|
715
|
+
expect( dockerfile ).to be =~ /RUN pacman -S --noconfirm openssh/
|
676
716
|
end
|
677
717
|
end
|
678
|
-
|
679
|
-
it 'should use pacman on archlinux' do
|
680
|
-
FakeFS.deactivate!
|
681
|
-
dockerfile = docker.send(:dockerfile_for, {
|
682
|
-
'platform' => 'archlinux-current-x86_64',
|
683
|
-
'image' => 'foobar',
|
684
|
-
})
|
685
|
-
|
686
|
-
expect( dockerfile ).to be =~ /RUN pacman -S --noconfirm openssh/
|
687
|
-
end
|
688
718
|
end
|
689
719
|
end
|
690
720
|
end
|