dry-dock 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +6 -0
- data/.pryrc +1 -0
- data/.rspec +2 -0
- data/Dockerfile +69 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +102 -0
- data/LICENSE +22 -0
- data/README.md +75 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/drydock +45 -0
- data/bin/json-test-consumer.rb +11 -0
- data/bin/json-test-producer.rb +25 -0
- data/bin/test-tar-writer-digest.rb +27 -0
- data/dry-dock.gemspec +135 -0
- data/examples/ruby-dsl.rb +14 -0
- data/examples/ruby-node-app-dsl.rb +128 -0
- data/examples/test-dsl.rb +9 -0
- data/examples/test.rb +46 -0
- data/lib/drydock/cli_flags.rb +46 -0
- data/lib/drydock/container_config.rb +75 -0
- data/lib/drydock/docker_api_patch.rb +176 -0
- data/lib/drydock/drydock.rb +65 -0
- data/lib/drydock/errors.rb +6 -0
- data/lib/drydock/file_manager.rb +26 -0
- data/lib/drydock/formatters.rb +13 -0
- data/lib/drydock/ignorefile_definition.rb +61 -0
- data/lib/drydock/image_repository.rb +50 -0
- data/lib/drydock/logger.rb +61 -0
- data/lib/drydock/object_caches/base.rb +24 -0
- data/lib/drydock/object_caches/filesystem_cache.rb +88 -0
- data/lib/drydock/object_caches/in_memory_cache.rb +52 -0
- data/lib/drydock/object_caches/no_cache.rb +38 -0
- data/lib/drydock/phase.rb +50 -0
- data/lib/drydock/phase_chain.rb +233 -0
- data/lib/drydock/plugins/apk.rb +31 -0
- data/lib/drydock/plugins/base.rb +15 -0
- data/lib/drydock/plugins/npm.rb +16 -0
- data/lib/drydock/plugins/package_manager.rb +30 -0
- data/lib/drydock/plugins/rubygems.rb +30 -0
- data/lib/drydock/project.rb +427 -0
- data/lib/drydock/runtime_options.rb +79 -0
- data/lib/drydock/stream_monitor.rb +54 -0
- data/lib/drydock/tar_writer.rb +36 -0
- data/lib/drydock.rb +35 -0
- data/spec/assets/MANIFEST +4 -0
- data/spec/assets/hello-world.txt +1 -0
- data/spec/assets/sample.tar +0 -0
- data/spec/assets/test.sh +3 -0
- data/spec/drydock/cli_flags_spec.rb +38 -0
- data/spec/drydock/container_config_spec.rb +230 -0
- data/spec/drydock/docker_api_patch_spec.rb +103 -0
- data/spec/drydock/drydock_spec.rb +25 -0
- data/spec/drydock/file_manager_spec.rb +53 -0
- data/spec/drydock/formatters_spec.rb +26 -0
- data/spec/drydock/ignorefile_definition_spec.rb +123 -0
- data/spec/drydock/image_repository_spec.rb +54 -0
- data/spec/drydock/object_caches/base_spec.rb +28 -0
- data/spec/drydock/object_caches/filesystem_cache_spec.rb +48 -0
- data/spec/drydock/object_caches/no_cache_spec.rb +62 -0
- data/spec/drydock/phase_chain_spec.rb +118 -0
- data/spec/drydock/phase_spec.rb +67 -0
- data/spec/drydock/plugins/apk_spec.rb +49 -0
- data/spec/drydock/plugins/base_spec.rb +13 -0
- data/spec/drydock/plugins/npm_spec.rb +26 -0
- data/spec/drydock/plugins/package_manager_spec.rb +12 -0
- data/spec/drydock/plugins/rubygems_spec.rb +53 -0
- data/spec/drydock/project_import_spec.rb +39 -0
- data/spec/drydock/project_spec.rb +156 -0
- data/spec/drydock/runtime_options_spec.rb +31 -0
- data/spec/drydock/stream_monitor_spec.rb +41 -0
- data/spec/drydock/tar_writer_spec.rb +27 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/shared_examples/base_class.rb +3 -0
- data/spec/support/shared_examples/container_config.rb +12 -0
- data/spec/support/shared_examples/drydockfile.rb +6 -0
- metadata +223 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
#!/usr/bin/env drydock
|
2
|
+
|
3
|
+
APP_ROOT = '/app'
|
4
|
+
BUILD_ROOT = '/build'
|
5
|
+
REPO_NAME = 'ripta/test'
|
6
|
+
TAG_NAME = 'v1.0'
|
7
|
+
|
8
|
+
|
9
|
+
from 'gliderlabs/alpine', '3.2'
|
10
|
+
|
11
|
+
with Plugins::APK do |pkg|
|
12
|
+
pkg.update
|
13
|
+
pkg.upgrade
|
14
|
+
pkg.add 'ruby', 'ruby-dev'
|
15
|
+
pkg.add 'nodejs', 'nodejs-dev'
|
16
|
+
pkg.add 'musl', 'musl-dev'
|
17
|
+
pkg.add 'linux-headers'
|
18
|
+
pkg.add 'gcc', 'g++'
|
19
|
+
pkg.add 'make'
|
20
|
+
pkg.add 'curl', 'curl-dev'
|
21
|
+
pkg.add 'openssh'
|
22
|
+
pkg.add 'libffi-dev', 'libxml2-dev', 'libxslt-dev'
|
23
|
+
pkg.add 'git'
|
24
|
+
end
|
25
|
+
|
26
|
+
download_once 'https://github.com/tianon/gosu/releases/download/1.3/gosu-amd64', '/bin/gosu', chmod: 0755
|
27
|
+
|
28
|
+
with Plugins::Rubygems do |g|
|
29
|
+
# g.source.add 'https://s3.amazonaws.com/production.s3.rubygems.org/'
|
30
|
+
# g.source.remove 'https://rubygems.org/'
|
31
|
+
g.update_system(document: false)
|
32
|
+
g.install('bundler', document: false)
|
33
|
+
g.install('unicorn', document: false)
|
34
|
+
end
|
35
|
+
|
36
|
+
run 'bundle config --global frozen 1'
|
37
|
+
run 'bundle config --global build.nokogiri --use-system-libraries'
|
38
|
+
|
39
|
+
with Plugins::NPM do |npm|
|
40
|
+
npm.install('bower', 'gulp', global: true)
|
41
|
+
end
|
42
|
+
|
43
|
+
one_week_old = CachingStrategies::TimeLimited.new(
|
44
|
+
max_age: 1.week,
|
45
|
+
max_depth: 1
|
46
|
+
)
|
47
|
+
|
48
|
+
derive do
|
49
|
+
env 'BUILD_ROOT', BUILD_ROOT
|
50
|
+
mkdir BUILD_ROOT
|
51
|
+
|
52
|
+
env 'APP_ROOT', APP_ROOT
|
53
|
+
mkdir APP_ROOT
|
54
|
+
|
55
|
+
gem_image = derive(label: 'rubygems', strategy: one_week_old) do
|
56
|
+
copy 'Gemfile', BUILD_ROOT
|
57
|
+
copy 'Gemfile.lock', BUILD_ROOT
|
58
|
+
|
59
|
+
cd BUILD_ROOT do
|
60
|
+
run 'bundle --path vendor'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
npm_image = if File.exist?('package.json')
|
65
|
+
derive(label: 'npm', strategy: one_week_old) do
|
66
|
+
copy 'package.json', BUILD_ROOT
|
67
|
+
cd BUILD_ROOT do
|
68
|
+
with(Plugins::NPM).install
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
import 'vendor', from: BUILD_ROOT, in: gem_image, to: APP_ROOT
|
74
|
+
import 'node_modules', from: BUILD_ROOT, in: npm_image, to: APP_ROOT
|
75
|
+
# import_stream gem_image.export_stream(BUILD_ROOT / 'vendor'), APP_ROOT / 'vendor'
|
76
|
+
# import_stream npm_image.export_stream(BUILD_ROOT / 'node_modules'), APP_ROOT / 'node_modules'
|
77
|
+
copy '.', BUILD_ROOT
|
78
|
+
|
79
|
+
# import_stream BUILD_ROOT do |writer|
|
80
|
+
# gem_image.export_stream(BUILD_ROOT + '/vendor') do |reader|
|
81
|
+
# while chunk = reader.read(32 * 1024)
|
82
|
+
# writer.write(chunk)
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
# tag REPO_NAME, TAG_NAME
|
88
|
+
end
|
89
|
+
|
90
|
+
# # Drydock.using(dd) { |base| ... }
|
91
|
+
# # or
|
92
|
+
# # base = Drydock.from(dd.id)
|
93
|
+
# # ...
|
94
|
+
# Drydock.using(dd) do |base|
|
95
|
+
# base.env('BUILD_ROOT', build_root)
|
96
|
+
# base.mkdir(build_root)
|
97
|
+
|
98
|
+
# # dd.snapshot('name') do |base|
|
99
|
+
# # ...
|
100
|
+
# # end
|
101
|
+
# # or
|
102
|
+
# # base = Drydock.from('name') || Drydock.from(dd.id)
|
103
|
+
# # ...
|
104
|
+
# # base.tag('name')
|
105
|
+
# bundle_image = base.snapshot('rubygems', max_age: Date.beginning_of_week) do |build|
|
106
|
+
# build.copy('Gemfile', build_root)
|
107
|
+
# build.copy('Gemfile.lock', build_root)
|
108
|
+
# build.in(build_root).run('bundle --path vendor')
|
109
|
+
# end
|
110
|
+
|
111
|
+
# npm_image = base.snapshot('npm', max_age: Date.beginning_of_week) do |build|
|
112
|
+
# build.copy('package.json', build_root)
|
113
|
+
# build.in(build_root).npm.install
|
114
|
+
# end
|
115
|
+
|
116
|
+
# Drydock.using(dd) do |build|
|
117
|
+
# build.env('APPLICATION_ROOT', app_root)
|
118
|
+
# build.mkdir(app_root)
|
119
|
+
|
120
|
+
# build.copy('.', app_root)
|
121
|
+
# build.import(bundle_image.export(build_root), app_root)
|
122
|
+
# build.import(npm_image.export(build_root), app_root)
|
123
|
+
|
124
|
+
# build.tag('ripta/dumplings', 'v1.0')
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
|
128
|
+
|
data/examples/test.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
Bundler.setup
|
7
|
+
require 'docker'
|
8
|
+
|
9
|
+
Docker.options[:read_timeout] = 300
|
10
|
+
|
11
|
+
estream = Thread.new do
|
12
|
+
Docker::Event.stream do |event|
|
13
|
+
puts event
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
stream_chunk_proc = proc do |stream, chunk|
|
18
|
+
puts "#{stream} #{chunk}"
|
19
|
+
end
|
20
|
+
|
21
|
+
i1 = Docker::Image.create(fromImage: 'gliderlabs/alpine', tag: '3.2')
|
22
|
+
puts i1.id
|
23
|
+
|
24
|
+
c1 = Docker::Container.create(Image: i1.id, Cmd: ['/bin/sh', '-c', 'apk update'], Tty: true)
|
25
|
+
c1.tap(&:start).attach(&stream_chunk_proc)
|
26
|
+
c1.wait
|
27
|
+
puts " #{c1.changes.size} changed files"
|
28
|
+
|
29
|
+
i2 = c1.commit
|
30
|
+
puts i2.id
|
31
|
+
|
32
|
+
Thread.kill(estream)
|
33
|
+
exit
|
34
|
+
|
35
|
+
c2 = Docker::Container.create(Image: i2.id, Cmd: ['/bin/sh', '-c', 'apk add ruby ruby-dev'], Tty: true)
|
36
|
+
c2.tap(&:start).attach(&stream_chunk_proc)
|
37
|
+
c2.wait
|
38
|
+
puts " #{c2.changes.size} changed files"
|
39
|
+
|
40
|
+
i3 = c2.commit
|
41
|
+
puts i3.id
|
42
|
+
|
43
|
+
##image.run('apk update')
|
44
|
+
##image.run('apk add ruby ruby-dev')
|
45
|
+
##image.run('apk add nodejs nodejs-dev')
|
46
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class CliFlags
|
4
|
+
|
5
|
+
def initialize(flags = {})
|
6
|
+
@flags = flags
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
return '' if flags.nil? || flags.empty?
|
11
|
+
|
12
|
+
buffer = StringIO.new
|
13
|
+
flags.each_pair do |k, v|
|
14
|
+
buffer << process_flag(k, v)
|
15
|
+
end
|
16
|
+
|
17
|
+
buffer.string
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
attr_reader :flags
|
22
|
+
|
23
|
+
def process_flag(key, value)
|
24
|
+
key = key.to_s
|
25
|
+
if key.size == 1
|
26
|
+
"-#{key} "
|
27
|
+
else
|
28
|
+
key = key.gsub(/_/, '-')
|
29
|
+
case value
|
30
|
+
when TrueClass
|
31
|
+
"--#{key} "
|
32
|
+
when FalseClass
|
33
|
+
"--no-#{key} "
|
34
|
+
else
|
35
|
+
"--#{key} #{process_value(value)}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_value(value)
|
41
|
+
value = value.to_s
|
42
|
+
value.match(/\s/) ? value.inspect : value
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class ContainerConfig < ::Hash
|
4
|
+
|
5
|
+
DEFAULTS = {
|
6
|
+
'OpenStdin' => false,
|
7
|
+
'AttachStdin' => false,
|
8
|
+
'AttachStdout' => false,
|
9
|
+
'AttachStderr' => false,
|
10
|
+
'User' => '',
|
11
|
+
'Tty' => false,
|
12
|
+
'Cmd' => nil,
|
13
|
+
'Env' => ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'],
|
14
|
+
'Labels' => nil,
|
15
|
+
'Entrypoint' => nil,
|
16
|
+
'ExposedPorts' => nil,
|
17
|
+
'Volumes' => nil
|
18
|
+
}
|
19
|
+
|
20
|
+
def self.from(hash)
|
21
|
+
return nil if hash.nil?
|
22
|
+
|
23
|
+
self.new.tap do |cfg|
|
24
|
+
DEFAULTS.each_pair do |k, v|
|
25
|
+
cfg[k] = v
|
26
|
+
end
|
27
|
+
hash.each_pair do |k, v|
|
28
|
+
cfg[k] = v
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Logic taken from https://github.com/docker/docker/blob/master/runconfig/compare.go
|
34
|
+
def ==(other)
|
35
|
+
return false if other.nil?
|
36
|
+
|
37
|
+
return false if self['OpenStdin'] || other['OpenStdin']
|
38
|
+
return false if self['AttachStdout'] != other['AttachStdout']
|
39
|
+
return false if self['AttachStderr'] != other['AttachStderr']
|
40
|
+
|
41
|
+
return false if self['User'] != other['User']
|
42
|
+
return false if self['Tty'] != other['Tty']
|
43
|
+
|
44
|
+
return false if self['Cmd'] != other['Cmd']
|
45
|
+
return false if Array(self['Env']).sort != Array(other['Env']).sort
|
46
|
+
return false if self['Labels'] != other['Labels']
|
47
|
+
return false if self['Entrypoint'] != other['Entrypoint']
|
48
|
+
|
49
|
+
my_ports = self['ExposedPorts'] || {}
|
50
|
+
other_ports = other['ExposedPorts'] || {}
|
51
|
+
return false if my_ports.keys.size != other_ports.keys.size
|
52
|
+
my_ports.keys.each do |my_port|
|
53
|
+
return false unless other_ports.key?(my_port)
|
54
|
+
end
|
55
|
+
|
56
|
+
my_vols = self['Volumes'] || {}
|
57
|
+
other_vols = other['Volumes'] || {}
|
58
|
+
return false if my_vols.keys.size != other_vols.keys.size
|
59
|
+
my_vols.keys.each do |my_vol|
|
60
|
+
return false unless other_vols.key?(my_vol)
|
61
|
+
end
|
62
|
+
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
|
66
|
+
def [](key)
|
67
|
+
super(key.to_s)
|
68
|
+
end
|
69
|
+
|
70
|
+
def []=(key, value)
|
71
|
+
super(key.to_s, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
|
2
|
+
module Docker
|
3
|
+
|
4
|
+
class Connection
|
5
|
+
|
6
|
+
def raw_request(*args, &block)
|
7
|
+
request = compile_request_params(*args, &block)
|
8
|
+
log_request(request)
|
9
|
+
resource.request(request)
|
10
|
+
rescue Excon::Errors::BadRequest => ex
|
11
|
+
raise ClientError, ex.response.body
|
12
|
+
rescue Excon::Errors::Unauthorized => ex
|
13
|
+
raise UnauthorizedError, ex.response.body
|
14
|
+
rescue Excon::Errors::NotFound => ex
|
15
|
+
raise NotFoundError, ex.response.body
|
16
|
+
rescue Excon::Errors::Conflict => ex
|
17
|
+
raise ConflictError, ex.response.body
|
18
|
+
rescue Excon::Errors::InternalServerError => ex
|
19
|
+
raise ServerError, ex.response.body
|
20
|
+
rescue Excon::Errors::Timeout => ex
|
21
|
+
raise TimeoutError, ex.message
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class Container
|
27
|
+
|
28
|
+
def archive_get(path = '/', &blk)
|
29
|
+
query = { 'path' => path }
|
30
|
+
connection.get(path_for(:archive), query, response_block: blk)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def archive_head(path = '/', &blk)
|
35
|
+
query = { 'path' => path }
|
36
|
+
response = connection.raw_request(:head, path_for(:archive), query, response_block: blk)
|
37
|
+
|
38
|
+
return if response.nil?
|
39
|
+
return if response.headers.empty?
|
40
|
+
return unless response.headers.key?('X-Docker-Container-Path-Stat')
|
41
|
+
|
42
|
+
ContainerPathStat.new(response.headers['X-Docker-Container-Path-Stat'])
|
43
|
+
rescue Docker::Error::NotFoundError
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def archive_put(path = '/', overwrite: false, &blk)
|
48
|
+
headers = { 'Content-Type' => 'application/x-tar' }
|
49
|
+
query = { 'path' => path, 'noOverwriteDirNonDir' => overwrite }
|
50
|
+
|
51
|
+
output = StringIO.new
|
52
|
+
blk.call(output)
|
53
|
+
output.rewind
|
54
|
+
|
55
|
+
connection.put(path_for(:archive), query, headers: headers, body: output)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
class ContainerPathStat
|
62
|
+
|
63
|
+
def initialize(definition)
|
64
|
+
@data = JSON.parse(Base64.decode64(definition))
|
65
|
+
end
|
66
|
+
|
67
|
+
def link_target
|
68
|
+
@data.fetch('linkTarget')
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(method_name, *method_args, &blk)
|
72
|
+
if mode.respond_to?(method_name)
|
73
|
+
mode.public_send(method_name, *method_args, &blk)
|
74
|
+
else
|
75
|
+
super
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def mode
|
80
|
+
@mode ||= UniversalFileMode.new(@data.fetch('mode'))
|
81
|
+
end
|
82
|
+
|
83
|
+
def mtime
|
84
|
+
@mtime ||= DateTime.parse(@data.fetch('mtime'))
|
85
|
+
end
|
86
|
+
|
87
|
+
def name
|
88
|
+
@data.fetch('name')
|
89
|
+
end
|
90
|
+
|
91
|
+
def respond_to?(method_name)
|
92
|
+
mode.respond_to?(method_name) || super
|
93
|
+
end
|
94
|
+
|
95
|
+
def size
|
96
|
+
@data.fetch('size')
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
# Go implementation of cross-system file modes: https://golang.org/pkg/os/#FileMode
|
102
|
+
class UniversalFileMode
|
103
|
+
|
104
|
+
BIT_FIELDS = [
|
105
|
+
{directory: 'd'},
|
106
|
+
{append_only: 'a'},
|
107
|
+
{exclusive: 'l'},
|
108
|
+
{temporary: 'T'},
|
109
|
+
{link: 'L'},
|
110
|
+
{device: 'D'},
|
111
|
+
{named_pipe: 'p'},
|
112
|
+
{socket: 'S'},
|
113
|
+
{setuid: 'u'},
|
114
|
+
{setgid: 'g'},
|
115
|
+
{character_device: 'c'},
|
116
|
+
{sticky: 't'}
|
117
|
+
]
|
118
|
+
|
119
|
+
def self.bit_for(name)
|
120
|
+
32 - 1 - BIT_FIELDS.index { |field| field.keys.first == name }
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.flags
|
124
|
+
BIT_FIELDS.map { |field| field.keys.first }
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.file_mode_mask
|
128
|
+
0777
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.short_flag_for(name)
|
132
|
+
BIT_FIELDS.find { |field| field.keys.first == name }.values.first
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.type_mode_mask
|
136
|
+
value_for(:directory) | value_for(:link) | value_for(:named_pipe) | value_for(:socket) | value_for(:device)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.value_for(name)
|
140
|
+
1 << bit_for(name)
|
141
|
+
end
|
142
|
+
|
143
|
+
def initialize(value)
|
144
|
+
@value = value
|
145
|
+
end
|
146
|
+
|
147
|
+
def file_mode
|
148
|
+
(@value & self.class.file_mode_mask)
|
149
|
+
end
|
150
|
+
|
151
|
+
def flags
|
152
|
+
self.class.flags.select { |name| send("#{name}?") }
|
153
|
+
end
|
154
|
+
|
155
|
+
def regular?
|
156
|
+
(@value & self.class.type_mode_mask) == 0
|
157
|
+
end
|
158
|
+
|
159
|
+
def short_flags
|
160
|
+
flags.map { |flag| self.class.short_flag_for(flag) }
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
short_flags.join
|
165
|
+
end
|
166
|
+
|
167
|
+
flags.each do |name|
|
168
|
+
define_method("#{name}?") do
|
169
|
+
bit_value = self.class.value_for(name)
|
170
|
+
(@value & bit_value) == bit_value
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
|
4
|
+
def self.banner
|
5
|
+
"Drydock v#{Drydock.version}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.build(opts = {}, &blk)
|
9
|
+
Project.new(opts).tap do |project|
|
10
|
+
dryfile, dryfilename = yield
|
11
|
+
|
12
|
+
Dir.chdir(File.dirname(dryfilename))
|
13
|
+
Drydock.logger.info("Working directory set to #{Dir.pwd}")
|
14
|
+
|
15
|
+
begin
|
16
|
+
catch :done do
|
17
|
+
project.instance_eval(dryfile, dryfilename)
|
18
|
+
end
|
19
|
+
rescue => e
|
20
|
+
Drydock.logger.error("Error processing #{dryfilename}:")
|
21
|
+
Drydock.logger.error(message: "#{e.class}: #{e.message}")
|
22
|
+
e.backtrace.each do |backtrace|
|
23
|
+
Drydock.logger.debug(message: "#{backtrace}", indent: 1)
|
24
|
+
end
|
25
|
+
ensure
|
26
|
+
Drydock.logger.info("Cleaning up")
|
27
|
+
project.finalize!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.build_on_chain(chain, opts = {}, &blk)
|
33
|
+
Project.new(opts.merge(chain: chain)).tap do |project|
|
34
|
+
project.instance_eval(&blk) if blk
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from(repo, opts = {}, &blk)
|
39
|
+
opts = opts.clone
|
40
|
+
tag = opts.delete(:tag, 'latest')
|
41
|
+
|
42
|
+
build(opts).tap do |project|
|
43
|
+
project.from(repo, tag)
|
44
|
+
yield project
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.logger
|
49
|
+
@logger ||= Logger.new(File.new('/dev/null', 'w+'))
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.logger=(logger)
|
53
|
+
@logger = logger
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.using(project)
|
57
|
+
raise NotImplementedError, "TODO(rpasay)"
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.version
|
61
|
+
version_file = File.join(File.dirname(__FILE__), '..', '..', 'VERSION')
|
62
|
+
File.exist?(version_file) ? File.read(version_file).chomp : ""
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class FileManager
|
4
|
+
|
5
|
+
def self.find(path, ignorefile, prepend_path: false, recursive: true)
|
6
|
+
path = path.sub(%r{/$}, '')
|
7
|
+
|
8
|
+
[].tap do |results|
|
9
|
+
::Find.find(path) do |subpath|
|
10
|
+
subpath = subpath.sub(%r{^#{path}/}, '')
|
11
|
+
|
12
|
+
Find.prune if ignorefile.match?(subpath)
|
13
|
+
|
14
|
+
if File.directory?(subpath)
|
15
|
+
Find.prune if path != subpath && !recursive
|
16
|
+
elsif prepend_path
|
17
|
+
results << File.join(path, subpath)
|
18
|
+
else
|
19
|
+
results << subpath
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Drydock
|
2
|
+
module Formatters
|
3
|
+
|
4
|
+
DELIMITER_PATTERN = /(\d)(?=(\d\d\d)+(?!\d))/
|
5
|
+
|
6
|
+
def self.number(value, delimiter: ',', separator: '.')
|
7
|
+
integers, decimals = value.to_s.split('.')
|
8
|
+
integers.gsub!(DELIMITER_PATTERN) { |digits| "#{digits}#{delimiter}" }
|
9
|
+
[integers, decimals].compact.join(separator)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class IgnorefileDefinition
|
4
|
+
class Rule < Struct.new(:pattern, :exclude)
|
5
|
+
alias_method :exclude?, :exclude
|
6
|
+
|
7
|
+
def match?(test)
|
8
|
+
File.fnmatch?(pattern, test)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
def_delegators :@rules, :count, :length, :size
|
15
|
+
|
16
|
+
def initialize(file_or_filename, dotfiles: false)
|
17
|
+
@dotfiles = dotfiles
|
18
|
+
|
19
|
+
if file_or_filename.respond_to?(:readlines)
|
20
|
+
patterns = Array(file_or_filename.readlines)
|
21
|
+
else
|
22
|
+
patterns = File.exist?(file_or_filename) ? File.readlines(file_or_filename) : []
|
23
|
+
end
|
24
|
+
|
25
|
+
@rules = patterns.map do |pattern|
|
26
|
+
pattern = pattern.chomp
|
27
|
+
if pattern.start_with?('!')
|
28
|
+
Rule.new(pattern.slice(1..-1), true)
|
29
|
+
else
|
30
|
+
Rule.new(pattern, false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def match?(filename)
|
36
|
+
return false if excludes?(filename)
|
37
|
+
return true if includes?(filename)
|
38
|
+
return true if is_dotfile?(filename)
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def excludes?(filename)
|
45
|
+
@rules.select { |rule| rule.exclude? }.any? do |rule|
|
46
|
+
rule.match?(filename)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def includes?(filename)
|
51
|
+
@rules.select { |rule| !rule.exclude? }.any? do |rule|
|
52
|
+
rule.match?(filename)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_dotfile?(filename)
|
57
|
+
@dotfiles && filename.start_with?('.') && filename.size > 1
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class ImageRepository
|
4
|
+
|
5
|
+
class <<self
|
6
|
+
include Enumerable
|
7
|
+
end
|
8
|
+
|
9
|
+
@image_cache = {}
|
10
|
+
|
11
|
+
def self.all
|
12
|
+
image_count = @image_cache.size
|
13
|
+
Docker::Image.all(all: 1).map do |image|
|
14
|
+
@image_cache[image.id] ||= Docker::Image.get(image.id)
|
15
|
+
end
|
16
|
+
ensure
|
17
|
+
delta_count = @image_cache.size - image_count
|
18
|
+
if delta_count > 0
|
19
|
+
Drydock.logger.info(message: "Loaded metadata for #{delta_count} images from docker cache")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.dangling
|
24
|
+
filters = {dangling: ["true"]}
|
25
|
+
Docker::Image.all(filters: filters.to_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.each(&blk)
|
29
|
+
self.all.each(&blk)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.find_by_config(config)
|
33
|
+
base_image = config['Image']
|
34
|
+
candidates = self.select_by_config(config)
|
35
|
+
|
36
|
+
possibles = candidates.select do |image|
|
37
|
+
image.info['Parent'] == base_image || image.info['ContainerConfig']['Image'] == base_image
|
38
|
+
end
|
39
|
+
|
40
|
+
possibles.sort_by { |image| image.info['Created'] }.last
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.select_by_config(config)
|
44
|
+
# we want to look at 'ContainerConfig', because we're interesting in how
|
45
|
+
# the image was built, not how the image will run
|
46
|
+
self.select { |image| config == ContainerConfig.from(image.info['ContainerConfig']) }
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|