dry-dock 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.
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