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