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,427 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class Project
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
auto_remove: true,
|
7
|
+
author: nil,
|
8
|
+
cache: nil,
|
9
|
+
event_handler: false,
|
10
|
+
ignorefile: '.dockerignore',
|
11
|
+
label: nil,
|
12
|
+
logs: false
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(build_opts = {})
|
16
|
+
@chain = build_opts.key?(:chain) && build_opts.delete(:chain).derive
|
17
|
+
@plugins = {}
|
18
|
+
|
19
|
+
@run_path = []
|
20
|
+
@serial = 0
|
21
|
+
|
22
|
+
@build_opts = DEFAULT_OPTIONS.clone
|
23
|
+
build_opts.each_pair { |key, value| set(key, value) }
|
24
|
+
|
25
|
+
@stream_monitor = build_opts[:event_handler] ? StreamMonitor.new(build_opts[:event_handler]) : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set the author for commits. This is not an instruction, per se, and only
|
29
|
+
# takes into effect after instructions that cause a commit.
|
30
|
+
def author(name: nil, email: nil)
|
31
|
+
value = email ? "#{name} <#{email}>" : name.to_s
|
32
|
+
set :author, value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Retrieve the current build ID for this project.
|
36
|
+
def build_id
|
37
|
+
chain ? chain.serial : '0'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Change directories for operations that require a directory.
|
41
|
+
def cd(path, &blk)
|
42
|
+
@run_path << path
|
43
|
+
blk.call
|
44
|
+
ensure
|
45
|
+
@run_path.pop
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set the command to automatically execute when the image is run.
|
49
|
+
def cmd(command)
|
50
|
+
requires_from!(:cmd)
|
51
|
+
log_step('cmd', command)
|
52
|
+
|
53
|
+
unless command.is_a?(Array)
|
54
|
+
command = ['/bin/sh', '-c', command.to_s]
|
55
|
+
end
|
56
|
+
|
57
|
+
chain.run("# CMD #{command.inspect}", command: command)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Copies files from `source_path` on the the build machine, into `target_path`
|
62
|
+
# in the container. This instruction automatically commits the result.
|
63
|
+
#
|
64
|
+
# When `chmod` is `false` (the default), the original file mode from its
|
65
|
+
# source file is kept when copying into the container. Otherwise, the mode
|
66
|
+
# provided will be used to override *all* file and directory modes.
|
67
|
+
#
|
68
|
+
# When `no_cache` is `false` (the default), the hash digest of the source path
|
69
|
+
# is used as the cache key. When `true`, the image is rebuilt every time.
|
70
|
+
#
|
71
|
+
# The `copy` instruction always respects the `ignorefile`.
|
72
|
+
def copy(source_path, target_path, chmod: false, no_cache: false, recursive: true)
|
73
|
+
requires_from!(:copy)
|
74
|
+
log_step('copy', source_path, target_path, chmod: (chmod ? sprintf('%o', chmod) : false))
|
75
|
+
|
76
|
+
if source_path.start_with?('/')
|
77
|
+
Drydock.logger.warn("#{source_path.inspect} is an absolute path; we recommend relative paths")
|
78
|
+
end
|
79
|
+
|
80
|
+
raise InvalidInstructionError, "#{source_path} does not exist" unless File.exist?(source_path)
|
81
|
+
|
82
|
+
source_files = if File.directory?(source_path)
|
83
|
+
FileManager.find(source_path, ignorefile, prepend_path: true, recursive: recursive)
|
84
|
+
else
|
85
|
+
[source_path]
|
86
|
+
end
|
87
|
+
source_files.sort!
|
88
|
+
|
89
|
+
raise InvalidInstructionError, "#{source_path} is empty or does not match a path" if source_files.empty?
|
90
|
+
|
91
|
+
buffer = StringIO.new
|
92
|
+
log_info("Processing #{source_files.size} files in tree")
|
93
|
+
TarWriter.new(buffer) do |tar|
|
94
|
+
source_files.each do |source_file|
|
95
|
+
File.open(source_file, 'r') do |input|
|
96
|
+
stat = input.stat
|
97
|
+
mode = chmod || stat.mode
|
98
|
+
tar.add_entry(source_file, mode: stat.mode, mtime: stat.mtime) do |tar_file|
|
99
|
+
tar_file.write(input.read)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
buffer.rewind
|
106
|
+
digest = Digest::MD5.hexdigest(buffer.read)
|
107
|
+
|
108
|
+
log_info("Tree digest is md5:#{digest}")
|
109
|
+
chain.run("# COPY #{source_path} #{target_path} DIGEST #{digest}", no_cache: no_cache) do |container|
|
110
|
+
target_stat = container.archive_head(target_path)
|
111
|
+
|
112
|
+
# TODO(rpasay): cannot autocreate the target, because `container` here is already dead
|
113
|
+
unless target_stat
|
114
|
+
raise InvalidInstructionError, "Target path #{target_path.inspect} does not exist"
|
115
|
+
end
|
116
|
+
|
117
|
+
unless target_stat.directory?
|
118
|
+
Drydock.logger.debug(target_stat)
|
119
|
+
raise InvalidInstructionError, "Target path #{target_path.inspect} exists, but is not a directory in the container"
|
120
|
+
end
|
121
|
+
|
122
|
+
container.archive_put(target_path) do |output|
|
123
|
+
buffer.rewind
|
124
|
+
output.write(buffer.read)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def destroy!(force: false)
|
132
|
+
chain.destroy!(force: force) if chain
|
133
|
+
finalize!(force: force)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Meta instruction to signal to the builder that the build is done.
|
137
|
+
def done!
|
138
|
+
throw :done
|
139
|
+
end
|
140
|
+
|
141
|
+
# Download (and cache) a file from `source_url`, and copy it into the
|
142
|
+
# `target_path` in the container with a specific `chmod` (defaults to 0644).
|
143
|
+
#
|
144
|
+
# The cache currently cannot be disabled.
|
145
|
+
def download_once(source_url, target_path, chmod: 0644)
|
146
|
+
requires_from!(:download_once)
|
147
|
+
|
148
|
+
unless cache.key?(source_url)
|
149
|
+
cache.set(source_url) do |obj|
|
150
|
+
chunked = Proc.new do |chunk, remaining_bytes, total_bytes|
|
151
|
+
obj.write(chunk)
|
152
|
+
end
|
153
|
+
Excon.get(source_url, response_block: chunked)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
log_step('download_once', source_url, target_path, chmod: sprintf('%o', chmod))
|
158
|
+
|
159
|
+
# TODO(rpasay): invalidate cache when the downloaded file changes,
|
160
|
+
# and then force rebuild
|
161
|
+
chain.run("# DOWNLOAD #{source_url} #{target_path}") do |container|
|
162
|
+
container.archive_put do |output|
|
163
|
+
TarWriter.new(output) do |tar|
|
164
|
+
cache.get(source_url) do |input|
|
165
|
+
tar.add_file(target_path, chmod) do |tar_file|
|
166
|
+
tar_file.write(input.read)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
# Set an environment variable.
|
177
|
+
def env(name, value)
|
178
|
+
requires_from!(:env)
|
179
|
+
log_step('env', name, value)
|
180
|
+
chain.run("# SET ENV #{name}", env: ["#{name}=#{value}"])
|
181
|
+
self
|
182
|
+
end
|
183
|
+
|
184
|
+
# Set multiple environment variables at once. `pairs` should be a
|
185
|
+
# hash-like enumerable.
|
186
|
+
def envs(pairs = {})
|
187
|
+
requires_from!(:envs)
|
188
|
+
log_step('envs', pairs)
|
189
|
+
|
190
|
+
values = pairs.map { |name, value| "#{name}=#{value}" }
|
191
|
+
chain.run("# SET ENVS #{pairs.inspect}", env: values)
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
# Expose one or more ports.
|
196
|
+
#
|
197
|
+
# When `ports` is specified, the format must be: ##/type where ## is the port
|
198
|
+
# number and type is either tcp or udp. For example, "80/tcp", "53/udp".
|
199
|
+
#
|
200
|
+
# Otherwise, when the `tcp` or `udp` options are specified, only the port
|
201
|
+
# numbers are required.
|
202
|
+
def expose(*ports, tcp: [], udp: [])
|
203
|
+
requires_from!(:expose)
|
204
|
+
|
205
|
+
Array(tcp).flatten.each { |p| ports << "#{p}/tcp" }
|
206
|
+
Array(udp).flatten.each { |p| ports << "#{p}/udp" }
|
207
|
+
|
208
|
+
log_step('expose', *ports)
|
209
|
+
|
210
|
+
chain.run("# SET PORTS #{ports.inspect}", expose: ports)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Build on top of the `from` image. This must be the first instruction of
|
214
|
+
# the project, although non-instructions may appear before this.
|
215
|
+
def from(repo, tag = 'latest')
|
216
|
+
raise InvalidInstructionError, '`from` must only be called once per project' if chain
|
217
|
+
log_step('from', repo, tag)
|
218
|
+
@chain = PhaseChain.from_repo(repo, tag)
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
# Finalize everything. This will be automatically invoked at the end of
|
223
|
+
# the build, and should not be called manually.
|
224
|
+
def finalize!(force: false)
|
225
|
+
if chain
|
226
|
+
chain.finalize!(force: force)
|
227
|
+
end
|
228
|
+
|
229
|
+
if stream_monitor
|
230
|
+
stream_monitor.kill
|
231
|
+
stream_monitor.join
|
232
|
+
end
|
233
|
+
|
234
|
+
self
|
235
|
+
end
|
236
|
+
|
237
|
+
# Derive a new project based on the current state of the build. This
|
238
|
+
# instruction returns a project that can be referred to elsewhere.
|
239
|
+
def derive(opts = {}, &blk)
|
240
|
+
Drydock.build_on_chain(chain, opts, &blk)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Access to the logger object.
|
244
|
+
def logger
|
245
|
+
Drydock.logger
|
246
|
+
end
|
247
|
+
|
248
|
+
# Import a `path` from a different project. The `from` option should be
|
249
|
+
# project, usually the result of a `derive` instruction.
|
250
|
+
#
|
251
|
+
# TODO(rpasay): add a #load method as an alternative to #import, which allows
|
252
|
+
# importing a full container, including things from /etc.
|
253
|
+
# TODO(rpasay): do not always append /. to the #archive_get calls; must check
|
254
|
+
# the type of `path` inside the container first.
|
255
|
+
# TODO(rpasay): break this large method into smaller ones.
|
256
|
+
def import(path, from: nil, force: false, spool: false)
|
257
|
+
mkdir(path)
|
258
|
+
|
259
|
+
requires_from!(:import)
|
260
|
+
raise InvalidInstructionError, 'cannot `import` from `/`' if path == '/' && !force
|
261
|
+
raise InvalidInstructionError, '`import` requires a `from:` option' if from.nil?
|
262
|
+
log_step('import', path, from: from.last_image.id)
|
263
|
+
|
264
|
+
total_size = 0
|
265
|
+
|
266
|
+
if spool
|
267
|
+
spool_file = Tempfile.new('drydock')
|
268
|
+
log_info("Spooling to #{spool_file.path}")
|
269
|
+
|
270
|
+
from.send(:chain).run("# EXPORT #{path}", no_commit: true) do |source_container|
|
271
|
+
source_container.archive_get(path + "/.") do |chunk|
|
272
|
+
spool_file.write(chunk.to_s).tap { |b| total_size += b }
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
spool_file.rewind
|
277
|
+
chain.run("# IMPORT #{path}", no_cache: true) do |target_container|
|
278
|
+
target_container.archive_put(path) do |output|
|
279
|
+
output.write(spool_file.read)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
spool_file.close
|
284
|
+
else
|
285
|
+
chain.run("# IMPORT #{path}", no_cache: true) do |target_container|
|
286
|
+
target_container.archive_put(path) do |output|
|
287
|
+
from.send(:chain).run("# EXPORT #{path}", no_commit: true) do |source_container|
|
288
|
+
source_container.archive_get(path + "/.") do |chunk|
|
289
|
+
output.write(chunk.to_s).tap { |b| total_size += b }
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
log_info("Imported #{Formatters.number(total_size)} bytes")
|
297
|
+
end
|
298
|
+
|
299
|
+
# The last image object built in this project.
|
300
|
+
def last_image
|
301
|
+
chain ? chain.last_image : nil
|
302
|
+
end
|
303
|
+
|
304
|
+
# Create a new directory specified by `path`. When `chmod` is given, the new
|
305
|
+
# directory will be chmodded. Otherwise, the default umask is used to determine
|
306
|
+
# the path's mode.
|
307
|
+
def mkdir(path, chmod: nil)
|
308
|
+
if chmod
|
309
|
+
run "mkdir -p #{path} && chmod #{chmod} #{path}"
|
310
|
+
else
|
311
|
+
run "mkdir -p #{path}"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# TODO(rpasay): on_build instructions should be deferred to the end
|
316
|
+
def on_build(instruction = nil, &blk)
|
317
|
+
requires_from!(:on_build)
|
318
|
+
log_step('on_build', instruction)
|
319
|
+
chain.run("# ON_BUILD #{instruction}", on_build: instruction)
|
320
|
+
self
|
321
|
+
end
|
322
|
+
|
323
|
+
# This instruction is used to run the command `cmd` against the current
|
324
|
+
# project. The `opts` may be one of:
|
325
|
+
#
|
326
|
+
# * `no_commit`, when true, the container will not be committed to a
|
327
|
+
# new image. Most of the time, you want this to be false (default).
|
328
|
+
# * `no_cache`, when true, the container will be rebuilt every time.
|
329
|
+
# Most of the time, you want this to be false (default). When
|
330
|
+
# `no_commit` is true, this option is automatically set to true.
|
331
|
+
# * `env`, which can be used to specify a set of environment variables.
|
332
|
+
# For normal usage, you should use the `env` or `envs` instructions.
|
333
|
+
# * `expose`, which can be used to specify a set of ports to expose. For
|
334
|
+
# normal usage, you should use the `expose` instruction instead.
|
335
|
+
# * `on_build`, which can be used to specify low-level on-build options. For
|
336
|
+
# normal usage, you should use the `on_build` instruction instead.
|
337
|
+
def run(cmd, opts = {}, &blk)
|
338
|
+
requires_from!(:run)
|
339
|
+
|
340
|
+
cmd = build_cmd(cmd)
|
341
|
+
|
342
|
+
run_opts = opts.dup
|
343
|
+
run_opts[:author] = opts[:author] || build_opts[:author]
|
344
|
+
run_opts[:comment] = opts[:comment] || build_opts[:comment]
|
345
|
+
|
346
|
+
log_step('run', cmd, run_opts)
|
347
|
+
chain.run(cmd, run_opts, &blk)
|
348
|
+
self
|
349
|
+
end
|
350
|
+
|
351
|
+
# Set project options.
|
352
|
+
def set(key, value = nil, &blk)
|
353
|
+
key = key.to_sym
|
354
|
+
raise ArgumentError, "unknown option #{key.inspect}" unless build_opts.key?(key)
|
355
|
+
raise ArgumentError, "one of value or block is required" if value.nil? && blk.nil?
|
356
|
+
raise ArgumentError, "only one of value or block may be provided" if value && blk
|
357
|
+
|
358
|
+
build_opts[key] = value || blk
|
359
|
+
end
|
360
|
+
|
361
|
+
# Tag the current state of the project with a repo and tag.
|
362
|
+
#
|
363
|
+
# When `force` is false (default), this instruction will raise an error if
|
364
|
+
# the tag already exists. When true, the tag will be overwritten without
|
365
|
+
# any warnings.
|
366
|
+
def tag(repo, tag = 'latest', force: false)
|
367
|
+
requires_from!(:tag)
|
368
|
+
log_step('tag', repo, tag, force: force)
|
369
|
+
|
370
|
+
chain.tag(repo, tag, force: force)
|
371
|
+
self
|
372
|
+
end
|
373
|
+
|
374
|
+
# Use a `plugin` to issue other commands. The block form can be used to issue
|
375
|
+
# multiple commands:
|
376
|
+
#
|
377
|
+
# with Plugins::APK do |apk|
|
378
|
+
# apk.update
|
379
|
+
# end
|
380
|
+
#
|
381
|
+
# In cases of single commands, the above is the same as:
|
382
|
+
#
|
383
|
+
# with(Plugins::APK).update
|
384
|
+
def with(plugin, &blk)
|
385
|
+
(@plugins[plugin] ||= plugin.new(self)).tap do |instance|
|
386
|
+
yield instance if block_given?
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
private
|
391
|
+
attr_reader :chain, :build_opts, :stream_monitor
|
392
|
+
|
393
|
+
def build_cmd(cmd)
|
394
|
+
if @run_path.empty?
|
395
|
+
cmd.to_s.strip
|
396
|
+
else
|
397
|
+
"cd #{@run_path.join('/')} && #{cmd}".strip
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def cache
|
402
|
+
build_opts[:cache] ||= ObjectCaches::NoCache.new
|
403
|
+
end
|
404
|
+
|
405
|
+
def ignorefile
|
406
|
+
@ignorefile ||= IgnorefileDefinition.new(build_opts[:ignorefile])
|
407
|
+
end
|
408
|
+
|
409
|
+
def log_info(msg, indent: 0)
|
410
|
+
Drydock.logger.info(indent: indent, message: msg)
|
411
|
+
end
|
412
|
+
|
413
|
+
def log_step(op, *args)
|
414
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
415
|
+
optstr = opts.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
|
416
|
+
|
417
|
+
argstr = args.map(&:inspect).join(', ')
|
418
|
+
|
419
|
+
Drydock.logger.info("##{chain ? chain.serial : 0}: #{op}(#{argstr}#{optstr.empty? ? '' : ", #{optstr}"})")
|
420
|
+
end
|
421
|
+
|
422
|
+
def requires_from!(instruction)
|
423
|
+
raise InvalidInstructionError, "`#{instruction}` cannot be called before `from`" unless chain
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Drydock
|
5
|
+
class RuntimeOptions
|
6
|
+
|
7
|
+
attr_accessor :build_opts, :cache, :includes, :log_level, :read_timeout
|
8
|
+
|
9
|
+
def self.parse!(args)
|
10
|
+
opts = new
|
11
|
+
|
12
|
+
parser = OptionParser.new do |cfg|
|
13
|
+
cfg.banner = "Usage: #{$0} [options...] [drydock-filename]"
|
14
|
+
|
15
|
+
cfg.separator ''
|
16
|
+
cfg.separator 'Runtime / build options:'
|
17
|
+
|
18
|
+
cfg.on('-b', '--build-opts KEY=VALUE', 'KEY=VALUE build-time options', 'can be specified multiple times') do |kv|
|
19
|
+
key, value = kv.split('=', 2)
|
20
|
+
opts.build_opts[key.to_s] = value
|
21
|
+
opts.build_opts[key.to_sym] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
cfg.on('-C', '--no-cache', 'Disable the build cache') do
|
25
|
+
opts.cache = false
|
26
|
+
end
|
27
|
+
|
28
|
+
cfg.on('-i', '--include PATH', 'Load custom plugins from PATH') do |path|
|
29
|
+
opts.includes << path
|
30
|
+
end
|
31
|
+
|
32
|
+
cfg.separator ''
|
33
|
+
cfg.separator 'General options:'
|
34
|
+
|
35
|
+
cfg.on('-h', '--help', 'Show this help message') do
|
36
|
+
puts cfg
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
cfg.on('-q', '--quiet', 'Run silently, except for errors') do |value|
|
41
|
+
opts.log_level = Logger::ERROR
|
42
|
+
end
|
43
|
+
|
44
|
+
cfg.on('-t SECONDS', '--timeout SECONDS',
|
45
|
+
"Set transaction timeout to SECONDS (default = #{opts.read_timeout})") do |value|
|
46
|
+
opts.read_timeout = value.to_i || 60
|
47
|
+
end
|
48
|
+
|
49
|
+
cfg.on('-v', '--verbose', 'Run verbosely') do |value|
|
50
|
+
opts.log_level = Logger::DEBUG
|
51
|
+
end
|
52
|
+
|
53
|
+
cfg.on('-V', '--version', 'Show version') do
|
54
|
+
puts Drydock.banner
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
parser.parse!(args)
|
60
|
+
opts.set!
|
61
|
+
opts
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@build_opts = {}
|
66
|
+
@cache = true
|
67
|
+
@includes = []
|
68
|
+
@log_level = Logger::INFO
|
69
|
+
|
70
|
+
@read_timeout = Excon.defaults[:read_timeout]
|
71
|
+
@read_timeout = 120 if @read_timeout < 120
|
72
|
+
end
|
73
|
+
|
74
|
+
def set!
|
75
|
+
Excon.defaults[:read_timeout] = self.read_timeout
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class StreamMonitor
|
4
|
+
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
CONTAINER_EVENTS = %i(
|
8
|
+
attach
|
9
|
+
commit copy create
|
10
|
+
destroy die
|
11
|
+
exec_create exec_start export
|
12
|
+
kill
|
13
|
+
oom
|
14
|
+
pause
|
15
|
+
rename resize restart
|
16
|
+
start stop
|
17
|
+
top
|
18
|
+
unpause
|
19
|
+
)
|
20
|
+
|
21
|
+
IMAGE_EVENTS = %i(delete import pull push tag untag)
|
22
|
+
|
23
|
+
def_delegators :@thread, :alive?, :join, :kill, :run
|
24
|
+
|
25
|
+
def self.event_type_for(type)
|
26
|
+
case type.to_sym
|
27
|
+
when *CONTAINER_EVENTS
|
28
|
+
:container
|
29
|
+
when *IMAGE_EVENTS
|
30
|
+
:image
|
31
|
+
else
|
32
|
+
:object
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(event_handler)
|
37
|
+
@thread = Thread.new do
|
38
|
+
previous_ids = {}
|
39
|
+
serial_no = 0
|
40
|
+
|
41
|
+
Docker::Event.stream do |event|
|
42
|
+
serial_no += 1
|
43
|
+
|
44
|
+
is_old = previous_ids.key?(event.id)
|
45
|
+
event_type = self.class.event_type_for(event.status)
|
46
|
+
event_handler.call(event, !is_old, serial_no, event_type)
|
47
|
+
|
48
|
+
previous_ids[event.id] = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module Drydock
|
3
|
+
class TarWriter < ::Gem::Package::TarWriter
|
4
|
+
|
5
|
+
def add_entry(name, mode: 0644, mtime: Time.now, uid: 0, gid: 0)
|
6
|
+
check_closed
|
7
|
+
|
8
|
+
raise Gem::Package::NonSeekableIO unless @io.respond_to?(:pos=)
|
9
|
+
|
10
|
+
name, prefix = split_name(name)
|
11
|
+
|
12
|
+
init_pos = @io.pos
|
13
|
+
@io.write "\0" * 512 # placeholder for the header
|
14
|
+
|
15
|
+
yield RestrictedStream.new(@io) if block_given?
|
16
|
+
|
17
|
+
size = @io.pos - init_pos - 512
|
18
|
+
|
19
|
+
remainder = (512 - (size % 512)) % 512
|
20
|
+
@io.write "\0" * remainder
|
21
|
+
|
22
|
+
final_pos = @io.pos
|
23
|
+
@io.pos = init_pos
|
24
|
+
|
25
|
+
header = Gem::Package::TarHeader.new(
|
26
|
+
name: name, mode: mode,
|
27
|
+
size: size, prefix: prefix, mtime: mtime
|
28
|
+
)
|
29
|
+
@io.write header
|
30
|
+
@io.pos = final_pos
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/drydock.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'docker'
|
3
|
+
require 'excon'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
require_relative 'drydock/docker_api_patch'
|
7
|
+
|
8
|
+
module Drydock # :nodoc:
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'drydock/drydock'
|
12
|
+
require_relative 'drydock/logger'
|
13
|
+
require_relative 'drydock/errors'
|
14
|
+
require_relative 'drydock/formatters'
|
15
|
+
require_relative 'drydock/runtime_options'
|
16
|
+
|
17
|
+
require_relative 'drydock/container_config'
|
18
|
+
require_relative 'drydock/image_repository'
|
19
|
+
|
20
|
+
require_relative 'drydock/cli_flags'
|
21
|
+
require_relative 'drydock/file_manager'
|
22
|
+
require_relative 'drydock/ignorefile_definition'
|
23
|
+
require_relative 'drydock/phase'
|
24
|
+
require_relative 'drydock/phase_chain'
|
25
|
+
require_relative 'drydock/project'
|
26
|
+
require_relative 'drydock/stream_monitor'
|
27
|
+
require_relative 'drydock/tar_writer'
|
28
|
+
|
29
|
+
require_relative 'drydock/object_caches/filesystem_cache'
|
30
|
+
require_relative 'drydock/object_caches/in_memory_cache'
|
31
|
+
require_relative 'drydock/object_caches/no_cache'
|
32
|
+
|
33
|
+
require_relative 'drydock/plugins/apk'
|
34
|
+
require_relative 'drydock/plugins/npm'
|
35
|
+
require_relative 'drydock/plugins/rubygems'
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello, World!
|
Binary file
|
data/spec/assets/test.sh
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
RSpec.describe Drydock::CliFlags do
|
3
|
+
|
4
|
+
describe '#to_s' do
|
5
|
+
|
6
|
+
context 'single-character flags' do
|
7
|
+
subject { described_class.new(v: true, t: false, a: nil).to_s }
|
8
|
+
it { is_expected.to eq('-v -t -a ') }
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'multiple-character positive flags' do
|
12
|
+
subject { described_class.new(verbose: true).to_s }
|
13
|
+
it { is_expected.to eq('--verbose ') }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'multiple-character negative flags' do
|
17
|
+
subject { described_class.new(verbose: false).to_s }
|
18
|
+
it { is_expected.to eq('--no-verbose ') }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'multiple-character string flags without whitespace' do
|
22
|
+
subject { described_class.new(tag: 'drydock/test:1.0').to_s }
|
23
|
+
it { is_expected.to eq('--tag drydock/test:1.0') }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'multiple-character string flags with whitespace' do
|
27
|
+
subject { described_class.new(author: 'John Doe').to_s }
|
28
|
+
it { is_expected.to eq('--author "John Doe"') }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'multiple-character numeric flags' do
|
32
|
+
subject { described_class.new(value: 2.5).to_s }
|
33
|
+
it { is_expected.to eq('--value 2.5') }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|