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