class Drydock::Project
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.
Constants
- DEFAULT_OPTIONS
Attributes
Public Class Methods
Create a new project. **Do not use directly.**
@api private @param [Hash] build_opts
Build-time options @option build_opts
[Boolean] :auto_remove Whether intermediate images
created during the build of this project should be automatically removed.
@option build_opts
[String] :author The default author field when an
author is not provided explicitly with {Project#author}.
@option build_opts
[ObjectCaches::Base] :cache An object cache manager. @option build_opts
[#call] :event_handler A handler that responds to a
`#call` message with four arguments: `[event, is_new, serial_no, event_type]` most useful to override logging.
@option build_opts
[PhaseChain] :chain A phase chain manager. @option build_opts
[String] :ignorefile The name of the ignore-file to load.
# 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
Public Instance Methods
Retrieve the current build ID for this project. If no image has been built, returns the string ‘0’.
# File lib/drydock/project.rb, line 69 def build_id chain ? chain.serial : '0' end
Change directories for operations that require a directory.
@param [String] path The path to change directories to. @yield block containing instructions to run inside the new directory
# File lib/drydock/project.rb, line 77 def cd(path = '/', &blk) @run_path << path blk.call if blk ensure @run_path.pop end
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.
@param [String, Array<String>] command The command set to run. When a
`String` is provided, the command is run inside a shell (`/bin/sh`). When an `Array` is given, the command is run as-is given.
# 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
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*.
@param [String] source_path The source path on the build machine (where
`drydock` is running) from which to copy files.
@param [String] target_path The target path inside the image to which to
copy the files. This path **must already exist** before copying begins.
@param [Integer, Boolean] chmod When ‘false` (the default), the original file
mode from its source file is kept when copying into the container. Otherwise, the mode provided (in integer octal form) will be used to override *all* file and directory modes.
@param [Boolean] no_cache When ‘false` (the default), the hash digest of the
source path--taking into account all its files, directories, and contents--is used as the cache key. When `true`, the image is rebuilt *every* time.
@param [Boolean] recursive When ‘true`, then `source_path` is expected to be
a directory, at which point all its contents would be recursively searched. When `false`, then `source_path` is expected to be a file.
@raise (see Instructions::Copy#run!
)
# 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 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`, a `Gemfile` in it, and a `vendor` directory containing vendored gems.
-
Create a new project referred to as ‘js_build`. The result of this project is an image with `/app`, a `package.json` in it, and a `node_modules` directory containing vendored node.js modules. This project does not contain any of the contents of `ruby_build`.
-
Create an anonymous project containing only the empty ‘/app` directory. Onto that, we’ll import the contents of ‘/app` from `ruby_build` into this anonymous project. We’ll do the same with the contents of ‘/app` from `js_build`. Finally, the resulting image is given the tag `jdoe/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).
@param (see initialize) @option (see initialize)
# 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 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.
@api private
# 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
Meta instruction to signal to the builder that the build is done.
@api private
# File lib/drydock/project.rb, line 174 def done! throw :done end
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.
# 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
**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](guides.rubygems.org/patterns/#semantic-versioning)) version specification; it even understands the twiddle-waka (‘~>`) operator.
@example
drydock '~> 0.5'
@param [String] version The version specification to use.
# 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
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.
@param (see cmd
)
# 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
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.
@param [String] name The name of the environment variable. By convention,
the name should be uppercased and underscored. The name should **not** be preceeded by a `$` sign in this context.
@param [String] value The value of the variable. No extra quoting should be
necessary here.
# 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
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}.
@param [Hash, map] pairs A hash-like enumerable, where ‘#map` yields exactly
two elements. See {#env} for any restrictions of the name (key) and value.
# 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 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.
@example Different ways of exposing port 53 UDP and ports 80 and 443 TCP:
expose '53/udp', '80/tcp', '443/tcp' expose udp: 53, tcp: [80, 443]
@param [Array<String>] ports An array of strings of port specifications.
Each port specification must look like `#/type`, where `#` is the port number, and `type` is either `udp` or `tcp`.
@param [Integer, Array<Integer>] tcp A TCP port number to open, or an array
of TCP port numbers to open.
@param [Integer, Array<Integer>] udp A UDP port number to open, or an array
of UDP port numbers to open.
# 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 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.
@api private
# 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
# File lib/drydock/project.rb, line 485 def finalized? @finalized end
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.
@param [#to_s] repo The name of the repository, which may be any valid docker
repository name, and may optionally include the registry address, e.g., `johndoe/thing` or `quay.io/jane/app`. The name *must not* contain the tag name.
@param [#to_s] tag The tag to use.
# 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 a ‘path` from a different project. The `from` option should be project, usually the result of a `derive` instruction.
@todo 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.
@todo Do not always append /. to the archive_get calls
We must check the type of `path` inside the container first.
@todo Break this large method into smaller ones.
# 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
Retrieve the last image object built in this project.
If no image has been built, returns ‘nil`.
# File lib/drydock/project.rb, line 554 def last_image chain ? chain.last_image : nil end
Access to the logger object.
@return [Logger] A logger object on which one could call ‘#info`, `#error`,
and the likes.
# File lib/drydock/project.rb, line 493 def logger Drydock.logger end
Create a new directory specified by ‘path` in the image.
@param [String] path The path to create inside the image. @param [String] chmod The mode to which the new directory will be chmodded.
If not specified, the default umask is used to determine the mode.
# 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
**NOT SUPPORTED YET**
@todo on_build
instructions should be deferred to the end.
# 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
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). When `no_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 the `env` or `envs` instructions.
-
‘expose`, which can be used to specify a set of ports to expose. For normal usage, you should use the `expose` instruction instead.
-
‘on_build`, which can be used to specify low-level on-build options. For normal usage, you should use the `on_build` instruction instead.
Additional ‘opts` are also recognized:
-
‘author`, a string, preferably in the format of “Name <email@domain.com>”. 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.
# 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 project options.
# 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 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.
# 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
Use a ‘plugin` to issue other commands. The block form can be used to issue multiple commands:
“‘
with Plugins::APK do |apk| apk.update end
“‘
In cases of single commands, the above is the same as:
“‘
with(Plugins::APK).update
“‘
# 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
Private Instance Methods
# File lib/drydock/project.rb, line 667 def build_cmd(cmd) if @run_path.empty? cmd.to_s.strip else "cd #{@run_path.join('/')} && #{cmd}".strip end end
# File lib/drydock/project.rb, line 675 def cache build_opts[:cache] ||= ObjectCaches::NoCache.new end
# File lib/drydock/project.rb, line 679 def ignorefile @ignorefile ||= IgnorefileDefinition.new(build_opts[:ignorefile]) end
# File lib/drydock/project.rb, line 683 def log_info(msg, indent: 0) Drydock.logger.info(indent: indent, message: msg) end
# File lib/drydock/project.rb, line 687 def log_step(op, *args) opts = args.last.is_a?(Hash) ? args.pop : {} optstr = opts.map { |k, v| "#{k}: #{v.inspect}" }.join(', ') argstr = args.map(&:inspect).join(', ') Drydock.logger.info("##{chain ? chain.serial : 0}: #{op}(#{argstr}#{optstr.empty? ? '' : ", #{optstr}"})") end
# File lib/drydock/project.rb, line 696 def requires_from!(instruction) fail InvalidInstructionError, "`#{instruction}` cannot be called before `from`" unless chain end