dry-dock 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5de350e13158e99318eabbb68b7d507804cb0c9c
4
- data.tar.gz: df476fedf7609cf0734aa92aaae0ab71235aa2df
3
+ metadata.gz: 5f2e43155c83ad0b25eb7ee30a1da32697c0b270
4
+ data.tar.gz: fe24886ba40a3d093989f2c03f0068026698dcdf
5
5
  SHA512:
6
- metadata.gz: 562e59af24727a8d9a23a2e8a677386b70a5bcadd48969af33a0c595c3c5592d68bd90cf72cc913a111bf4c27127ac03e051ce01aa114aa4d7db1e5189522183
7
- data.tar.gz: f5807ebf5764c81c1fdef2e9c7650fc0e57f97608e305c586a18dcfe04fb46ef9a450d4c00085c1be94f181c1353df0da1719a4bf7f141d1f1e2bd23a4e64ba6
6
+ metadata.gz: daf3a6b9aea72c10fb072740f97c2682df12f6378d88f768d0de43ffd16fd4f2f4bb92cb43190598e9d3e04d6c48bdd964c6edcd7ea8e8c1594e5c883a9968ba
7
+ data.tar.gz: b300b19d4869daf770dd612f7524ce5a8cbf909d3812be2feb9661ee42a01f55ec639071e2f2515c4d6c6b7e4d23823321be501bc234457cb26597e8ad74dafe
data/.codeclimate.yml ADDED
@@ -0,0 +1,9 @@
1
+ engines:
2
+ rubocop:
3
+ enabled: true
4
+ ratings:
5
+ paths:
6
+ - lib/**/*
7
+ exclude_paths:
8
+ - spec/**/*
9
+ - vendor/**/*
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ sudo: required
2
+ services:
3
+ - docker
4
+ language: ruby
5
+ rvm:
6
+ - 2.1.5
7
+ before_install:
8
+ - sudo service docker stop
9
+ - sudo apt-get purge lxc-docker-1.7.0
10
+ - curl -sSL https://get.docker.com/ | sudo sh
11
+ - docker version
12
+ - docker info
13
+ script: bundle exec rspec
14
+ addons:
15
+ code_climate:
16
+ repo_token: e1dfe86e3cc086b44532f7f1122f585fe828e141767ac41e833b365b21fd275a
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup markdown
2
+ --readme README.md
data/Gemfile CHANGED
@@ -11,6 +11,7 @@ group :development do
11
11
  end
12
12
 
13
13
  group :test do
14
+ gem 'codeclimate-test-reporter', require: false
14
15
  gem 'rspec', '~> 3.0'
15
16
  gem 'rspec-collection_matchers'
16
17
  gem 'fakefs', require: false
data/Gemfile.lock CHANGED
@@ -3,6 +3,8 @@ GEM
3
3
  specs:
4
4
  addressable (2.3.8)
5
5
  builder (3.2.2)
6
+ codeclimate-test-reporter (0.4.8)
7
+ simplecov (>= 0.7.1, < 1.0.0)
6
8
  coderay (1.1.0)
7
9
  descendants_tracker (0.0.4)
8
10
  thread_safe (~> 0.3, >= 0.3.1)
@@ -87,6 +89,7 @@ PLATFORMS
87
89
  ruby
88
90
 
89
91
  DEPENDENCIES
92
+ codeclimate-test-reporter
90
93
  docker-api (~> 1.22)
91
94
  excon (~> 0.45)
92
95
  fakefs
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # drydock
2
2
 
3
- (WORK IN PROGRESS)
3
+ WORK IN PROGRESS — ALPHA-RELEASE SOFTWARE
4
+ SOME FEATURES REQUIRE DOCKER 1.8.0 OR NEWER
5
+
6
+ ![Automated Build Status](https://travis-ci.org/ripta/drydock.svg)
4
7
 
5
8
  A ruby DSL to build your own docker images. Images are built based on instructions
6
9
  contained in your project's `Drydockfile`.
@@ -38,8 +41,10 @@ Drydockfiles are written in ruby.
38
41
 
39
42
  ## Production Installation
40
43
 
41
- Either (a) `gem install drydock`, or (b) add "drydock" to your project's Gemfile,
42
- and run `bundle`.
44
+ Either (a) `gem install dry-dock`, or (b) add "dry-dock" to your project's Gemfile,
45
+ and run `bundle`. Sorry, but the gem name `drydock` was already taken by a defunct
46
+ gem, and I'm too lazy to contact them; the binary and name of the project, however,
47
+ are both `drydock`.
43
48
 
44
49
  In your project's root directory, you'll want to create a `Drydockfile` containing
45
50
  drydock functions. When you're ready, build an image using:
@@ -67,9 +72,28 @@ $ git clone git@github.com:ripta/drydock.git
67
72
  $ bundle
68
73
  ```
69
74
 
75
+ ## Drydockfile Syntax
76
+
77
+ As previously mentioned, Drydockfiles are ruby. The contents of Drydockfile are
78
+ evaluated in the context of an instance of `Drydock::Project`; you can refer to
79
+ the documentation for it for more in-depth information.
80
+
81
+ Because Drydockfiles are ruby, most constructs should work as-is: you can declare
82
+ constants and refer to them later; call `Kernel#abort` to exit the program and
83
+ stop the build; and write plugins to be called from within the Drydockfile.
84
+
85
+ It would help if you understand ruby and
86
+ [Dockerfiles](https://docs.docker.com/reference/builder/) before jumping in.
87
+
88
+ All instructions are evaluated in the order that they are seen; syntax errors or
89
+ any logical errors might not be caught until execution arrives at that point.
90
+
91
+ For a complete and updated list of Drydockfile instructions, see the public API
92
+ methods of the {Drydock::Project} class or head to the
93
+ [automatically-generated ruby docs](http://www.rubydoc.info/gems/dry-dock).
94
+
95
+
70
96
  ## Roadmap
71
97
 
72
98
  1. Customizable caching subsystem.
73
- 2. Derived docker images from a previous build step.
74
- 3. Composable docker images.
75
99
  4. Customizable caching rules.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
data/dry-dock.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: dry-dock 0.1.4 ruby lib
5
+ # stub: dry-dock 0.1.5 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "dry-dock"
9
- s.version = "0.1.4"
9
+ s.version = "0.1.5"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Ripta Pasay"]
14
- s.date = "2015-10-02"
14
+ s.date = "2015-10-07"
15
15
  s.description = "A Dockerfile-replacement DSL for building complex images"
16
16
  s.email = "github@r8y.org"
17
17
  s.executables = ["drydock", "json-test-consumer.rb", "json-test-producer.rb", "test-tar-writer-digest.rb"]
@@ -20,9 +20,12 @@ Gem::Specification.new do |s|
20
20
  "README.md"
21
21
  ]
22
22
  s.files = [
23
+ ".codeclimate.yml",
23
24
  ".dockerignore",
24
25
  ".pryrc",
25
26
  ".rspec",
27
+ ".travis.yml",
28
+ ".yardopts",
26
29
  "Dockerfile",
27
30
  "Gemfile",
28
31
  "Gemfile.lock",
@@ -29,12 +29,6 @@ module Drydock
29
29
  end
30
30
  end
31
31
 
32
- def self.build_on_chain(chain, opts = {}, &blk)
33
- Project.new(opts.merge(chain: chain)).tap do |project|
34
- project.instance_eval(&blk) if blk
35
- end
36
- end
37
-
38
32
  def self.from(repo, opts = {}, &blk)
39
33
  opts = opts.clone
40
34
  tag = opts.delete(:tag, 'latest')
@@ -6,4 +6,6 @@ module Drydock
6
6
 
7
7
  class ExecutionError < OperationError; end
8
8
  class InvalidCommandExecutionError < ExecutionError; end
9
+
10
+ class InsufficientVersionError < OperationError; end
9
11
  end
@@ -9,9 +9,13 @@ module Drydock
9
9
  def self.build_commit_opts(opts = {})
10
10
  {}.tap do |commit|
11
11
  if opts.key?(:command)
12
- commit['run'] = {
13
- Cmd: opts[:command]
14
- }
12
+ commit['run'] ||= {}
13
+ commit['run'][:Cmd] = opts[:command]
14
+ end
15
+
16
+ if opts.key?(:entrypoint)
17
+ commit['run'] ||= {}
18
+ commit['run'][:Entrypoint] = opts[:entrypoint]
15
19
  end
16
20
 
17
21
  commit[:author] = opts.fetch(:author, '') if opts.key?(:author)
@@ -127,6 +131,22 @@ module Drydock
127
131
  new(Docker::Image.create(build_pull_opts(repo, tag)))
128
132
  end
129
133
 
134
+ def self.propagate_config!(src_image, config_name, opts, opt_key)
135
+ if opts.key?(opt_key)
136
+ Drydock.logger.info("Command override: #{opts[opt_key].inspect}")
137
+ else
138
+ src_image.refresh!
139
+ if src_image.info && src_image.info.key?('Config')
140
+ src_image_config = src_image.info['Config']
141
+ opts[opt_key] = src_image_config[config_name] if src_image_config.key?(config_name)
142
+ end
143
+
144
+ Drydock.logger.debug(message: "Command retrieval: #{opts[opt_key].inspect}")
145
+ Drydock.logger.debug(message: "Source image info: #{src_image.info.class} #{src_image.info.inspect}")
146
+ Drydock.logger.debug(message: "Source image config: #{src_image.info['Config'].inspect}")
147
+ end
148
+ end
149
+
130
150
  def initialize(from, parent = nil)
131
151
  @chain = []
132
152
  @from = from
@@ -231,20 +251,8 @@ module Drydock
231
251
  Drydock.logger.info(message: "Skipping commit phase")
232
252
  ephemeral_containers << container
233
253
  else
234
- if opts.key?(:command)
235
- Drydock.logger.info("Command override: #{opts[:command].inspect}")
236
- else
237
- src_image.refresh!
238
- if src_image.info && src_image.info.key?('Config')
239
- src_image_config = src_image.info['Config']
240
- opts[:command] = src_image_config['Cmd'] if src_image_config.key?('Cmd')
241
- end
242
-
243
- Drydock.logger.debug(message: "Command retrieval: #{opts[:command].inspect}")
244
- Drydock.logger.debug(message: "Source image info: #{src_image.info.class} #{src_image.info.inspect}")
245
- Drydock.logger.debug(message: "Source image config: #{src_image.info['Config'].inspect}")
246
- end
247
-
254
+ self.class.propagate_config!(src_image, 'Cmd', opts, :command)
255
+ self.class.propagate_config!(src_image, 'Entrypoint', opts, :entrypoint)
248
256
  commit_config = self.class.build_commit_opts(opts)
249
257
 
250
258
  result = container.commit(commit_config)
@@ -7,11 +7,23 @@ module Drydock
7
7
  author: nil,
8
8
  cache: nil,
9
9
  event_handler: false,
10
- ignorefile: '.dockerignore',
11
- label: nil,
12
- logs: false
10
+ ignorefile: '.dockerignore'
13
11
  }
14
12
 
13
+ # Create a new project. **Do not use directly.**
14
+ #
15
+ # @api private
16
+ # @param [Hash] build_opts Build-time options
17
+ # @option build_opts [Boolean] :auto_remove Whether intermediate images
18
+ # created during the build of this project should be automatically removed.
19
+ # @option build_opts [String] :author The default author field when an
20
+ # author is not provided explicitly with {#author}.
21
+ # @option build_opts [ObjectCaches::Base] :cache An object cache manager.
22
+ # @option build_opts [#call] :event_handler A handler that responds to a
23
+ # `#call` message with four arguments: `[event, is_new, serial_no, event_type]`
24
+ # most useful to override logging or
25
+ # @option build_opts [PhaseChain] :chain A phase chain manager.
26
+ # @option build_opts [String] :ignorefile The name of the ignore-file to load.
15
27
  def initialize(build_opts = {})
16
28
  @chain = build_opts.key?(:chain) && build_opts.delete(:chain).derive
17
29
  @plugins = {}
@@ -27,25 +39,66 @@ module Drydock
27
39
 
28
40
  # Set the author for commits. This is not an instruction, per se, and only
29
41
  # takes into effect after instructions that cause a commit.
42
+ #
43
+ # This instruction affects **all instructions after it**, but nothing before it.
44
+ #
45
+ # At least one of `name` or `email` must be given. If one is provided, the
46
+ # other is optional.
47
+ #
48
+ # If no author instruction is provided, the author field is left blank by default.
49
+ #
50
+ # @param [String] name The name of the author or maintainer of the image.
51
+ # @param [String] email The email of the author or maintainer.
52
+ # @raise [InvalidInstructionArgumentError] when neither name nor email is provided
30
53
  def author(name: nil, email: nil)
54
+ if (name.nil? || name.empty?) && (email.nil? || name.empty?)
55
+ raise InvalidInstructionArgumentError, 'at least one of `name:` or `email:` must be provided'
56
+ end
57
+
31
58
  value = email ? "#{name} <#{email}>" : name.to_s
32
59
  set :author, value
33
60
  end
34
61
 
35
- # Retrieve the current build ID for this project.
62
+ # Retrieve the current build ID for this project. If no image has been built,
63
+ # returns the string '0'.
36
64
  def build_id
37
65
  chain ? chain.serial : '0'
38
66
  end
39
67
 
40
68
  # Change directories for operations that require a directory.
41
- def cd(path, &blk)
69
+ #
70
+ # @param [String] path The path to change directories to.
71
+ # @yield block containing instructions to run inside the new directory
72
+ def cd(path = '/', &blk)
42
73
  @run_path << path
43
- blk.call
74
+ blk.call if blk
44
75
  ensure
45
76
  @run_path.pop
46
77
  end
47
78
 
48
- # Set the command to automatically execute when the image is run.
79
+ # Set the command that is automatically executed by default when the image
80
+ # is run through the `docker run` command.
81
+ #
82
+ # {#cmd} corresponds to the `CMD` Dockerfile instruction. This instruction
83
+ # does **not** run the command, but rather provides the default command to
84
+ # be run when the image is run without specifying a command.
85
+ #
86
+ # As with the `CMD` Dockerfile instruction, the {#cmd} instruction has three
87
+ # forms:
88
+ #
89
+ # * `['executable', 'param1', 'param2', '...']`, which would run the
90
+ # executable directly when the image is run;
91
+ # * `['param1', 'param2', '...']`, which would pass the parameters to the
92
+ # executable provided in the {#entrypoint} instruction; or
93
+ # * `'executable param1 param2'`, which would run the executable inside
94
+ # a subshell.
95
+ #
96
+ # The first two forms are preferred over the last one. See also {#entrypoint}
97
+ # to see how the instruction interacts with this one.
98
+ #
99
+ # @param [String, Array<String>] command The command set to run. When a
100
+ # `String` is provided, the command is run inside a shell (`/bin/sh`).
101
+ # When an `Array` is given, the command is run as-is given.
49
102
  def cmd(command)
50
103
  requires_from!(:cmd)
51
104
  log_step('cmd', command)
@@ -61,14 +114,33 @@ module Drydock
61
114
  # Copies files from `source_path` on the the build machine, into `target_path`
62
115
  # in the container. This instruction automatically commits the result.
63
116
  #
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.
117
+ # The `copy` instruction always respects the `ignorefile`.
67
118
  #
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.
119
+ # When `no_cache` is `true` (also see parameter explanation below), then any
120
+ # instruction after {#copy} will also be rebuilt *every time*.
70
121
  #
71
- # The `copy` instruction always respects the `ignorefile`.
122
+ # @param [String] source_path The source path on the build machine (where
123
+ # `drydock` is running) from which to copy files.
124
+ # @param [String] target_path The target path inside the image to which to
125
+ # copy the files. This path **must already exist** before copying begins.
126
+ # @param [Integer, Boolean] chmod When `false` (the default), the original file
127
+ # mode from its source file is kept when copying into the container. Otherwise,
128
+ # the mode provided (in integer octal form) will be used to override *all*
129
+ # file and directory modes.
130
+ # @param [Boolean] no_cache When `false` (the default), the hash digest of the
131
+ # source path—taking into account all its files, directories, and contents—is
132
+ # used as the cache key. When `true`, the image is rebuilt *every* time.
133
+ # @param [Boolean] recursive When `true`, then `source_path` is expected to be
134
+ # a directory, at which point all its contents would be recursively searched.
135
+ # When `false`, then `source_path` is expected to be a file.
136
+ #
137
+ # @raise [InvalidInstructionError] when the `source_path` does not exist
138
+ # @raise [InvalidInstructionError] when the `source_path` is an empty directory
139
+ # with nothing to copy
140
+ # @raise [InvalidInstructionError] when the `target_path` does not exist in the
141
+ # container
142
+ # @raise [InvalidInstructionError] when the `target_path` exists in the container,
143
+ # but is not actually a directory
72
144
  def copy(source_path, target_path, chmod: false, no_cache: false, recursive: true)
73
145
  requires_from!(:copy)
74
146
  log_step('copy', source_path, target_path, chmod: (chmod ? sprintf('%o', chmod) : false))
@@ -128,12 +200,18 @@ module Drydock
128
200
  self
129
201
  end
130
202
 
203
+ # Destroy the images and containers created, and attempt to return the docker
204
+ # state as it was before the project.
205
+ #
206
+ # @api private
131
207
  def destroy!(force: false)
132
208
  chain.destroy!(force: force) if chain
133
209
  finalize!(force: force)
134
210
  end
135
211
 
136
212
  # Meta instruction to signal to the builder that the build is done.
213
+ #
214
+ # @api private
137
215
  def done!
138
216
  throw :done
139
217
  end
@@ -173,7 +251,101 @@ module Drydock
173
251
  self
174
252
  end
175
253
 
176
- # Set an environment variable.
254
+ # **This instruction is *optional*, but if specified, must appear at the
255
+ # beginning of the file.**
256
+ #
257
+ # This instruction is used to restrict the version of `drydock` required to
258
+ # run the `Drydockfile`. When not specified, any version of `drydock` is
259
+ # allowed to run the file.
260
+ #
261
+ # The version specifier understands any bundler-compatible (and therefore
262
+ # [gem-compatible](http://guides.rubygems.org/patterns/#semantic-versioning))
263
+ # version specification; it even understands the twiddle-waka (`~>`) operator.
264
+ #
265
+ # @example
266
+ # drydock '~> 0.5'
267
+ # @param [String] version The version specification to use.
268
+ def drydock(version = '>= 0')
269
+ raise InvalidInstructionError, '`drydock` must be called before `from`' if chain
270
+ log_step('drydock', version)
271
+
272
+ requirement = Gem::Requirement.create(version)
273
+ current = Gem::Version.create(Drydock.version)
274
+
275
+ unless requirement.satisfied_by?(current)
276
+ raise InsufficientVersionError, "build requires #{version.inspect}, but you're on #{Drydock.version.inspect}"
277
+ end
278
+
279
+ self
280
+ end
281
+
282
+ # Sets the entrypoint command for an image.
283
+ #
284
+ # {#entrypoint} corresponds to the `ENTRYPOINT` Dockerfile instruction. This
285
+ # instruction does **not** run the command, but rather provides the default
286
+ # command to be run when the image is run without specifying a command.
287
+ #
288
+ # As with the {#cmd} instruction, {#entrypoint} has three forms, of which the
289
+ # first two forms are preferred over the last one.
290
+ #
291
+ # @param (see #cmd)
292
+ def entrypoint(command)
293
+ requires_from!(:entrypoint)
294
+ log_step('entrypoint', command)
295
+
296
+ unless command.is_a?(Array)
297
+ command = ['/bin/sh', '-c', command.to_s]
298
+ end
299
+
300
+ chain.run("# ENTRYPOINT #{command.inspect}", entrypoint: command)
301
+ self
302
+ end
303
+
304
+ # Set an environment variable, which will be persisted in future images
305
+ # (unless it is specifically overwritten) and derived projects.
306
+ #
307
+ # Subsequent commands can refer to the environment variable by preceeding
308
+ # the variable with a `$` sign, e.g.:
309
+ #
310
+ # ```
311
+ # env 'APP_ROOT', '/app'
312
+ # mkdir '$APP_ROOT'
313
+ # run ['some-command', '--install-into=$APP_ROOT']
314
+ # ```
315
+ #
316
+ # Multiple calls to this instruction will build on top of one another.
317
+ # That is, after the following two instructions:
318
+ #
319
+ # ```
320
+ # env 'APP_ROOT', '/app'
321
+ # env 'BUILD_ROOT', '/build'
322
+ # ```
323
+ #
324
+ # the resulting image will have both `APP_ROOT` and `BUILD_ROOT` set. Later
325
+ # instructions overwrites previous instructions of the same name:
326
+ #
327
+ # ```
328
+ # # 1
329
+ # env 'APP_ROOT', '/app'
330
+ # # 2
331
+ # env 'APP_ROOT', '/home/jdoe/app'
332
+ # # 3
333
+ # ```
334
+ #
335
+ # At `#1`, `APP_ROOT` is not set (assuming no other instruction comes before
336
+ # it). At `#2`, `APP_ROOT` is set to '/app'. At `#3`, `APP_ROOT` is set to
337
+ # `/home/jdoe/app`, and its previous value is no longer available.
338
+ #
339
+ # Note that the environment variable is not evaluated in ruby; in fact, the
340
+ # `$` sign should be passed as-is to the instruction. As with shell
341
+ # programming, the variable name should **not** be preceeded by the `$`
342
+ # sign when declared, but **must be** when referenced.
343
+ #
344
+ # @param [String] name The name of the environment variable. By convention,
345
+ # the name should be uppercased and underscored. The name should **not**
346
+ # be preceeded by a `$` sign in this context.
347
+ # @param [String] value The value of the variable. No extra quoting should be
348
+ # necessary here.
177
349
  def env(name, value)
178
350
  requires_from!(:env)
179
351
  log_step('env', name, value)
@@ -181,9 +353,31 @@ module Drydock
181
353
  self
182
354
  end
183
355
 
184
- # Set multiple environment variables at once. `pairs` should be a
185
- # hash-like enumerable.
186
- def envs(pairs = {})
356
+ # Set multiple environment variables at once. The values will be persisted in
357
+ # future images and derived projects, unless specifically overwritten.
358
+ #
359
+ # The following instruction:
360
+ #
361
+ # ```
362
+ # envs APP_ROOT: '/app', BUILD_ROOT: '/tmp/build'
363
+ # ```
364
+ #
365
+ # is equivalent to the more verbose:
366
+ #
367
+ # ```
368
+ # env 'APP_ROOT', '/app'
369
+ # env 'BUILD_ROOT', '/tmp/build'
370
+ # ```
371
+ #
372
+ # When the same key appears more than once in the same {#envs} instruction,
373
+ # the same rules for ruby hashes are used, which most likely (but not guaranteed
374
+ # between ruby version) means the last value set is used.
375
+ #
376
+ # See also notes for {#env}.
377
+ #
378
+ # @param [Hash, #map] pairs A hash-like enumerable, where `#map` yields exactly
379
+ # two elements. See {#env} for any restrictions of the name (key) and value.
380
+ def envs(pairs)
187
381
  requires_from!(:envs)
188
382
  log_step('envs', pairs)
189
383
 
@@ -192,13 +386,24 @@ module Drydock
192
386
  self
193
387
  end
194
388
 
195
- # Expose one or more ports.
389
+ # Expose one or more ports. The values will be persisted in future images
196
390
  #
197
391
  # When `ports` is specified, the format must be: ##/type where ## is the port
198
392
  # number and type is either tcp or udp. For example, "80/tcp", "53/udp".
199
393
  #
200
394
  # Otherwise, when the `tcp` or `udp` options are specified, only the port
201
395
  # numbers are required.
396
+ #
397
+ # @example Different ways of exposing port 53 UDP and ports 80 and 443 TCP:
398
+ # expose '53/udp', '80/tcp', '443/tcp'
399
+ # expose udp: 53, tcp: [80, 443]
400
+ # @param [Array<String>] ports An array of strings of port specifications.
401
+ # Each port specification must look like `#/type`, where `#` is the port
402
+ # number, and `type` is either `udp` or `tcp`.
403
+ # @param [Integer, Array<Integer>] tcp A TCP port number to open, or an array
404
+ # of TCP port numbers to open.
405
+ # @param [Integer, Array<Integer>] udp A UDP port number to open, or an array
406
+ # of UDP port numbers to open.
202
407
  def expose(*ports, tcp: [], udp: [])
203
408
  requires_from!(:expose)
204
409
 
@@ -210,10 +415,21 @@ module Drydock
210
415
  chain.run("# SET PORTS #{ports.inspect}", expose: ports)
211
416
  end
212
417
 
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.
418
+ # Build on top of the `from` image. **This must be the first instruction of
419
+ # the project,** although non-instructions may appear before this.
420
+ #
421
+ # If the `drydock` instruction is provided, `from` should come after it.
422
+ #
423
+ # @param [#to_s] repo The name of the repository, which may be any valid docker
424
+ # repository name, and may optionally include the registry address, e.g.,
425
+ # `johndoe/thing` or `quay.io/jane/app`. The name *must not* contain the tag name.
426
+ # @param [#to_s] tag The tag to use.
215
427
  def from(repo, tag = 'latest')
216
428
  raise InvalidInstructionError, '`from` must only be called once per project' if chain
429
+
430
+ repo = repo.to_s
431
+ tag = tag.to_s
432
+
217
433
  log_step('from', repo, tag)
218
434
  @chain = PhaseChain.from_repo(repo, tag)
219
435
  self
@@ -221,6 +437,8 @@ module Drydock
221
437
 
222
438
  # Finalize everything. This will be automatically invoked at the end of
223
439
  # the build, and should not be called manually.
440
+ #
441
+ # @api private
224
442
  def finalize!(force: false)
225
443
  if chain
226
444
  chain.finalize!(force: force)
@@ -234,13 +452,73 @@ module Drydock
234
452
  self
235
453
  end
236
454
 
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.
455
+ # Derive a new project based on the current state of the current project.
456
+ # This instruction returns the new project that can be referred to elsewhere,
457
+ # and most useful when combined with other inter-project instructions,
458
+ # such as {#import}.
459
+ #
460
+ # For example:
461
+ #
462
+ # ```
463
+ # from 'some-base-image'
464
+ #
465
+ # APP_ROOT = '/app'
466
+ # mkdir APP_ROOT
467
+ #
468
+ # # 1:
469
+ # ruby_build = derive {
470
+ # copy 'Gemfile', APP_ROOT
471
+ # run 'bundle install --path vendor'
472
+ # }
473
+ #
474
+ # # 2:
475
+ # js_build = derive {
476
+ # copy 'package.json', APP_ROOT
477
+ # run 'npm install'
478
+ # }
479
+ #
480
+ # # 3:
481
+ # derive {
482
+ # import APP_ROOT, from: ruby_build
483
+ # import APP_ROOT, from: js_build
484
+ # tag 'jdoe/app', 'latest', force: true
485
+ # }
486
+ # ```
487
+ #
488
+ # In the example above, an image is created with a new directory `/app`.
489
+ # From there, the build branches out into three directions:
490
+ #
491
+ # 1. Create a new project referred to as `ruby_build`. The result of this
492
+ # project is an image with `/app`, a `Gemfile` in it, and a `vendor`
493
+ # directory containing vendored gems.
494
+ # 2. Create a new project referred to as `js_build`. The result of this
495
+ # project is an image with `/app`, a `package.json` in it, and a
496
+ # `node_modules` directory containing vendored node.js modules.
497
+ # This project does **not** contain any of the contents of `ruby_build`.
498
+ # 3. Create an anonymous project containing only the empty `/app` directory.
499
+ # Onto that, we'll import the contents of `/app` from `ruby_build` into
500
+ # this anonymous project. We'll do the same with the contents of `/app`
501
+ # from `js_build`. Finally, the resulting image is given the tag
502
+ # `jdoe/app:latest`.
503
+ #
504
+ # Because each derived project lives on its own and only depends on the
505
+ # root project (whose end state is essentially the {#mkdir} instruction),
506
+ # when `Gemfile` changes but `package.json` does not, only the first
507
+ # derived project will be rebuilt (and following that, the third as well).
508
+ #
239
509
  def derive(opts = {}, &blk)
240
- Drydock.build_on_chain(chain, opts, &blk)
510
+ clean_opts = build_opts.delete_if { |k, v| v.nil? }
511
+ derive_opts = clean_opts.merge(opts).merge(chain: chain)
512
+
513
+ Project.new(derive_opts).tap do |project|
514
+ project.instance_eval(&blk) if blk
515
+ end
241
516
  end
242
517
 
243
518
  # Access to the logger object.
519
+ #
520
+ # @return [Logger] A logger object on which one could call `#info`, `#error`,
521
+ # and the likes.
244
522
  def logger
245
523
  Drydock.logger
246
524
  end
@@ -248,11 +526,14 @@ module Drydock
248
526
  # Import a `path` from a different project. The `from` option should be
249
527
  # project, usually the result of a `derive` instruction.
250
528
  #
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.
529
+ # @todo Add a #load method as an alternative to #import
530
+ # Doing so would allow importing a full container, including things from
531
+ # /etc, some of which may be mounted from the host.
532
+ #
533
+ # @todo Do not always append /. to the #archive_get calls
534
+ # We must check the type of `path` inside the container first.
535
+ #
536
+ # @todo Break this large method into smaller ones.
256
537
  def import(path, from: nil, force: false, spool: false)
257
538
  mkdir(path)
258
539
 
@@ -296,14 +577,18 @@ module Drydock
296
577
  log_info("Imported #{Formatters.number(total_size)} bytes")
297
578
  end
298
579
 
299
- # The last image object built in this project.
580
+ # Retrieve the last image object built in this project.
581
+ #
582
+ # If no image has been built, returns `nil`.
300
583
  def last_image
301
584
  chain ? chain.last_image : nil
302
585
  end
303
586
 
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.
587
+ # Create a new directory specified by `path` in the image.
588
+ #
589
+ # @param [String] path The path to create inside the image.
590
+ # @param [String] chmod The mode to which the new directory will be chmodded.
591
+ # If not specified, the default umask is used to determine the mode.
307
592
  def mkdir(path, chmod: nil)
308
593
  if chmod
309
594
  run "mkdir -p #{path} && chmod #{chmod} #{path}"
@@ -312,7 +597,7 @@ module Drydock
312
597
  end
313
598
  end
314
599
 
315
- # TODO(rpasay): on_build instructions should be deferred to the end
600
+ # @todo on_build instructions should be deferred to the end.
316
601
  def on_build(instruction = nil, &blk)
317
602
  requires_from!(:on_build)
318
603
  log_step('on_build', instruction)
@@ -334,6 +619,15 @@ module Drydock
334
619
  # normal usage, you should use the `expose` instruction instead.
335
620
  # * `on_build`, which can be used to specify low-level on-build options. For
336
621
  # normal usage, you should use the `on_build` instruction instead.
622
+ #
623
+ # Additional `opts` are also recognized:
624
+ #
625
+ # * `author`, a string, preferably in the format of "Name <email@domain.com>".
626
+ # If provided, this overrides
627
+ # * `comment`, an arbitrary string used as a comment for the resulting image
628
+ #
629
+ # If `run` results in a container being created and `&blk` is provided, the
630
+ # container will be yielded to the block.
337
631
  def run(cmd, opts = {}, &blk)
338
632
  requires_from!(:run)
339
633
 
@@ -374,13 +668,17 @@ module Drydock
374
668
  # Use a `plugin` to issue other commands. The block form can be used to issue
375
669
  # multiple commands:
376
670
  #
671
+ # ```
377
672
  # with Plugins::APK do |apk|
378
673
  # apk.update
379
674
  # end
675
+ # ```
380
676
  #
381
677
  # In cases of single commands, the above is the same as:
382
678
  #
679
+ # ```
383
680
  # with(Plugins::APK).update
681
+ # ```
384
682
  def with(plugin, &blk)
385
683
  (@plugins[plugin] ||= plugin.new(self)).tap do |instance|
386
684
  yield instance if block_given?
@@ -61,6 +61,27 @@ RSpec.describe Drydock::Project do
61
61
  expect(hash_output).to include('60fde9c2310b0d4cad4dab8d126b04387efba289')
62
62
  end
63
63
 
64
+ it 'fails to change working directory if it does not exist' do
65
+ project.from('alpine')
66
+ expect { project.cd('/app') }.not_to raise_error
67
+ expect { project.cd('/app') { project.run('pwd') } }.to raise_error(Drydock::InvalidCommandExecutionError)
68
+ end
69
+
70
+ it 'correctly changes working directories' do
71
+ project.from('alpine')
72
+ project.mkdir('/app')
73
+ expect { project.cd('/app') { project.run('pwd') } }.not_to raise_error
74
+ end
75
+
76
+ it 'complains when version check does not pass' do
77
+ expect { project.drydock('~> 901.301') }.to raise_error(Drydock::InsufficientVersionError)
78
+ end
79
+
80
+ it 'requires version check to go before #from instruction' do
81
+ project.from('alpine')
82
+ expect { project.drydock('~> 0.1') }.to raise_error(Drydock::InvalidInstructionError)
83
+ end
84
+
64
85
  it 'autocreates the target path on copy' do
65
86
  project.from('alpine')
66
87
  expect {
@@ -116,6 +137,18 @@ RSpec.describe Drydock::Project do
116
137
  expect(image.info['ContainerConfig']['Cmd']).to eq(['/bin/sh', '-c', '/bin/date'])
117
138
  end
118
139
 
140
+ it 'sets the raw Entrypoint' do
141
+ project.from('alpine')
142
+ project.entrypoint(['/bin/bash'])
143
+
144
+ expect(project.last_image).not_to be_nil
145
+
146
+ image = Docker::Image.get(project.last_image.id)
147
+ expect(image).not_to be_nil
148
+ expect(image.info['Config']['Entrypoint']).to eq(['/bin/bash'])
149
+ end
150
+
151
+
119
152
  it 'sets the Env' do
120
153
  project.from('alpine')
121
154
  project.env('APP_ROOT_TEST', '/app/current')
@@ -127,10 +160,11 @@ RSpec.describe Drydock::Project do
127
160
  expect(image.info['Config']['Env']).to include('APP_ROOT_TEST=/app/current')
128
161
  end
129
162
 
130
- it 'sets the Env with multiple values' do
163
+ it 'sets the Env with multiple values, and retained after other commands' do
131
164
  project.from('alpine')
132
165
  project.env('APP_ROOT_TEST', '/app/current')
133
166
  project.env('BUILD_ROOT', '/tmp/build')
167
+ project.run('touch /hello-world')
134
168
 
135
169
  expect(project.last_image).not_to be_nil
136
170
 
@@ -140,6 +174,19 @@ RSpec.describe Drydock::Project do
140
174
  expect(image.info['Config']['Env']).to include('BUILD_ROOT=/tmp/build')
141
175
  end
142
176
 
177
+ it 'sets multiple Envs in one go, and retained after other commands' do
178
+ project.from('alpine')
179
+ project.envs(APP_ROOT: '/app', BUILD_ROOT: '/build')
180
+ project.run('touch /hello-world')
181
+
182
+ expect(project.last_image).not_to be_nil
183
+
184
+ image = Docker::Image.get(project.last_image.id)
185
+ expect(image).not_to be_nil
186
+ expect(image.info['Config']['Env']).to include('APP_ROOT=/app')
187
+ expect(image.info['Config']['Env']).to include('BUILD_ROOT=/build')
188
+ end
189
+
143
190
  it 'sets the ExposedPorts' do
144
191
  project.from('alpine')
145
192
  project.expose(tcp: [80, 443], udp: 53)
@@ -30,7 +30,13 @@ RSpec.describe Drydock::StreamMonitor do
30
30
  expect(monitor).not_to be_nil
31
31
  expect(run_image).not_to be_nil
32
32
 
33
- expect(events).to have(5).items
33
+ sleep 1
34
+ expect(events).to have_at_least(1).item
35
+
36
+ event_statuses = events.map(&:status).sort
37
+ expect(event_statuses).to include('create')
38
+ expect(event_statuses).to include('pull')
39
+ expect(event_statuses).to include('start')
34
40
 
35
41
  commit_event = events.find { |evt| evt.status == 'commit' }
36
42
  expect(commit_event).not_to be_nil
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,9 @@ require 'fakefs/spec_helpers'
7
7
  require 'simplecov'
8
8
  require 'simplecov-rcov'
9
9
 
10
+ require 'codeclimate-test-reporter'
11
+ CodeClimate::TestReporter.start
12
+
10
13
  unless ENV.key?('RCOV')
11
14
  SimpleCov.start {
12
15
  add_filter '/vendor/'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-dock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ripta Pasay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-02 00:00:00.000000000 Z
11
+ date: 2015-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docker-api
@@ -120,9 +120,12 @@ extra_rdoc_files:
120
120
  - LICENSE
121
121
  - README.md
122
122
  files:
123
+ - ".codeclimate.yml"
123
124
  - ".dockerignore"
124
125
  - ".pryrc"
125
126
  - ".rspec"
127
+ - ".travis.yml"
128
+ - ".yardopts"
126
129
  - Dockerfile
127
130
  - Gemfile
128
131
  - Gemfile.lock