dry-dock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +6 -0
  3. data/.pryrc +1 -0
  4. data/.rspec +2 -0
  5. data/Dockerfile +69 -0
  6. data/Gemfile +20 -0
  7. data/Gemfile.lock +102 -0
  8. data/LICENSE +22 -0
  9. data/README.md +75 -0
  10. data/Rakefile +53 -0
  11. data/VERSION +1 -0
  12. data/bin/drydock +45 -0
  13. data/bin/json-test-consumer.rb +11 -0
  14. data/bin/json-test-producer.rb +25 -0
  15. data/bin/test-tar-writer-digest.rb +27 -0
  16. data/dry-dock.gemspec +135 -0
  17. data/examples/ruby-dsl.rb +14 -0
  18. data/examples/ruby-node-app-dsl.rb +128 -0
  19. data/examples/test-dsl.rb +9 -0
  20. data/examples/test.rb +46 -0
  21. data/lib/drydock/cli_flags.rb +46 -0
  22. data/lib/drydock/container_config.rb +75 -0
  23. data/lib/drydock/docker_api_patch.rb +176 -0
  24. data/lib/drydock/drydock.rb +65 -0
  25. data/lib/drydock/errors.rb +6 -0
  26. data/lib/drydock/file_manager.rb +26 -0
  27. data/lib/drydock/formatters.rb +13 -0
  28. data/lib/drydock/ignorefile_definition.rb +61 -0
  29. data/lib/drydock/image_repository.rb +50 -0
  30. data/lib/drydock/logger.rb +61 -0
  31. data/lib/drydock/object_caches/base.rb +24 -0
  32. data/lib/drydock/object_caches/filesystem_cache.rb +88 -0
  33. data/lib/drydock/object_caches/in_memory_cache.rb +52 -0
  34. data/lib/drydock/object_caches/no_cache.rb +38 -0
  35. data/lib/drydock/phase.rb +50 -0
  36. data/lib/drydock/phase_chain.rb +233 -0
  37. data/lib/drydock/plugins/apk.rb +31 -0
  38. data/lib/drydock/plugins/base.rb +15 -0
  39. data/lib/drydock/plugins/npm.rb +16 -0
  40. data/lib/drydock/plugins/package_manager.rb +30 -0
  41. data/lib/drydock/plugins/rubygems.rb +30 -0
  42. data/lib/drydock/project.rb +427 -0
  43. data/lib/drydock/runtime_options.rb +79 -0
  44. data/lib/drydock/stream_monitor.rb +54 -0
  45. data/lib/drydock/tar_writer.rb +36 -0
  46. data/lib/drydock.rb +35 -0
  47. data/spec/assets/MANIFEST +4 -0
  48. data/spec/assets/hello-world.txt +1 -0
  49. data/spec/assets/sample.tar +0 -0
  50. data/spec/assets/test.sh +3 -0
  51. data/spec/drydock/cli_flags_spec.rb +38 -0
  52. data/spec/drydock/container_config_spec.rb +230 -0
  53. data/spec/drydock/docker_api_patch_spec.rb +103 -0
  54. data/spec/drydock/drydock_spec.rb +25 -0
  55. data/spec/drydock/file_manager_spec.rb +53 -0
  56. data/spec/drydock/formatters_spec.rb +26 -0
  57. data/spec/drydock/ignorefile_definition_spec.rb +123 -0
  58. data/spec/drydock/image_repository_spec.rb +54 -0
  59. data/spec/drydock/object_caches/base_spec.rb +28 -0
  60. data/spec/drydock/object_caches/filesystem_cache_spec.rb +48 -0
  61. data/spec/drydock/object_caches/no_cache_spec.rb +62 -0
  62. data/spec/drydock/phase_chain_spec.rb +118 -0
  63. data/spec/drydock/phase_spec.rb +67 -0
  64. data/spec/drydock/plugins/apk_spec.rb +49 -0
  65. data/spec/drydock/plugins/base_spec.rb +13 -0
  66. data/spec/drydock/plugins/npm_spec.rb +26 -0
  67. data/spec/drydock/plugins/package_manager_spec.rb +12 -0
  68. data/spec/drydock/plugins/rubygems_spec.rb +53 -0
  69. data/spec/drydock/project_import_spec.rb +39 -0
  70. data/spec/drydock/project_spec.rb +156 -0
  71. data/spec/drydock/runtime_options_spec.rb +31 -0
  72. data/spec/drydock/stream_monitor_spec.rb +41 -0
  73. data/spec/drydock/tar_writer_spec.rb +27 -0
  74. data/spec/spec_helper.rb +47 -0
  75. data/spec/support/shared_examples/base_class.rb +3 -0
  76. data/spec/support/shared_examples/container_config.rb +12 -0
  77. data/spec/support/shared_examples/drydockfile.rb +6 -0
  78. metadata +223 -0
@@ -0,0 +1,62 @@
1
+
2
+ RSpec.describe Drydock::ObjectCaches::NoCache do
3
+
4
+ describe '.new' do
5
+ it 'takes no arguments' do
6
+ expect { described_class.new }.not_to raise_error
7
+ end
8
+ end
9
+
10
+ describe '#fetch' do
11
+ blk = Proc.new { :value }
12
+ let(:cache) { described_class.new }
13
+
14
+ it 'always executes the block' do
15
+ expect(blk).to receive(:call) { :value }.exactly(3).times
16
+ expect { cache.fetch(:foo, &blk) }.not_to raise_error
17
+ expect(cache.fetch(:foo, &blk)).to eq(:value)
18
+ expect(cache.fetch(:foo, &blk)).to eq(:value)
19
+ end
20
+ end
21
+
22
+ describe '#get' do
23
+ blk = Proc.new { :value }
24
+ let(:cache) { described_class.new }
25
+
26
+ it 'always returns nil' do
27
+ expect(blk).not_to receive(:call)
28
+ expect { cache.get(:foo) }.not_to raise_error
29
+ expect(cache.get(:foo)).to be_nil
30
+ end
31
+ end
32
+
33
+ describe '#set' do
34
+ let(:cache) { described_class.new }
35
+
36
+ context 'when given a value' do
37
+ it 'returns nil' do
38
+ blk = Proc.new { |f| }
39
+ expect(blk).not_to receive(:call)
40
+ expect(cache.set(:foo, :some_value)).to be_nil
41
+ end
42
+ end
43
+
44
+ context 'when given a writer block' do
45
+ it 'returns nil' do
46
+ blk = Proc.new { |f| }
47
+ expect(blk).to receive(:call).once
48
+ expect(cache.set(:foo, &blk)).to be_nil
49
+ end
50
+
51
+ it 'invokes the block with a file' do
52
+ file = nil
53
+ blk = Proc.new { |f| }
54
+ expect(blk).to receive(:call) { |f| file = f }.once
55
+
56
+ cache.set(:foo, &blk)
57
+ expect(file).to be_an(IO)
58
+ end
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,118 @@
1
+
2
+ RSpec.describe Drydock::PhaseChain do
3
+
4
+ EXPECTED_IMAGE_TO_ID_MAPPINGS = {
5
+ 'alpine' => {
6
+ '3.2' => 'f4fddc471ec2',
7
+ 'latest' => 'f4fddc471ec2'
8
+ }
9
+ }
10
+
11
+ describe '.from_repo' do
12
+
13
+ it 'takes a repo name forced without a tag' do
14
+ expect { described_class.from_repo('alpine', nil) }.not_to raise_error
15
+ end
16
+
17
+ it 'takes a repo name without a tag' do
18
+ expect { described_class.from_repo('alpine') }.not_to raise_error
19
+ end
20
+
21
+ it 'takes a repo name and tag' do
22
+ expect { described_class.from_repo('alpine', 'latest') }.not_to raise_error
23
+ end
24
+
25
+ end
26
+
27
+ describe '#root_image' do
28
+
29
+ subject { described_class.from_repo('alpine', '3.2').root_image }
30
+
31
+ it 'returns the image object' do
32
+ expect(subject).to be_a(Docker::Image)
33
+ end
34
+
35
+ it 'matches the image ID' do
36
+ expect(subject.id).to eq(EXPECTED_IMAGE_TO_ID_MAPPINGS['alpine']['3.2'])
37
+ end
38
+
39
+ end
40
+
41
+ describe '#images' do
42
+
43
+ let(:chain) { described_class.from_repo('alpine', '3.2') }
44
+ subject { chain.images }
45
+
46
+ it 'returns only the root object' do
47
+ expect(subject).to eq([chain.root_image])
48
+ end
49
+
50
+ end
51
+
52
+ describe '#last_image' do
53
+
54
+ let(:chain) { described_class.from_repo('alpine', '3.2') }
55
+ subject { chain.last_image }
56
+ it { is_expected.to be_nil }
57
+
58
+ end
59
+
60
+ describe '#run' do
61
+
62
+ let(:chain) { described_class.from_repo('alpine', '3.2') }
63
+
64
+ it 'runs a simple command successfully' do
65
+ expect(chain.images.size).to eq(1)
66
+ expect { chain.run('/bin/hostname') }.not_to raise_error
67
+ expect(chain.images.size).to eq(2)
68
+ expect { chain.destroy! }.not_to raise_error
69
+ end
70
+
71
+ it 'runs a command without committing successfully' do
72
+ expect(chain.images.size).to eq(1)
73
+ expect { chain.run('/bin/hostname', no_commit: true) }.not_to raise_error
74
+ expect(chain.images.size).to eq(1)
75
+ expect { chain.destroy! }.not_to raise_error
76
+ end
77
+
78
+ it 'sets a comment or author when provided' do
79
+ expect { chain.run('/bin/hostname', comment: 'Some random comment', author: 'John Doe') }.not_to raise_error
80
+ chain.last_image.tap do |image|
81
+ image.refresh!
82
+ expect(image.info['Comment']).to eq('Some random comment')
83
+ expect(image.info['Author']).to eq('John Doe')
84
+ end
85
+ expect { chain.destroy! }.not_to raise_error
86
+ end
87
+
88
+ it 'sets a command' do
89
+ expect { chain.run('/bin/hostname', command: ['/bin/ls', '/']) }.not_to raise_error
90
+ chain.last_image.tap do |image|
91
+ image.refresh!
92
+ expect(image.info['Config']['Cmd']).to eq(['/bin/ls', '/'])
93
+ end
94
+ expect { chain.destroy! }.not_to raise_error
95
+ end
96
+
97
+ it 'sets an environment' do
98
+ expect { chain.run('/bin/hostname', env: ["APP_ROOT=/app", "BUILD_ROOT=/build"]) }.not_to raise_error
99
+ chain.last_image.tap do |image|
100
+ image.refresh!
101
+ expect(image.info['Config']['Env']).to include('APP_ROOT=/app')
102
+ expect(image.info['Config']['Env']).to include('BUILD_ROOT=/build')
103
+ end
104
+ expect { chain.destroy! }.not_to raise_error
105
+ end
106
+
107
+ it 'exposes a port' do
108
+ expect { chain.run('/bin/hostname', expose: ['80/tcp']) }.not_to raise_error
109
+ chain.last_image.tap do |image|
110
+ image.refresh!
111
+ expect(image.info['Config']['ExposedPorts']).to eq("80/tcp" => {})
112
+ end
113
+ expect { chain.destroy! }.not_to raise_error
114
+ end
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,67 @@
1
+
2
+ RSpec.describe Drydock::Phase do
3
+
4
+ describe '.from' do
5
+
6
+ it 'takes optional values' do
7
+ expect { described_class.from({}) }.not_to raise_error
8
+ end
9
+
10
+ [:source_image, :build_container, :result_image].each do |name|
11
+ it "takes a valid #{name} attribute" do
12
+ expect { described_class.from(name => 'value_x') }.not_to raise_error
13
+ end
14
+ end
15
+
16
+ it 'rejects an invalid attribute' do
17
+ expect { described_class.from(whatever: 'value_x') }.to raise_error(ArgumentError)
18
+ end
19
+
20
+ end
21
+
22
+ let(:build_container) { double('BuildContainer') }
23
+ let(:no_build_container) { described_class.from(source_image: '123', result_image: '456') }
24
+ let(:has_build_container) { described_class.from(source_image: '123', result_image: '456', build_container: build_container) }
25
+
26
+ describe '#cached?' do
27
+
28
+ context 'when a build container is missing' do
29
+ subject { no_build_container.cached? }
30
+ it { is_expected.to be_truthy }
31
+ end
32
+
33
+ context 'when a build container is present' do
34
+ subject { has_build_container.cached? }
35
+ it { is_expected.to be_falsey }
36
+ end
37
+
38
+ end
39
+
40
+ describe '#built?' do
41
+
42
+ context 'when a build container is missing' do
43
+ subject { no_build_container.built? }
44
+ it { is_expected.to be_falsey }
45
+ end
46
+
47
+ context 'when a build container is present' do
48
+ subject { has_build_container.built? }
49
+ it { is_expected.to be_truthy }
50
+ end
51
+
52
+ end
53
+
54
+ describe 'finalize!' do
55
+
56
+ it 'when a build container is missing' do
57
+ expect { no_build_container.finalize! }.not_to raise_error
58
+ end
59
+
60
+ it 'when a build container is present' do
61
+ expect(build_container).to receive(:remove).with(hash_including(:force))
62
+ expect { has_build_container.finalize! }.not_to raise_error
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,49 @@
1
+
2
+ RSpec.describe Drydock::Plugins::APK do
3
+
4
+ let(:project) { double('Project') }
5
+ let(:plugin) { described_class.new(project) }
6
+
7
+ describe '#add' do
8
+
9
+ it 'adds one package' do
10
+ expect(project).to receive(:run).with('apk add ruby', {})
11
+ expect { plugin.add('ruby') }.not_to raise_error
12
+ end
13
+
14
+ it 'adds more than one packages' do
15
+ expect(project).to receive(:run).with('apk add curl ruby nodejs', {})
16
+ expect { plugin.add('curl', 'ruby', 'nodejs') }.not_to raise_error
17
+ end
18
+
19
+ end
20
+
21
+ describe '#remove' do
22
+
23
+ it 'deletes one package' do
24
+ expect(project).to receive(:run).with('apk del ruby')
25
+ expect { plugin.remove('ruby') }.not_to raise_error
26
+ end
27
+
28
+ it 'deletes more than one packages' do
29
+ expect(project).to receive(:run).with('apk del curl python nginx')
30
+ expect { plugin.remove('curl', 'python', 'nginx') }.not_to raise_error
31
+ end
32
+
33
+ end
34
+
35
+ describe '#update' do
36
+ it 'updates the package index' do
37
+ expect(project).to receive(:run).with('apk update')
38
+ expect { plugin.update }.not_to raise_error
39
+ end
40
+ end
41
+
42
+ describe '#upgrade' do
43
+ it 'upgrades the installed packages' do
44
+ expect(project).to receive(:run).with('apk upgrade')
45
+ expect { plugin.upgrade }.not_to raise_error
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,13 @@
1
+
2
+ RSpec.describe Drydock::Plugins::Base do
3
+
4
+ describe '.new' do
5
+
6
+ it 'accepts a project' do
7
+ project = double('Project')
8
+ expect { described_class.new(project) }.not_to raise_error
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,26 @@
1
+
2
+ RSpec.describe Drydock::Plugins::NPM do
3
+
4
+ let(:project) { double('Project') }
5
+ let(:plugin) { described_class.new(project) }
6
+
7
+ describe '#install' do
8
+
9
+ it 'adds one package' do
10
+ expect(project).to receive(:run).with('npm install sinopia')
11
+ expect { plugin.install('sinopia') }.not_to raise_error
12
+ end
13
+
14
+ it 'adds one package globally' do
15
+ expect(project).to receive(:run).with('npm install -g bower')
16
+ expect { plugin.install('bower', g: true) }.not_to raise_error
17
+ end
18
+
19
+ it 'adds more than one packages' do
20
+ expect(project).to receive(:run).with('npm install sinopia bower')
21
+ expect { plugin.install('sinopia', 'bower') }.not_to raise_error
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,12 @@
1
+
2
+ RSpec.describe Drydock::Plugins::PackageManager do
3
+
4
+ [:add, :clean, :remove, :update, :upgrade].each do |action|
5
+ describe "##{action}" do
6
+ it 'is not implemented' do
7
+ expect { described_class.new(nil).public_send(action) }.to raise_error(NotImplementedError)
8
+ end
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,53 @@
1
+
2
+ RSpec.describe Drydock::Plugins::Rubygems do
3
+
4
+ let(:project) { double('Project') }
5
+ let(:plugin) { described_class.new(project) }
6
+
7
+ describe '#add_source' do
8
+
9
+ it 'adds one source' do
10
+ expect(project).to receive(:run).with('gem sources --add http://whatever/')
11
+ expect { plugin.add_source('http://whatever/') }.not_to raise_error
12
+ end
13
+
14
+ end
15
+
16
+ describe '#remove_source' do
17
+
18
+ it 'removes one source' do
19
+ expect(project).to receive(:run).with('gem sources --remove http://whatever/')
20
+ expect { plugin.remove_source('http://whatever/') }.not_to raise_error
21
+ end
22
+
23
+ end
24
+
25
+ describe '#install' do
26
+
27
+ it 'installs one package' do
28
+ expect(project).to receive(:run).with('gem install curb ', timeout: 120)
29
+ expect { plugin.install('curb') }.not_to raise_error
30
+ end
31
+
32
+ it 'installs one package with timeout' do
33
+ expect(project).to receive(:run).with('gem install curb ', timeout: 45)
34
+ expect { plugin.install('curb', timeout: 45) }.not_to raise_error
35
+ end
36
+
37
+ it 'installs one package without docs' do
38
+ expect(project).to receive(:run).with('gem install curb --no-rdoc --no-ri ', timeout: 120)
39
+ expect { plugin.install('curb', rdoc: false, ri: false) }.not_to raise_error
40
+ end
41
+
42
+ end
43
+
44
+ describe '#update_system' do
45
+
46
+ it 'updates rubygems' do
47
+ expect(project).to receive(:run).with('gem update --system ', timeout: 200)
48
+ expect { plugin.update_system(timeout: 200) }.not_to raise_error
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,39 @@
1
+
2
+ RSpec.describe Drydock::Project do
3
+
4
+ let(:project) { described_class.new }
5
+ after(:each) { project.destroy!(force: true) if project }
6
+
7
+ it 'derives a project and imports between derivations' do
8
+ project.from('alpine')
9
+ project.mkdir('/app/1')
10
+
11
+ build = project.derive
12
+ build.mkdir('/app/2')
13
+ build.run('echo 427911 > /app/1/VERSION')
14
+ build.run('echo 427912 > /app/2/VERSION')
15
+ build.run('echo 427913 > /tmp/VERSION')
16
+
17
+ app = project.derive
18
+ app.import('/app', from: build)
19
+
20
+ b_container = build.last_image.run('cat /tmp/VERSION')
21
+ b_output = b_container.tap(&:wait).streaming_logs(stdout: true, stderr: true)
22
+ b_container.remove
23
+
24
+ expect(b_output).to eq("427913\n")
25
+
26
+ v1_container = app.last_image.run('cat /app/2/VERSION')
27
+ v1_output = v1_container.tap(&:wait).streaming_logs(stdout: true, stderr: true)
28
+ v1_container.remove
29
+
30
+ expect(v1_output).to eq("427912\n")
31
+
32
+ v2_container = app.last_image.run('cat /tmp/VERSION')
33
+ v2_output = v2_container.tap(&:wait).streaming_logs(stdout: true, stderr: true)
34
+ v2_container.remove
35
+
36
+ expect(v2_output).to include("'/tmp/VERSION': No such file or directory")
37
+ end
38
+
39
+ end
@@ -0,0 +1,156 @@
1
+
2
+ RSpec.describe Drydock::Project do
3
+
4
+ let(:project) { described_class.new }
5
+ after(:each) { project.destroy!(force: true) if project }
6
+
7
+ let(:asset_path) { 'spec/assets' }
8
+
9
+ it 'require a `from` before a `run`' do
10
+ expect { project.run('ls /') }.to raise_error(Drydock::InvalidInstructionError)
11
+ end
12
+
13
+ it 'require only one `from` per project' do
14
+ expect { project.from('alpine') }.not_to raise_error
15
+ expect { project.from('alpine') }.to raise_error(Drydock::InvalidInstructionError)
16
+ end
17
+
18
+ it 'sets no extra information on image commit' do
19
+ expect { project.from('alpine') }.not_to raise_error
20
+ expect { project.run('ls /') }.not_to raise_error
21
+
22
+ image = Docker::Image.get(project.last_image.id)
23
+ expect(image).not_to be_nil
24
+ expect(image.info['Author']).to eq('')
25
+ end
26
+
27
+ it 'sets the author after an image commit' do
28
+ name = 'John Doe'
29
+ email = 'john@doe.int'
30
+ author = 'John Doe <john@doe.int>'
31
+
32
+ project.from('alpine')
33
+ project.author(name: name, email: email)
34
+
35
+ expect { project.run('ls /') }.not_to raise_error
36
+ expect(project.last_image).not_to be_nil
37
+
38
+ image = Docker::Image.get(project.last_image.id)
39
+ expect(image).not_to be_nil
40
+ expect(image.info['Author']).to eq(author)
41
+ end
42
+
43
+ it 'build ID is incremented at every build step' do
44
+ expect(project.build_id).to eq('0')
45
+
46
+ project.from('alpine')
47
+ expect(project.build_id).to eq('1')
48
+ end
49
+
50
+ it 'copies asset files into an image' do
51
+ project.from('alpine')
52
+ expect { project.copy(asset_path, '/', chmod: false, no_cache: true, recursive: true) }.not_to raise_error
53
+
54
+ expect(project.last_image).not_to be_nil
55
+ expect(project.last_image.id).not_to be_empty
56
+
57
+ hash_container = project.last_image.run('sha1sum /spec/assets/hello-world.txt')
58
+ hash_output = hash_container.tap(&:wait).streaming_logs(stdout: true, stderr: true)
59
+ hash_container.remove
60
+
61
+ expect(hash_output).to include('60fde9c2310b0d4cad4dab8d126b04387efba289')
62
+ end
63
+
64
+ it 'autocreates the target path on copy' do
65
+ project.from('alpine')
66
+ expect {
67
+ project.copy(asset_path, '/assets', chmod: false, no_cache: true, recursive: true)
68
+ }.to raise_error(Drydock::InvalidInstructionError)
69
+
70
+ expect(project.last_image).to be_nil
71
+ end
72
+
73
+ it 'downloads from the source URL once' do
74
+ expect(Excon).to receive(:get).once.with('http://httpbin.org/ip', hash_including(:response_block))
75
+
76
+ project.set :cache, Drydock::ObjectCaches::InMemoryCache.new
77
+ project.from('alpine')
78
+ expect {
79
+ project.download_once('http://httpbin.org/ip', '/etc/ip_address.json', chmod: 0600)
80
+ project.download_once('http://httpbin.org/ip', '/etc/ip_address_2.json', chmod: 0600)
81
+ }.not_to raise_error
82
+ end
83
+
84
+ it 'sets the raw Cmd' do
85
+ project.from('alpine')
86
+ project.cmd(['/bin/bash'])
87
+
88
+ expect(project.last_image).not_to be_nil
89
+
90
+ image = Docker::Image.get(project.last_image.id)
91
+ expect(image).not_to be_nil
92
+ expect(image.info['Config']['Cmd']).to eq(['/bin/bash'])
93
+ end
94
+
95
+ it 'sets the shell Cmd' do
96
+ project.from('alpine')
97
+ project.cmd('/bin/ls')
98
+
99
+ expect(project.last_image).not_to be_nil
100
+
101
+ image = Docker::Image.get(project.last_image.id)
102
+ expect(image).not_to be_nil
103
+ expect(image.info['Config']['Cmd']).to eq(['/bin/sh', '-c', '/bin/ls'])
104
+ end
105
+
106
+ it 'keeps the shell Cmd even after another build command' do
107
+ project.from('alpine')
108
+ project.cmd('/bin/ls')
109
+ project.run('/bin/date')
110
+
111
+ expect(project.last_image).not_to be_nil
112
+
113
+ image = Docker::Image.get(project.last_image.id)
114
+ expect(image).not_to be_nil
115
+ expect(image.info['Config']['Cmd']).to eq(['/bin/sh', '-c', '/bin/ls'])
116
+ expect(image.info['ContainerConfig']['Cmd']).to eq(['/bin/sh', '-c', '/bin/date'])
117
+ end
118
+
119
+ it 'sets the Env' do
120
+ project.from('alpine')
121
+ project.env('APP_ROOT_TEST', '/app/current')
122
+
123
+ expect(project.last_image).not_to be_nil
124
+
125
+ image = Docker::Image.get(project.last_image.id)
126
+ expect(image).not_to be_nil
127
+ expect(image.info['Config']['Env']).to include('APP_ROOT_TEST=/app/current')
128
+ end
129
+
130
+ it 'sets the Env with multiple values' do
131
+ project.from('alpine')
132
+ project.env('APP_ROOT_TEST', '/app/current')
133
+ project.env('BUILD_ROOT', '/tmp/build')
134
+
135
+ expect(project.last_image).not_to be_nil
136
+
137
+ image = Docker::Image.get(project.last_image.id)
138
+ expect(image).not_to be_nil
139
+ expect(image.info['Config']['Env']).to include('APP_ROOT_TEST=/app/current')
140
+ expect(image.info['Config']['Env']).to include('BUILD_ROOT=/tmp/build')
141
+ end
142
+
143
+ it 'sets the ExposedPorts' do
144
+ project.from('alpine')
145
+ project.expose(tcp: [80, 443], udp: 53)
146
+
147
+ expect(project.last_image).not_to be_nil
148
+
149
+ image = Docker::Image.get(project.last_image.id)
150
+ expect(image).not_to be_nil
151
+ expect(image.info['Config']['ExposedPorts']).to include('80/tcp')
152
+ expect(image.info['Config']['ExposedPorts']).to include('443/tcp')
153
+ expect(image.info['Config']['ExposedPorts']).to include('53/udp')
154
+ end
155
+
156
+ end
@@ -0,0 +1,31 @@
1
+
2
+ RSpec.describe Drydock::RuntimeOptions do
3
+
4
+ describe '.parse!' do
5
+ it 'accepts build options' do
6
+ opts = described_class.parse!(%w{--no-cache -i test.rb -q})
7
+ expect(opts.cache).to be_falsey
8
+ expect(opts.includes).to include('test.rb')
9
+ expect(opts.log_level).to eq(Logger::ERROR)
10
+ end
11
+
12
+ it 'accepts extra build arguments' do
13
+ opts = described_class.parse!(%w{--build-opts version=2.0.3 --build-opts validate=true})
14
+
15
+ expect(opts.build_opts['version']).to eq('2.0.3')
16
+ expect(opts.build_opts[:version]).to eq('2.0.3')
17
+ expect(opts.build_opts[:validate]).to eq('true')
18
+ end
19
+
20
+ it 'converts timeout to integer' do
21
+ opts = described_class.parse!(%w{--timeout 20})
22
+ expect(opts.read_timeout).to eq(20)
23
+ end
24
+
25
+ it 'allows one to enable debug mode' do
26
+ opts = described_class.parse!(%w{--verbose})
27
+ expect(opts.log_level).to eq(Logger::DEBUG)
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,41 @@
1
+
2
+ RSpec.describe Drydock::StreamMonitor do
3
+ let(:events) { Array.new }
4
+ let(:handler) { lambda { |evt, is_new, serial, evt_type| events << evt } }
5
+ let(:monitor) { described_class.new(handler) }
6
+
7
+ after(:each) do
8
+ monitor.kill
9
+ monitor.join
10
+ end
11
+
12
+ describe '#alive?' do
13
+ subject { monitor.alive? }
14
+ it { is_expected.to be_truthy }
15
+ end
16
+
17
+ describe 'scenario' do
18
+ let(:image) { Docker::Image.create(fromImage: 'alpine', tag: 'latest') }
19
+ let(:container) { image.run('ls -l').tap(&:wait) }
20
+ let(:run_image) { container.commit }
21
+
22
+ after(:each) do
23
+ container.remove
24
+ run_image.remove
25
+ end
26
+
27
+ it 'receives the precise number of events' do
28
+ expect(events).to have(0).items
29
+
30
+ expect(monitor).not_to be_nil
31
+ expect(run_image).not_to be_nil
32
+
33
+ expect(events).to have(5).items
34
+
35
+ commit_event = events.find { |evt| evt.status == 'commit' }
36
+ expect(commit_event).not_to be_nil
37
+ expect(commit_event.id).to eq(container.id)
38
+ end
39
+ end
40
+
41
+ end