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