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 +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
|
+
![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
|
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
|