beaker-docker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +25 -0
- data/.simplecov +9 -0
- data/Gemfile +25 -0
- data/LICENSE +202 -0
- data/README.md +36 -0
- data/Rakefile +161 -0
- data/acceptance/config/nodes/hosts.yaml +25 -0
- data/beaker-docker.gemspec +38 -0
- data/bin/beaker-docker +32 -0
- data/docker.md +148 -0
- data/lib/beaker-docker/version.rb +3 -0
- data/lib/beaker/hypervisor/docker.rb +336 -0
- data/spec/beaker/hypervisor/docker_spec.rb +491 -0
- data/spec/spec_helper.rb +17 -0
- metadata +213 -0
@@ -0,0 +1,491 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fakefs/spec_helpers'
|
3
|
+
|
4
|
+
# fake the docker-api
|
5
|
+
module Docker
|
6
|
+
class Image
|
7
|
+
end
|
8
|
+
class Container
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Beaker
|
13
|
+
platforms = [
|
14
|
+
"ubuntu-14.04-x86_64",
|
15
|
+
"cumulus-2.2-x86_64",
|
16
|
+
"fedora-22-x86_64",
|
17
|
+
"centos-7-x86_64",
|
18
|
+
"sles-12-x86_64"
|
19
|
+
]
|
20
|
+
|
21
|
+
describe Docker do
|
22
|
+
let(:hosts) { make_hosts }
|
23
|
+
|
24
|
+
let(:logger) do
|
25
|
+
logger = double('logger')
|
26
|
+
allow( logger ).to receive(:debug)
|
27
|
+
allow( logger ).to receive(:info)
|
28
|
+
allow( logger ).to receive(:warn)
|
29
|
+
allow( logger ).to receive(:error)
|
30
|
+
allow( logger ).to receive(:notify)
|
31
|
+
logger
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:options) {{
|
35
|
+
:logger => logger,
|
36
|
+
:forward_ssh_agent => true,
|
37
|
+
:provision => true
|
38
|
+
}}
|
39
|
+
|
40
|
+
let(:image) do
|
41
|
+
image = double('Docker::Image')
|
42
|
+
allow( image ).to receive(:id)
|
43
|
+
allow( image ).to receive(:tag)
|
44
|
+
allow( image ).to receive(:delete)
|
45
|
+
image
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:container) do
|
49
|
+
container = double('Docker::Container')
|
50
|
+
allow( container ).to receive(:id)
|
51
|
+
allow( container ).to receive(:start)
|
52
|
+
allow( container ).to receive(:info).and_return(
|
53
|
+
*(0..2).map { |index| { 'Names' => ["/spec-container-#{index}"] } }
|
54
|
+
)
|
55
|
+
allow( container ).to receive(:json).and_return({
|
56
|
+
'NetworkSettings' => {
|
57
|
+
'IPAddress' => '192.0.2.1',
|
58
|
+
'Ports' => {
|
59
|
+
'22/tcp' => [
|
60
|
+
{
|
61
|
+
'HostIp' => '127.0.1.1',
|
62
|
+
'HostPort' => 8022,
|
63
|
+
},
|
64
|
+
],
|
65
|
+
},
|
66
|
+
},
|
67
|
+
})
|
68
|
+
allow( container ).to receive(:kill)
|
69
|
+
allow( container ).to receive(:delete)
|
70
|
+
allow( container ).to receive(:exec)
|
71
|
+
container
|
72
|
+
end
|
73
|
+
|
74
|
+
let (:docker) { ::Beaker::Docker.new( hosts, options ) }
|
75
|
+
let(:docker_options) { nil }
|
76
|
+
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"} }
|
77
|
+
|
78
|
+
before :each do
|
79
|
+
# Stub out all of the docker-api gem. we should never really call it
|
80
|
+
# from these tests
|
81
|
+
allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker')
|
82
|
+
allow( ::Docker ).to receive(:options).and_return(docker_options)
|
83
|
+
allow( ::Docker ).to receive(:options=)
|
84
|
+
allow( ::Docker ).to receive(:logger=)
|
85
|
+
allow( ::Docker ).to receive(:version).and_return(version)
|
86
|
+
allow( ::Docker::Image ).to receive(:build).and_return(image)
|
87
|
+
allow( ::Docker::Container ).to receive(:create).and_return(container)
|
88
|
+
allow_any_instance_of( ::Docker::Container ).to receive(:start)
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#initialize, failure to validate' do
|
92
|
+
before :each do
|
93
|
+
require 'excon'
|
94
|
+
allow( ::Docker ).to receive(:validate_version!).and_raise(Excon::Errors::SocketError.new( StandardError.new('oops') ))
|
95
|
+
end
|
96
|
+
it 'should fail when docker not present' do
|
97
|
+
expect { docker }.to raise_error(RuntimeError, /Docker instance not connectable./)
|
98
|
+
expect { docker }.to raise_error(RuntimeError, /Check your DOCKER_HOST variable has been set/)
|
99
|
+
expect { docker }.to raise_error(RuntimeError, /If you are on OSX or Windows, you might not have Docker Machine setup correctly/)
|
100
|
+
expect { docker }.to raise_error(RuntimeError, /Error was: oops/)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#initialize' do
|
105
|
+
before :each do
|
106
|
+
allow( ::Docker ).to receive(:validate_version!)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should require the docker gem' do
|
110
|
+
expect_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').once
|
111
|
+
|
112
|
+
docker
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should fail when the gem is absent' do
|
116
|
+
allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').and_raise(LoadError)
|
117
|
+
expect { docker }.to raise_error(LoadError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should set Docker options' do
|
121
|
+
expect( ::Docker ).to receive(:options=).with({:write_timeout => 300, :read_timeout => 300}).once
|
122
|
+
|
123
|
+
docker
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'when Docker options are already set' do
|
127
|
+
let(:docker_options) {{:write_timeout => 600, :foo => :bar}}
|
128
|
+
|
129
|
+
it 'should not override Docker options' do
|
130
|
+
expect( ::Docker ).to receive(:options=).with({:write_timeout => 600, :read_timeout => 300, :foo => :bar}).once
|
131
|
+
|
132
|
+
docker
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should check the Docker gem can work with the api' do
|
137
|
+
expect( ::Docker ).to receive(:validate_version!).once
|
138
|
+
|
139
|
+
docker
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should hook the Beaker logger into the Docker one' do
|
143
|
+
expect( ::Docker ).to receive(:logger=).with(logger)
|
144
|
+
|
145
|
+
docker
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '#provision' do
|
150
|
+
before :each do
|
151
|
+
allow( ::Docker ).to receive(:validate_version!)
|
152
|
+
allow( docker ).to receive(:dockerfile_for)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should call dockerfile_for with all the hosts' do
|
156
|
+
hosts.each do |host|
|
157
|
+
expect( docker ).to receive(:dockerfile_for).with(host).and_return('')
|
158
|
+
end
|
159
|
+
|
160
|
+
docker.provision
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should pass the Dockerfile on to Docker::Image.create' do
|
164
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
165
|
+
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => '{}' })
|
166
|
+
|
167
|
+
docker.provision
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should pass the buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do
|
171
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
172
|
+
ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128'
|
173
|
+
expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => "{\"HTTP_PROXY\":\"http://1.1.1.1:3128\"}" })
|
174
|
+
|
175
|
+
docker.provision
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should create a container based on the Image (identified by image.id)' do
|
179
|
+
hosts.each do |host|
|
180
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
181
|
+
'Image' => image.id,
|
182
|
+
'Hostname' => host.name,
|
183
|
+
})
|
184
|
+
end
|
185
|
+
|
186
|
+
docker.provision
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'should pass the multiple buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do
|
190
|
+
allow( docker ).to receive(:dockerfile_for).and_return('special testing value')
|
191
|
+
ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128 HTTPS_PROXY=https://1.1.1.1:3129'
|
192
|
+
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\"}" })
|
193
|
+
|
194
|
+
docker.provision
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should create a container based on the Image (identified by image.id)' do
|
198
|
+
hosts.each do |host|
|
199
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
200
|
+
'Image' => image.id,
|
201
|
+
'Hostname' => host.name,
|
202
|
+
})
|
203
|
+
end
|
204
|
+
|
205
|
+
docker.provision
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'should create a named container based on the Image (identified by image.id)' do
|
209
|
+
hosts.each_with_index do |host, index|
|
210
|
+
container_name = "spec-container-#{index}"
|
211
|
+
host['docker_container_name'] = container_name
|
212
|
+
|
213
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
214
|
+
'Image' => image.id,
|
215
|
+
'Hostname' => host.name,
|
216
|
+
'name' => container_name,
|
217
|
+
})
|
218
|
+
end
|
219
|
+
|
220
|
+
docker.provision
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
it 'should create a container with volumes bound' do
|
225
|
+
hosts.each_with_index do |host, index|
|
226
|
+
host['mount_folders'] = {
|
227
|
+
'mount1' => {
|
228
|
+
'host_path' => '/source_folder',
|
229
|
+
'container_path' => '/mount_point',
|
230
|
+
},
|
231
|
+
'mount2' => {
|
232
|
+
'host_path' => '/another_folder',
|
233
|
+
'container_path' => '/another_mount',
|
234
|
+
'opts' => 'ro',
|
235
|
+
},
|
236
|
+
'mount3' => {
|
237
|
+
'host_path' => '/different_folder',
|
238
|
+
'container_path' => '/different_mount',
|
239
|
+
'opts' => 'rw',
|
240
|
+
},
|
241
|
+
'mount4' => {
|
242
|
+
'host_path' => './',
|
243
|
+
'container_path' => '/relative_mount',
|
244
|
+
},
|
245
|
+
'mount5' => {
|
246
|
+
'host_path' => 'local_folder',
|
247
|
+
'container_path' => '/another_relative_mount',
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
expect( ::Docker::Container ).to receive(:create).with({
|
252
|
+
'Image' => image.id,
|
253
|
+
'Hostname' => host.name,
|
254
|
+
'HostConfig' => {
|
255
|
+
'Binds' => [
|
256
|
+
'/source_folder:/mount_point',
|
257
|
+
'/another_folder:/another_mount:ro',
|
258
|
+
'/different_folder:/different_mount:rw',
|
259
|
+
"#{File.expand_path('./')}:/relative_mount",
|
260
|
+
"#{File.expand_path('local_folder')}:/another_relative_mount",
|
261
|
+
]
|
262
|
+
}
|
263
|
+
})
|
264
|
+
end
|
265
|
+
|
266
|
+
docker.provision
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'should start the container' do
|
270
|
+
expect( container ).to receive(:start).with({'PublishAllPorts' => true, 'Privileged' => true})
|
271
|
+
|
272
|
+
docker.provision
|
273
|
+
end
|
274
|
+
|
275
|
+
context "connecting to ssh" do
|
276
|
+
before { @docker_host = ENV['DOCKER_HOST'] }
|
277
|
+
after { ENV['DOCKER_HOST'] = @docker_host }
|
278
|
+
|
279
|
+
it 'should expose port 22 to beaker' do
|
280
|
+
ENV['DOCKER_HOST'] = nil
|
281
|
+
docker.provision
|
282
|
+
|
283
|
+
expect( hosts[0]['ip'] ).to be === '127.0.1.1'
|
284
|
+
expect( hosts[0]['port'] ).to be === 8022
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'should expose port 22 to beaker when using DOCKER_HOST' do
|
288
|
+
ENV['DOCKER_HOST'] = "tcp://192.0.2.2:2375"
|
289
|
+
docker.provision
|
290
|
+
|
291
|
+
expect( hosts[0]['ip'] ).to be === '192.0.2.2'
|
292
|
+
expect( hosts[0]['port'] ).to be === 8022
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'should have ssh agent forwarding enabled' do
|
296
|
+
ENV['DOCKER_HOST'] = nil
|
297
|
+
docker.provision
|
298
|
+
|
299
|
+
expect( hosts[0]['ip'] ).to be === '127.0.1.1'
|
300
|
+
expect( hosts[0]['port'] ).to be === 8022
|
301
|
+
expect( hosts[0]['ssh'][:password] ).to be === 'root'
|
302
|
+
expect( hosts[0]['ssh'][:port] ).to be === 8022
|
303
|
+
expect( hosts[0]['ssh'][:forward_agent] ).to be === true
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
it "should generate a new /etc/hosts file referencing each host" do
|
309
|
+
ENV['DOCKER_HOST'] = nil
|
310
|
+
docker.provision
|
311
|
+
hosts.each do |host|
|
312
|
+
expect( docker ).to receive( :get_domain_name ).with( host ).and_return( 'labs.lan' )
|
313
|
+
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
|
314
|
+
end
|
315
|
+
docker.hack_etc_hosts( hosts, options )
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'should record the image and container for later' do
|
319
|
+
docker.provision
|
320
|
+
|
321
|
+
expect( hosts[0]['docker_image'] ).to be === image
|
322
|
+
expect( hosts[0]['docker_container'] ).to be === container
|
323
|
+
end
|
324
|
+
|
325
|
+
context 'provision=false' do
|
326
|
+
let(:options) {{
|
327
|
+
:logger => logger,
|
328
|
+
:forward_ssh_agent => true,
|
329
|
+
:provision => false
|
330
|
+
}}
|
331
|
+
|
332
|
+
|
333
|
+
it 'should fix ssh' do
|
334
|
+
hosts.each_with_index do |host, index|
|
335
|
+
container_name = "spec-container-#{index}"
|
336
|
+
host['docker_container_name'] = container_name
|
337
|
+
|
338
|
+
expect( ::Docker::Container ).to receive(:all).and_return([container])
|
339
|
+
expect(container).to receive(:exec).exactly(4).times
|
340
|
+
end
|
341
|
+
docker.provision
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'should not create a container if a named one already exists' do
|
345
|
+
hosts.each_with_index do |host, index|
|
346
|
+
container_name = "spec-container-#{index}"
|
347
|
+
host['docker_container_name'] = container_name
|
348
|
+
|
349
|
+
expect( ::Docker::Container ).to receive(:all).and_return([container])
|
350
|
+
expect( ::Docker::Container ).not_to receive(:create)
|
351
|
+
end
|
352
|
+
|
353
|
+
docker.provision
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
describe '#cleanup' do
|
359
|
+
before :each do
|
360
|
+
# get into a state where there's something to clean
|
361
|
+
allow( ::Docker ).to receive(:validate_version!)
|
362
|
+
allow( docker ).to receive(:dockerfile_for)
|
363
|
+
docker.provision
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'should stop the containers' do
|
367
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
368
|
+
expect( container ).to receive(:kill)
|
369
|
+
docker.cleanup
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'should delete the containers' do
|
373
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
374
|
+
expect( container ).to receive(:delete)
|
375
|
+
docker.cleanup
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'should delete the images' do
|
379
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
380
|
+
expect( image ).to receive(:delete)
|
381
|
+
docker.cleanup
|
382
|
+
end
|
383
|
+
|
384
|
+
it 'should not delete the image if docker_preserve_image is set to true' do
|
385
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
386
|
+
hosts.each do |host|
|
387
|
+
host['docker_preserve_image']=true
|
388
|
+
end
|
389
|
+
expect( image ).to_not receive(:delete)
|
390
|
+
docker.cleanup
|
391
|
+
end
|
392
|
+
|
393
|
+
it 'should delete the image if docker_preserve_image is set to false' do
|
394
|
+
allow( docker ).to receive( :sleep ).and_return(true)
|
395
|
+
hosts.each do |host|
|
396
|
+
host['docker_preserve_image']=false
|
397
|
+
end
|
398
|
+
expect( image ).to receive(:delete)
|
399
|
+
docker.cleanup
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|
403
|
+
|
404
|
+
describe '#dockerfile_for' do
|
405
|
+
FakeFS.deactivate!
|
406
|
+
before :each do
|
407
|
+
allow( ::Docker ).to receive(:validate_version!)
|
408
|
+
end
|
409
|
+
it 'should raise on an unsupported platform' do
|
410
|
+
expect { docker.send(:dockerfile_for, {'platform' => 'a_sidewalk', 'image' => 'foobar' }) }.to raise_error(/platform a_sidewalk not yet supported/)
|
411
|
+
end
|
412
|
+
|
413
|
+
it 'should raise on missing image' do
|
414
|
+
expect { docker.send(:dockerfile_for, {'platform' => 'centos-7-x86_64'})}.to raise_error(/Docker image undefined/)
|
415
|
+
end
|
416
|
+
|
417
|
+
it 'should set "ENV container docker"' do
|
418
|
+
FakeFS.deactivate!
|
419
|
+
platforms.each do |platform|
|
420
|
+
dockerfile = docker.send(:dockerfile_for, {
|
421
|
+
'platform' => platform,
|
422
|
+
'image' => 'foobar',
|
423
|
+
})
|
424
|
+
expect( dockerfile ).to be =~ /ENV container docker/
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
it 'should add docker_image_commands as RUN statements' do
|
429
|
+
FakeFS.deactivate!
|
430
|
+
platforms.each do |platform|
|
431
|
+
dockerfile = docker.send(:dockerfile_for, {
|
432
|
+
'platform' => platform,
|
433
|
+
'image' => 'foobar',
|
434
|
+
'docker_image_commands' => [
|
435
|
+
'special one',
|
436
|
+
'special two',
|
437
|
+
'special three',
|
438
|
+
]
|
439
|
+
})
|
440
|
+
|
441
|
+
expect( dockerfile ).to be =~ /RUN special one\nRUN special two\nRUN special three/
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
it 'should add docker_image_entrypoint' do
|
446
|
+
FakeFS.deactivate!
|
447
|
+
platforms.each do |platform|
|
448
|
+
dockerfile = docker.send(:dockerfile_for, {
|
449
|
+
'platform' => platform,
|
450
|
+
'image' => 'foobar',
|
451
|
+
'docker_image_entrypoint' => '/bin/bash'
|
452
|
+
})
|
453
|
+
|
454
|
+
expect( dockerfile ).to be =~ %r{ENTRYPOINT /bin/bash}
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
it 'should use zypper on sles' do
|
459
|
+
FakeFS.deactivate!
|
460
|
+
dockerfile = docker.send(:dockerfile_for, {
|
461
|
+
'platform' => 'sles-12-x86_64',
|
462
|
+
'image' => 'foobar',
|
463
|
+
})
|
464
|
+
|
465
|
+
expect( dockerfile ).to be =~ /RUN zypper -n in openssh/
|
466
|
+
end
|
467
|
+
|
468
|
+
(22..29).to_a.each do | fedora_release |
|
469
|
+
it "should use dnf on fedora #{fedora_release}" do
|
470
|
+
FakeFS.deactivate!
|
471
|
+
dockerfile = docker.send(:dockerfile_for, {
|
472
|
+
'platform' => "fedora-#{fedora_release}-x86_64",
|
473
|
+
'image' => 'foobar',
|
474
|
+
})
|
475
|
+
|
476
|
+
expect( dockerfile ).to be =~ /RUN dnf install -y sudo/
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'should use user dockerfile if specified' do
|
481
|
+
FakeFS.deactivate!
|
482
|
+
dockerfile = docker.send(:dockerfile_for, {
|
483
|
+
'dockerfile' => 'README.md'
|
484
|
+
})
|
485
|
+
|
486
|
+
expect( dockerfile ).to be == File.read('README.md')
|
487
|
+
end
|
488
|
+
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|