dry-dock 0.1.4 → 0.1.5

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