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 +4 -4
- data/.codeclimate.yml +9 -0
- data/.travis.yml +16 -0
- data/.yardopts +2 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -0
- data/README.md +29 -5
- data/VERSION +1 -1
- data/dry-dock.gemspec +6 -3
- data/lib/drydock/drydock.rb +0 -6
- data/lib/drydock/errors.rb +2 -0
- data/lib/drydock/phase_chain.rb +25 -17
- data/lib/drydock/project.rb +331 -33
- data/spec/drydock/project_spec.rb +48 -1
- data/spec/drydock/stream_monitor_spec.rb +7 -1
- data/spec/spec_helper.rb +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f2e43155c83ad0b25eb7ee30a1da32697c0b270
|
4
|
+
data.tar.gz: fe24886ba40a3d093989f2c03f0068026698dcdf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: daf3a6b9aea72c10fb072740f97c2682df12f6378d88f768d0de43ffd16fd4f2f4bb92cb43190598e9d3e04d6c48bdd964c6edcd7ea8e8c1594e5c883a9968ba
|
7
|
+
data.tar.gz: b300b19d4869daf770dd612f7524ce5a8cbf909d3812be2feb9661ee42a01f55ec639071e2f2515c4d6c6b7e4d23823321be501bc234457cb26597e8ad74dafe
|
data/.codeclimate.yml
ADDED
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
data/Gemfile
CHANGED
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
|
-
|
3
|
+
WORK IN PROGRESS — ALPHA-RELEASE SOFTWARE
|
4
|
+
SOME FEATURES REQUIRE DOCKER 1.8.0 OR NEWER
|
5
|
+
|
6
|
+

|
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
|
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.
|
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.
|
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.
|
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-
|
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",
|
data/lib/drydock/drydock.rb
CHANGED
@@ -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')
|
data/lib/drydock/errors.rb
CHANGED
data/lib/drydock/phase_chain.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
235
|
-
|
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)
|
data/lib/drydock/project.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
#
|
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 `
|
69
|
-
#
|
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
|
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
|
-
#
|
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.
|
185
|
-
#
|
186
|
-
|
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
|
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
|
238
|
-
# instruction returns
|
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
|
-
|
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
|
-
#
|
252
|
-
# importing a full container, including things from
|
253
|
-
#
|
254
|
-
#
|
255
|
-
#
|
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
|
-
#
|
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
|
305
|
-
#
|
306
|
-
#
|
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
|
-
#
|
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
|
-
|
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
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
|
+
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-
|
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
|