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,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
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env drydock
2
+
3
+ set :event_handler do |event|
4
+ puts event
5
+ end
6
+
7
+ from 'gliderlabs/alpine', '3.2'
8
+ run 'apk update'
9
+ puts latest_image.id
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,6 @@
1
+
2
+ module Drydock
3
+ class OperationError < StandardError; end
4
+
5
+ class InvalidInstructionError < OperationError; end
6
+ 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