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,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,4 @@
1
+ 60fde9c2310b0d4cad4dab8d126b04387efba289 spec/assets/hello-world.txt
2
+ 36df6ea2da70728177e8cc5acd2e947bf745990d spec/assets/MANIFEST
3
+ 018105e208c48ac432f6c1798681041fc28b9f0c spec/assets/sample.tar
4
+ a5e6419ea98d8428ff1d4514197769286e70e220 spec/assets/test.sh
@@ -0,0 +1 @@
1
+ Hello, World!
Binary file
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ date
@@ -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