Class: Drydock::Project
- Inherits:
-
Object
- Object
- Drydock::Project
- Defined in:
- lib/drydock/project.rb
Overview
A project defines the methods available in a Drydockfile
. When run using
the binary drydock
, this object will be instantiated automatically for you.
The contents of a Drydockfile
is automatically evaluated in the context
of a project, so you don't need to instantiate the object manually.
Constant Summary collapse
- DEFAULT_OPTIONS =
{ auto_remove: true, author: nil, cache: nil, event_handler: false, ignorefile: '.dockerignore' }
Instance Method Summary collapse
-
#author(name: nil, email: nil) ⇒ Object
Set the author for commits.
-
#build_id ⇒ Object
Retrieve the current build ID for this project.
-
#cd(path = '/') { ... } ⇒ Object
Change directories for operations that require a directory.
-
#cmd(command) ⇒ Object
Set the command that is automatically executed by default when the image is run through the
docker run
command. -
#copy(source_path, target_path, chmod: false, no_cache: false, recursive: true) ⇒ Object
Copies files from
source_path
on the the build machine, intotarget_path
in the container. -
#derive(opts = {}, &blk) ⇒ Object
Derive a new project based on the current state of the current project.
-
#destroy!(force: false) ⇒ Object
private
Destroy the images and containers created, and attempt to return the docker state as it was before the project.
-
#done! ⇒ Object
private
Meta instruction to signal to the builder that the build is done.
-
#download_once(source_url, target_path, chmod: 0644) ⇒ Object
Download (and cache) a file from
source_url
, and copy it into thetarget_path
in the container with a specificchmod
(defaults to 0644). -
#drydock(version = '>= 0') ⇒ Object
This instruction is optional, but if specified, must appear at the beginning of the file..
-
#entrypoint(command) ⇒ Object
Sets the entrypoint command for an image.
-
#env(name, value) ⇒ Object
Set an environment variable, which will be persisted in future images (unless it is specifically overwritten) and derived projects.
-
#envs(pairs) ⇒ Object
Set multiple environment variables at once.
-
#expose(*ports, tcp: [], udp: []) ⇒ Object
Expose one or more ports.
-
#finalize!(force: false) ⇒ Object
private
Finalize everything.
- #finalized? ⇒ Boolean
-
#from(repo, tag = 'latest') ⇒ Object
Build on top of the
from
image. -
#import(path, from: nil, force: false, spool: false) ⇒ Object
Import a
path
from a different project. -
#initialize(build_opts = {}) ⇒ Project
constructor
private
Create a new project.
-
#last_image ⇒ Object
Retrieve the last image object built in this project.
-
#logger ⇒ Logger
Access to the logger object.
-
#mkdir(path, chmod: nil) ⇒ Object
Create a new directory specified by
path
in the image. -
#on_build(instruction = nil, &_blk) ⇒ Object
NOT SUPPORTED YET.
-
#run(cmd, opts = {}, &blk) ⇒ Object
This instruction is used to run the command
cmd
against the current project. -
#set(key, value = nil, &blk) ⇒ Object
Set project options.
-
#tag(repo, tag = 'latest', force: false) ⇒ Object
Tag the current state of the project with a repo and tag.
-
#with(plugin, &blk) ⇒ Object
Use a
plugin
to issue other commands.
Constructor Details
#initialize(build_opts = {}) ⇒ Project
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Create a new project. Do not use directly.
32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/drydock/project.rb', line 32 def initialize(build_opts = {}) @chain = build_opts.key?(:chain) && build_opts.delete(:chain).derive @plugins = {} @run_path = [] @serial = 0 @build_opts = DEFAULT_OPTIONS.clone build_opts.each_pair { |key, value| set(key, value) } @stream_monitor = build_opts[:event_handler] ? StreamMonitor.new(build_opts[:event_handler]) : nil end |
Instance Method Details
#author(name: nil, email: nil) ⇒ Object
Set the author for commits. This is not an instruction, per se, and only takes into effect after instructions that cause a commit.
This instruction affects all instructions after it, but nothing before it.
At least one of name
or email
must be given. If one is provided, the
other is optional.
If no author instruction is provided, the author field is left blank by default.
58 59 60 61 62 63 64 65 |
# File 'lib/drydock/project.rb', line 58 def (name: nil, email: nil) if (name.nil? || name.empty?) && (email.nil? || name.empty?) fail InvalidInstructionArgumentError, 'at least one of `name:` or `email:` must be provided' end value = email ? "#{name} <#{email}>" : name.to_s set :author, value end |
#build_id ⇒ Object
Retrieve the current build ID for this project. If no image has been built, returns the string '0'.
69 70 71 |
# File 'lib/drydock/project.rb', line 69 def build_id chain ? chain.serial : '0' end |
#cd(path = '/') { ... } ⇒ Object
Change directories for operations that require a directory.
77 78 79 80 81 82 |
# File 'lib/drydock/project.rb', line 77 def cd(path = '/', &blk) @run_path << path blk.call if blk ensure @run_path.pop end |
#cmd(command) ⇒ Object
Set the command that is automatically executed by default when the image
is run through the docker run
command.
#cmd corresponds to the CMD
Dockerfile instruction. This instruction
does not run the command, but rather provides the default command to
be run when the image is run without specifying a command.
As with the CMD
Dockerfile instruction, the #cmd instruction has three
forms:
['executable', 'param1', 'param2', '...']
, which would run the executable directly when the image is run;['param1', 'param2', '...']
, which would pass the parameters to the executable provided in the #entrypoint instruction; or'executable param1 param2'
, which would run the executable inside a subshell.
The first two forms are preferred over the last one. See also #entrypoint to see how the instruction interacts with this one.
107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/drydock/project.rb', line 107 def cmd(command) requires_from!(:cmd) log_step('cmd', command) unless command.is_a?(Array) command = ['/bin/sh', '-c', command.to_s] end chain.run("# CMD #{command.inspect}", command: command) self end |
#copy(source_path, target_path, chmod: false, no_cache: false, recursive: true) ⇒ Object
Copies files from source_path
on the the build machine, into target_path
in the container. This instruction automatically commits the result.
The copy
instruction always respects the ignorefile
.
When no_cache
is true
(also see parameter explanation below), then any
instruction after #copy will also be rebuilt every time.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/drydock/project.rb', line 143 def copy(source_path, target_path, chmod: false, no_cache: false, recursive: true) requires_from!(:copy) log_step('copy', source_path, target_path, chmod: (chmod ? sprintf('%o', chmod) : false)) Instructions::Copy.new(chain, source_path, target_path).tap do |ins| ins.chmod = chmod if chmod ins.ignorefile = ignorefile ins.no_cache = no_cache ins.recursive = recursive ins.run! end self end |
#derive(opts = {}, &blk) ⇒ Object
Derive a new project based on the current state of the current project. This instruction returns the new project that can be referred to elsewhere, and most useful when combined with other inter-project instructions, such as #import.
For example:
from 'some-base-image'
APP_ROOT = '/app'
mkdir APP_ROOT
# 1:
ruby_build = derive {
copy 'Gemfile', APP_ROOT
run 'bundle install --path vendor'
}
# 2:
js_build = derive {
copy 'package.json', APP_ROOT
run 'npm install'
}
# 3:
derive {
import APP_ROOT, from: ruby_build
import APP_ROOT, from: js_build
tag 'jdoe/app', 'latest', force: true
}
In the example above, an image is created with a new directory /app
.
From there, the build branches out into three directions:
- Create a new project referred to as
ruby_build
. The result of this project is an image with/app
, aGemfile
in it, and avendor
directory containing vendored gems. - Create a new project referred to as
js_build
. The result of this project is an image with/app
, apackage.json
in it, and anode_modules
directory containing vendored node.js modules. This project does not contain any of the contents ofruby_build
. - Create an anonymous project containing only the empty
/app
directory. Onto that, we'll import the contents of/app
fromruby_build
into this anonymous project. We'll do the same with the contents of/app
fromjs_build
. Finally, the resulting image is given the tagjdoe/app:latest
.
Because each derived project lives on its own and only depends on the
root project (whose end state is essentially the #mkdir instruction),
when Gemfile
changes but package.json
does not, only the first
derived project will be rebuilt (and following that, the third as well).
476 477 478 479 480 481 482 483 |
# File 'lib/drydock/project.rb', line 476 def derive(opts = {}, &blk) clean_opts = build_opts.delete_if { |_, v| v.nil? } derive_opts = clean_opts.merge(opts).merge(chain: chain) Project.new(derive_opts).tap do |project| project.instance_eval(&blk) if blk end end |
#destroy!(force: false) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Destroy the images and containers created, and attempt to return the docker state as it was before the project. The project object itself cannot be reused after it is destroyed.
164 165 166 167 168 169 |
# File 'lib/drydock/project.rb', line 164 def destroy!(force: false) return self if frozen? finalize!(force: force) chain.destroy!(force: force) if chain freeze end |
#done! ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Meta instruction to signal to the builder that the build is done.
174 175 176 |
# File 'lib/drydock/project.rb', line 174 def done! throw :done end |
#download_once(source_url, target_path, chmod: 0644) ⇒ Object
Download (and cache) a file from source_url
, and copy it into the
target_path
in the container with a specific chmod
(defaults to 0644).
The cache currently cannot be disabled.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/drydock/project.rb', line 182 def download_once(source_url, target_path, chmod: 0644) requires_from!(:download_once) unless cache.key?(source_url) cache.set(source_url) do |obj| chunked = proc do |chunk, _remaining_bytes, _total_bytes| obj.write(chunk) end Excon.get(source_url, response_block: chunked) end end log_step('download_once', source_url, target_path, chmod: sprintf('%o', chmod)) # TODO(rpasay): invalidate cache when the downloaded file changes, # and then force rebuild digest = Digest::MD5.hexdigest(source_url) chain.run("# DOWNLOAD file:md5:#{digest} #{target_path}") do |container| container.archive_put do |output| TarWriter.new(output) do |tar| cache.get(source_url) do |input| tar.add_file(target_path, chmod) do |tar_file| tar_file.write(input.read) end end end end end self end |
#drydock(version = '>= 0') ⇒ Object
This instruction is optional, but if specified, must appear at the beginning of the file.
This instruction is used to restrict the version of drydock
required to
run the Drydockfile
. When not specified, any version of drydock
is
allowed to run the file.
The version specifier understands any bundler-compatible (and therefore
gem-compatible)
version specification; it even understands the twiddle-waka (~>
) operator.
228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/drydock/project.rb', line 228 def drydock(version = '>= 0') fail InvalidInstructionError, '`drydock` must be called before `from`' if chain log_step('drydock', version) requirement = Gem::Requirement.create(version) current = Gem::Version.create(Drydock.version) unless requirement.satisfied_by?(current) fail InsufficientVersionError, "build requires #{version.inspect}, but you're on #{Drydock.version.inspect}" end self end |
#entrypoint(command) ⇒ Object
Sets the entrypoint command for an image.
#entrypoint corresponds to the ENTRYPOINT
Dockerfile instruction. This
instruction does not run the command, but rather provides the default
command to be run when the image is run without specifying a command.
As with the #cmd instruction, #entrypoint has three forms, of which the first two forms are preferred over the last one.
252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/drydock/project.rb', line 252 def entrypoint(command) requires_from!(:entrypoint) log_step('entrypoint', command) unless command.is_a?(Array) command = ['/bin/sh', '-c', command.to_s] end chain.run("# ENTRYPOINT #{command.inspect}", entrypoint: command) self end |
#env(name, value) ⇒ Object
Set an environment variable, which will be persisted in future images (unless it is specifically overwritten) and derived projects.
Subsequent commands can refer to the environment variable by preceeding
the variable with a $
sign, e.g.:
env 'APP_ROOT', '/app'
mkdir '$APP_ROOT'
run ['some-command', '--install-into=$APP_ROOT']
Multiple calls to this instruction will build on top of one another. That is, after the following two instructions:
env 'APP_ROOT', '/app'
env 'BUILD_ROOT', '/build'
the resulting image will have both APP_ROOT
and BUILD_ROOT
set. Later
instructions overwrites previous instructions of the same name:
# 1
env 'APP_ROOT', '/app'
# 2
env 'APP_ROOT', '/home/jdoe/app'
# 3
At #1
, APP_ROOT
is not set (assuming no other instruction comes before
it). At #2
, APP_ROOT
is set to '/app'. At #3
, APP_ROOT
is set to
/home/jdoe/app
, and its previous value is no longer available.
Note that the environment variable is not evaluated in ruby; in fact, the
$
sign should be passed as-is to the instruction. As with shell
programming, the variable name should not be preceeded by the $
sign when declared, but must be when referenced.
309 310 311 312 313 314 |
# File 'lib/drydock/project.rb', line 309 def env(name, value) requires_from!(:env) log_step('env', name, value) chain.run("# SET ENV #{name}", env: ["#{name}=#{value}"]) self end |
#envs(pairs) ⇒ Object
Set multiple environment variables at once. The values will be persisted in future images and derived projects, unless specifically overwritten.
The following instruction:
envs APP_ROOT: '/app', BUILD_ROOT: '/tmp/build'
is equivalent to the more verbose:
env 'APP_ROOT', '/app'
env 'BUILD_ROOT', '/tmp/build'
When the same key appears more than once in the same #envs instruction, the same rules for ruby hashes are used, which most likely (but not guaranteed between ruby version) means the last value set is used.
See also notes for #env.
340 341 342 343 344 345 346 347 |
# File 'lib/drydock/project.rb', line 340 def envs(pairs) requires_from!(:envs) log_step('envs', pairs) values = pairs.map { |name, value| "#{name}=#{value}" } chain.run("# SET ENVS #{pairs.inspect}", env: values) self end |
#expose(*ports, tcp: [], udp: []) ⇒ Object
Expose one or more ports. The values will be persisted in future images
When ports
is specified, the format must be: ##/type where ## is the port
number and type is either tcp or udp. For example, "80/tcp", "53/udp".
Otherwise, when the tcp
or udp
options are specified, only the port
numbers are required.
367 368 369 370 371 372 373 374 375 376 |
# File 'lib/drydock/project.rb', line 367 def expose(*ports, tcp: [], udp: []) requires_from!(:expose) Array(tcp).flatten.each { |p| ports << "#{p}/tcp" } Array(udp).flatten.each { |p| ports << "#{p}/udp" } log_step('expose', *ports) chain.run("# SET PORTS #{ports.inspect}", expose: ports) end |
#finalize!(force: false) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Finalize everything. This will be automatically invoked at the end of the build, and should not be called manually.
No further changes to the object is possible after finalization.
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/drydock/project.rb', line 404 def finalize!(force: false) return self if finalized? if chain chain.finalize!(force: force) end if stream_monitor stream_monitor.kill stream_monitor.join end @finalized = true self end |
#finalized? ⇒ Boolean
485 486 487 |
# File 'lib/drydock/project.rb', line 485 def finalized? @finalized end |
#from(repo, tag = 'latest') ⇒ Object
Build on top of the from
image. This must be the first instruction of
the project, although non-instructions may appear before this.
If the drydock
instruction is provided, from
should come after it.
387 388 389 390 391 392 393 394 395 396 |
# File 'lib/drydock/project.rb', line 387 def from(repo, tag = 'latest') fail InvalidInstructionError, '`from` must only be called once per project' if chain repo = repo.to_s tag = tag.to_s log_step('from', repo, tag) @chain = PhaseChain.from_repo(repo, tag) self end |
#import(path, from: nil, force: false, spool: false) ⇒ Object
Add a #load method as an alternative to #import Doing so would allow importing a full container, including things from /etc, some of which may be mounted from the host.
Do not always append /. to the #archive_get calls
We must check the type of path
inside the container first.
Break this large method into smaller ones.
Import a path
from a different project. The from
option should be
project, usually the result of a derive
instruction.
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/drydock/project.rb', line 508 def import(path, from: nil, force: false, spool: false) mkdir(path) requires_from!(:import) fail InvalidInstructionError, 'cannot `import` from `/`' if path == '/' && !force fail InvalidInstructionError, '`import` requires a `from:` option' if from.nil? log_step('import', path, from: from.last_image.id) total_size = 0 if spool spool_file = Tempfile.new('drydock') log_info("Spooling to #{spool_file.path}") from.send(:chain).run("# EXPORT #{path}", no_commit: true) do |source_container| source_container.archive_get(path + "/.") do |chunk| spool_file.write(chunk.to_s).tap { |b| total_size += b } end end spool_file.rewind chain.run("# IMPORT #{path}", no_cache: true) do |target_container| target_container.archive_put(path) do |output| output.write(spool_file.read) end end spool_file.close else chain.run("# IMPORT #{path}", no_cache: true) do |target_container| target_container.archive_put(path) do |output| from.send(:chain).run("# EXPORT #{path}", no_commit: true) do |source_container| source_container.archive_get(path + "/.") do |chunk| output.write(chunk.to_s).tap { |b| total_size += b } end end end end end log_info("Imported #{Formatters.number(total_size)} bytes") end |
#last_image ⇒ Object
Retrieve the last image object built in this project.
If no image has been built, returns nil
.
554 555 556 |
# File 'lib/drydock/project.rb', line 554 def last_image chain ? chain.last_image : nil end |
#logger ⇒ Logger
Access to the logger object.
493 494 495 |
# File 'lib/drydock/project.rb', line 493 def logger Drydock.logger end |
#mkdir(path, chmod: nil) ⇒ Object
Create a new directory specified by path
in the image.
563 564 565 566 567 568 569 |
# File 'lib/drydock/project.rb', line 563 def mkdir(path, chmod: nil) if chmod run "mkdir -p #{path} && chmod #{chmod} #{path}" else run "mkdir -p #{path}" end end |
#on_build(instruction = nil, &_blk) ⇒ Object
on_build instructions should be deferred to the end.
NOT SUPPORTED YET
574 575 576 577 578 579 580 581 |
# File 'lib/drydock/project.rb', line 574 def on_build(instruction = nil, &_blk) fail NotImplementedError, "on_build is not yet supported" requires_from!(:on_build) log_step('on_build', instruction) chain.run("# ON_BUILD #{instruction}", on_build: instruction) self end |
#run(cmd, opts = {}, &blk) ⇒ Object
This instruction is used to run the command cmd
against the current
project. The opts
may be one of:
no_commit
, when true, the container will not be committed to a new image. Most of the time, you want this to be false (default).no_cache
, when true, the container will be rebuilt every time. Most of the time, you want this to be false (default). Whenno_commit
is true, this option is automatically set to true.env
, which can be used to specify a set of environment variables. For normal usage, you should use theenv
orenvs
instructions.expose
, which can be used to specify a set of ports to expose. For normal usage, you should use theexpose
instruction instead.on_build
, which can be used to specify low-level on-build options. For normal usage, you should use theon_build
instruction instead.
Additional opts
are also recognized:
author
, a string, preferably in the format of "Name [email protected]". If provided, this overrides the author name set with #author.comment
, an arbitrary string used as a comment for the resulting image
If run
results in a container being created and &blk
is provided, the
container will be yielded to the block.
606 607 608 609 610 611 612 613 614 615 616 617 618 |
# File 'lib/drydock/project.rb', line 606 def run(cmd, opts = {}, &blk) requires_from!(:run) cmd = build_cmd(cmd) run_opts = opts.dup run_opts[:author] = opts[:author] || build_opts[:author] run_opts[:comment] = opts[:comment] || build_opts[:comment] log_step('run', cmd, run_opts) chain.run(cmd, run_opts, &blk) self end |
#set(key, value = nil, &blk) ⇒ Object
Set project options.
621 622 623 624 625 626 627 628 |
# File 'lib/drydock/project.rb', line 621 def set(key, value = nil, &blk) key = key.to_sym fail ArgumentError, "unknown option #{key.inspect}" unless build_opts.key?(key) fail ArgumentError, "one of value or block is required" if value.nil? && blk.nil? fail ArgumentError, "only one of value or block may be provided" if value && blk build_opts[key] = value || blk end |
#tag(repo, tag = 'latest', force: false) ⇒ Object
Tag the current state of the project with a repo and tag.
When force
is false (default), this instruction will raise an error if
the tag already exists. When true, the tag will be overwritten without
any warnings.
635 636 637 638 639 640 641 |
# File 'lib/drydock/project.rb', line 635 def tag(repo, tag = 'latest', force: false) requires_from!(:tag) log_step('tag', repo, tag, force: force) chain.tag(repo, tag, force: force) self end |
#with(plugin, &blk) ⇒ Object
657 658 659 660 661 |
# File 'lib/drydock/project.rb', line 657 def with(plugin, &blk) (@plugins[plugin] ||= plugin.new(self)).tap do |instance| blk.call(instance) if blk end end |