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.
@@ -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