beaker-docker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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