dry-dock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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