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,61 @@
1
+
2
+ require 'logger'
3
+
4
+ module Drydock
5
+ class Logger < ::Logger
6
+
7
+ def add(severity, message = nil, progname = nil, &block)
8
+ annotation = nil
9
+ indent = 0
10
+
11
+ if message.nil?
12
+ if block_given?
13
+ message = yield
14
+ else
15
+ message = progname
16
+ progname = @progname
17
+ end
18
+ end
19
+
20
+ if message.respond_to?(:key?)
21
+ indent = message[:indent].to_i + 1
22
+ annotation = message.fetch(:annotation, '-->')
23
+ if message.key?(:message)
24
+ messages = Array(message[:message])
25
+ elsif message.key?(:messages)
26
+ messages = Array(message[:messages])
27
+ end
28
+ else
29
+ messages = [message]
30
+ end
31
+
32
+ annotation << " " if annotation
33
+ indentation = ' ' * indent
34
+
35
+ messages.each do |m|
36
+ m.to_s.split(/\n/).each do |line|
37
+ if annotation
38
+ super(severity, "#{indentation}#{annotation} #{line}", progname)
39
+ else
40
+ super(severity, line, progname)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ alias log add
47
+
48
+ end
49
+
50
+ class Formatter < ::Logger::Formatter
51
+
52
+ def call(severity, time, program, message)
53
+ "%s [%s] %s\n" % [
54
+ severity.slice(0, 1),
55
+ time.strftime('%H:%M:%S'),
56
+ msg2str(message)
57
+ ]
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+
2
+ module Drydock
3
+ module ObjectCaches
4
+ class Base
5
+
6
+ def clear
7
+ raise NotImplementedError, '#clear must be overridden in the subclass'
8
+ end
9
+
10
+ def fetch(key, &blk)
11
+ raise NotImplementedError, '#fetch must be overridden in the subclass'
12
+ end
13
+
14
+ def get(key, &blk)
15
+ raise NotImplementedError, '#get must be overridden in the subclass'
16
+ end
17
+
18
+ def set(key, value = nil, &blk)
19
+ raise NotImplementedError, '#set must be overridden in the subclass'
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,88 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module ObjectCaches
6
+ class FilesystemCache < Base
7
+
8
+ def initialize(dir = "~/.drydock")
9
+ @dir = File.expand_path(File.join(dir.to_s, 'cache'))
10
+ FileUtils.mkdir_p(@dir)
11
+ end
12
+
13
+ def clear
14
+ begin
15
+ FileUtils.remove_entry(@dir)
16
+ true
17
+ rescue => e
18
+ Drydock.logger.error("Cannot clear #{self.class} at #{@dir.inspect}: #{e}")
19
+ return false
20
+ end
21
+ end
22
+
23
+ def fetch(key, &blk)
24
+ filename = build_path(key)
25
+
26
+ if File.exist?(filename)
27
+ File.read(filename)
28
+ else
29
+ dirname = File.dirname(filename)
30
+ FileUtils.mkdir_p(dirname)
31
+
32
+ blk.call.tap do |contents|
33
+ File.open(filename, 'w') do |file|
34
+ file.write contents
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def get(key, &blk)
41
+ filename = build_path(key)
42
+ if File.exist?(filename)
43
+ if blk.nil?
44
+ File.read(filename)
45
+ else
46
+ File.open(filename) do |file|
47
+ blk.call file
48
+ end
49
+ end
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def key?(key)
56
+ File.exist?(build_path(key))
57
+ end
58
+
59
+ def set(key, value = nil, &blk)
60
+ filename = build_path(key)
61
+ dirname = File.dirname(filename)
62
+ FileUtils.mkdir_p(dirname)
63
+
64
+ File.open(filename, 'w') do |file|
65
+ if blk.nil?
66
+ file.write value
67
+ else
68
+ blk.call file
69
+ end
70
+ end
71
+
72
+ nil
73
+ end
74
+
75
+ private
76
+ attr_reader :dir
77
+
78
+ def build_path(key)
79
+ digest = Digest::SHA2.hexdigest(key)
80
+ subdir1 = digest.slice(0, 2)
81
+ subdir2 = digest.slice(2, 2)
82
+ filename = digest.slice(4, digest.length - 4)
83
+ File.join(dir, subdir1, subdir2, filename)
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,52 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module ObjectCaches
6
+ class InMemoryCache < Base
7
+
8
+ def initialize
9
+ @mem = {}
10
+ end
11
+
12
+ def clear
13
+ @mem.clear
14
+ true
15
+ end
16
+
17
+ def fetch(key, &blk)
18
+ @mem.fetch(key, &blk)
19
+ end
20
+
21
+ def get(key, &blk)
22
+ if @mem.key?(key)
23
+ if blk.nil?
24
+ @mem[key]
25
+ else
26
+ blk.call(StringIO.new(@mem[key]))
27
+ end
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ def key?(key)
34
+ @mem.key?(key)
35
+ end
36
+
37
+ def set(key, value = nil, &blk)
38
+ if blk
39
+ buffer = StringIO.new
40
+ blk.call buffer
41
+ buffer.rewind
42
+ @mem[key] = buffer.string
43
+ else
44
+ @mem[key] = value
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module ObjectCaches
6
+ class NoCache < Base
7
+
8
+ def clear
9
+ true
10
+ end
11
+
12
+ def fetch(key, &blk)
13
+ blk.call
14
+ end
15
+
16
+ def get(key, &blk)
17
+ nil
18
+ end
19
+
20
+ def key?(key)
21
+ false
22
+ end
23
+
24
+ def set(key, value = nil, &blk)
25
+ if blk
26
+ File.open('/dev/null', 'w') do |file|
27
+ blk.call file
28
+ end
29
+ else
30
+ # :noop:
31
+ end
32
+
33
+ nil
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+
2
+ module Drydock
3
+ class Phase < Struct.new(:source_image, :build_container, :result_image)
4
+
5
+ alias_method :build, :build_container
6
+ alias_method :build=, :build_container=
7
+
8
+ alias_method :result, :result_image
9
+ alias_method :result=, :result_image=
10
+
11
+ alias_method :source, :source_image
12
+ alias_method :source=, :source_image=
13
+
14
+ def self.from(hsh)
15
+ h = hsh.to_h
16
+ extra_keys = h.keys - members
17
+ raise ArgumentError, "unknown options: #{extra_keys.join(', ')}" unless extra_keys.empty?
18
+ new(*h.values_at(*members))
19
+ end
20
+
21
+ def built?
22
+ !cached?
23
+ end
24
+
25
+ def cached?
26
+ build_container.nil?
27
+ end
28
+
29
+ def destroy!(force: false)
30
+ if result_image
31
+ begin
32
+ result_image.remove(force: force)
33
+ rescue Docker::Error::NotFoundError => e
34
+ # Ignore, because the image could have been deleted by another phase in
35
+ # another derived chain.
36
+ end
37
+ end
38
+
39
+ build_container.remove(force: force) if built?
40
+ self
41
+ end
42
+
43
+ def finalize!(force: false)
44
+ return self unless built?
45
+ build_container.remove(force: force)
46
+ self
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,233 @@
1
+
2
+ module Drydock
3
+ class PhaseChain
4
+ extend Forwardable
5
+ include Enumerable
6
+
7
+ def_delegators :@chain, :<<, :at, :empty?, :last, :length, :push, :size
8
+
9
+ def self.build_commit_opts(opts = {})
10
+ {}.tap do |commit|
11
+ if opts.key?(:command)
12
+ commit['run'] = {
13
+ Cmd: opts[:command]
14
+ }
15
+ end
16
+
17
+ commit[:author] = opts.fetch(:author, '') if opts.key?(:author)
18
+ commit[:comment] = opts.fetch(:comment, '') if opts.key?(:comment)
19
+ end
20
+ end
21
+
22
+ def self.build_container_opts(image_id, cmd, opts = {})
23
+ cmd = ['/bin/sh', '-c', cmd.to_s] unless cmd.is_a?(Array)
24
+
25
+ ContainerConfig.from(
26
+ Cmd: cmd,
27
+ Tty: opts.fetch(:tty, false),
28
+ Image: image_id
29
+ ).tap do |cc|
30
+ env = Array(opts[:env])
31
+ cc[:Env].push(*env) unless env.empty?
32
+
33
+ if opts.key?(:expose)
34
+ cc[:ExposedPorts] ||= {}
35
+ opts[:expose].each do |port|
36
+ cc[:ExposedPorts][port] = {}
37
+ end
38
+ end
39
+
40
+ (cc[:OnBuild] ||= []).push(opts[:on_build]) if opts.key?(:on_build)
41
+ end
42
+ end
43
+
44
+ def self.build_pull_opts(repo, tag = nil)
45
+ if tag
46
+ {fromImage: repo, tag: tag}
47
+ else
48
+ {fromImage: repo}
49
+ end
50
+ end
51
+
52
+ def self.create_container(cfg, timeout: nil, &blk)
53
+ timeout ||= Excon.defaults[:read_timeout]
54
+
55
+ Docker::Container.create(cfg).tap do |c|
56
+ t = Thread.new do
57
+ begin
58
+ c.attach(stream: true, stdout: true, stderr: true) do |stream, chunk|
59
+ case stream
60
+ when :stdout
61
+ Drydock.logger.info(message: chunk, annotation: '(O)')
62
+ when :stderr
63
+ Drydock.logger.info(message: chunk, annotation: '(E)')
64
+ else
65
+ Drydock.logger.info(message: chunk, annotation: '(?)')
66
+ end
67
+ end
68
+ rescue Docker::Error::TimeoutError
69
+ Drydock.logger.warn(message: "Lost connection to stream; retrying")
70
+ retry
71
+ end
72
+ end
73
+
74
+ c.start
75
+
76
+ blk.call(c) if blk
77
+
78
+ c.wait(timeout)
79
+ t.join
80
+ end
81
+ end
82
+
83
+ def self.from_repo(repo, tag = 'latest')
84
+ new(Docker::Image.create(build_pull_opts(repo, tag)))
85
+ end
86
+
87
+ def initialize(from, parent = nil)
88
+ @chain = []
89
+ @from = from
90
+ @parent = parent
91
+ @children = []
92
+
93
+ @ephemeral_containers = []
94
+
95
+ if parent
96
+ parent.children << self
97
+ end
98
+ end
99
+
100
+ def children
101
+ @children
102
+ end
103
+
104
+ def containers
105
+ map(&:build_container)
106
+ end
107
+
108
+ def depth
109
+ @parent ? @parent.depth + 1 : 1
110
+ end
111
+
112
+ def derive
113
+ self.class.new(last_image, self)
114
+ end
115
+
116
+ def destroy!(force: false)
117
+ return self if frozen?
118
+ children.reverse_each { |c| c.destroy!(force: force) } if children
119
+ ephemeral_containers.map { |c| c.remove(force: force) }
120
+
121
+ reverse_each { |c| c.destroy!(force: force) }
122
+ freeze
123
+ end
124
+
125
+ def each(&blk)
126
+ @chain.each(&blk)
127
+ end
128
+
129
+ def ephemeral_containers
130
+ @ephemeral_containers
131
+ end
132
+
133
+ def finalize!(force: false)
134
+ return self if frozen?
135
+
136
+ children.map { |c| c.finalize!(force: force) } if children
137
+ ephemeral_containers.map { |c| c.remove(force: force) }
138
+
139
+ Drydock.logger.info("##{serial}: Final image ID is #{last_image.id}") unless empty?
140
+ map { |p| p.finalize!(force: force) }
141
+ freeze
142
+ end
143
+
144
+ def images
145
+ [root_image] + map(&:result_image)
146
+ end
147
+
148
+ def last_image
149
+ @chain.last ? @chain.last.result_image : nil
150
+ end
151
+
152
+ def root_image
153
+ @from
154
+ end
155
+
156
+ def run(cmd, opts = {}, &blk)
157
+ src_image = last ? last.result_image : @from
158
+ no_commit = opts.fetch(:no_commit, false)
159
+
160
+ no_cache = opts.fetch(:no_cache, false)
161
+ no_cache = true if no_commit
162
+
163
+ build_config = self.class.build_container_opts(src_image.id, cmd, opts)
164
+ Drydock.logger.info(build_config.inspect)
165
+ cached_image = ImageRepository.find_by_config(build_config)
166
+
167
+ if cached_image && !no_cache
168
+ Drydock.logger.info(message: "Using cached image ID #{cached_image.id.slice(0, 12)}")
169
+
170
+ if no_commit
171
+ Drydock.logger.info(message: "Skipping commit phase")
172
+ else
173
+ self << Phase.from(
174
+ source_image: src_image,
175
+ result_image: cached_image
176
+ )
177
+ end
178
+ else
179
+ if cached_image && no_commit
180
+ Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_commit")
181
+ elsif cached_image && no_cache
182
+ Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_cache")
183
+ end
184
+
185
+ container = self.class.create_container(build_config)
186
+ yield container if block_given?
187
+
188
+ if no_commit
189
+ Drydock.logger.info(message: "Skipping commit phase")
190
+ ephemeral_containers << container
191
+ else
192
+ if opts.key?(:command)
193
+ Drydock.logger.info("Command override: #{opts[:command].inspect}")
194
+ else
195
+ src_image.refresh!
196
+ if src_image.info && src_image.info.key?('Config')
197
+ src_image_config = src_image.info['Config']
198
+ opts[:command] = src_image_config['Cmd'] if src_image_config.key?('Cmd')
199
+ end
200
+
201
+ Drydock.logger.info("Command retrieval: #{opts[:command].inspect}")
202
+ Drydock.logger.info("Source image info: #{src_image.info.class} #{src_image.info.inspect}")
203
+ Drydock.logger.info("Source image config: #{src_image.info['Config'].inspect}")
204
+ end
205
+
206
+ commit_config = self.class.build_commit_opts(opts)
207
+ Drydock.logger.info(opts.inspect)
208
+ Drydock.logger.info(commit_config.inspect)
209
+
210
+ result = container.commit(commit_config)
211
+ Drydock.logger.info(message: "Committed image ID #{result.id.slice(0, 12)}")
212
+
213
+ self << Phase.from(
214
+ source_image: src_image,
215
+ build_container: container,
216
+ result_image: result
217
+ )
218
+ end
219
+ end
220
+
221
+ self
222
+ end
223
+
224
+ def serial
225
+ @parent ? "#{@parent.serial}.#{@parent.children.index(self) + 1}.#{size + 1}" : "#{size + 1}"
226
+ end
227
+
228
+ def tag(repo, tag = 'latest', force: false)
229
+ last_image.tag(repo: repo, tag: tag, force: force)
230
+ end
231
+
232
+ end
233
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require_relative 'package_manager'
3
+
4
+ module Drydock
5
+ module Plugins
6
+ class APK < PackageManager
7
+
8
+ def add(*pkgs)
9
+ opts = pkgs.last.is_a?(Hash) ? pkgs.pop : {}
10
+ project.run "apk add #{pkgs.join(' ')}", opts
11
+ end
12
+
13
+ def clean
14
+ project.run "rm -rf /var/cache/apk/*"
15
+ end
16
+
17
+ def remove(*pkgs)
18
+ project.run "apk del #{pkgs.join(' ')}"
19
+ end
20
+
21
+ def update
22
+ project.run "apk update"
23
+ end
24
+
25
+ def upgrade
26
+ project.run "apk upgrade"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module Drydock
3
+ module Plugins
4
+ class Base
5
+
6
+ attr_reader :project
7
+
8
+ def initialize(project)
9
+ @project = project
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,16 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module Plugins
6
+ class NPM < Base
7
+
8
+ def install(*pkgs)
9
+ opts = pkgs.last.is_a?(Hash) ? pkgs.pop : {}
10
+ flags = CliFlags.new(opts)
11
+ project.run("npm install #{flags}#{pkgs.join(' ')}")
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module Plugins
6
+ class PackageManager < Base
7
+
8
+ def add(*pkgs)
9
+ raise NotImplementedError, '#add must be overridde in the subclass'
10
+ end
11
+
12
+ def clean
13
+ raise NotImplementedError, '#clean must be overridde in the subclass'
14
+ end
15
+
16
+ def remove(*pkgs)
17
+ raise NotImplementedError, '#remove must be overridde in the subclass'
18
+ end
19
+
20
+ def update
21
+ raise NotImplementedError, '#update must be overridde in the subclass'
22
+ end
23
+
24
+ def upgrade
25
+ raise NotImplementedError, '#upgrade must be overridde in the subclass'
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+
2
+ require_relative 'base'
3
+
4
+ module Drydock
5
+ module Plugins
6
+ class Rubygems < Base
7
+
8
+ def add_source(uri)
9
+ project.run("gem sources --add #{uri}")
10
+ end
11
+
12
+ def install(pkg, opts = {})
13
+ timeout = opts.delete(:timeout) || 120
14
+ flags = CliFlags.new(opts)
15
+ project.run("gem install #{pkg} #{flags}", timeout: timeout)
16
+ end
17
+
18
+ def remove_source(uri)
19
+ project.run("gem sources --remove #{uri}")
20
+ end
21
+
22
+ def update_system(opts = {})
23
+ timeout = opts.delete(:timeout) || 300
24
+ flags = CliFlags.new(opts)
25
+ project.run("gem update --system #{flags}", timeout: timeout)
26
+ end
27
+
28
+ end
29
+ end
30
+ end