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