Class: Drydock::Project
- Inherits:
-
Object
- Object
- Drydock::Project
- Defined in:
- lib/drydock/project.rb
Constant Summary collapse
- DEFAULT_OPTIONS =
{ auto_remove: true, author: nil, cache: nil, event_handler: false, ignorefile: '.dockerignore', label: nil, logs: false }
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, &blk) ⇒ Object
Change directories for operations that require a directory.
-
#cmd(command) ⇒ Object
Set the command to automatically execute when the image is run.
-
#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.
-
#derive(opts = {}, &blk) ⇒ Object
Derive a new project based on the current state of the build.
- #destroy!(force: false) ⇒ Object
-
#done! ⇒ Object
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 the `target_path` in the container with a specific `chmod` (defaults to 0644).
-
#env(name, value) ⇒ Object
Set an environment variable.
-
#envs(pairs = {}) ⇒ Object
Set multiple environment variables at once.
-
#expose(*ports, tcp: [], udp: []) ⇒ Object
Expose one or more ports.
-
#finalize!(force: false) ⇒ Object
Finalize everything.
-
#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
A new instance of Project.
-
#last_image ⇒ Object
The last image object built in this project.
-
#logger ⇒ Object
Access to the logger object.
-
#mkdir(path, chmod: nil) ⇒ Object
Create a new directory specified by ‘path`.
-
#on_build(instruction = nil, &blk) ⇒ Object
TODO(rpasay): on_build instructions should be deferred to the end.
-
#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
15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/drydock/project.rb', line 15 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.
30 31 32 33 |
# File 'lib/drydock/project.rb', line 30 def (name: nil, email: nil) value = email ? "#{name} <#{email}>" : name.to_s set :author, value end |
#build_id ⇒ Object
Retrieve the current build ID for this project.
36 37 38 |
# File 'lib/drydock/project.rb', line 36 def build_id chain ? chain.serial : '0' end |
#cd(path, &blk) ⇒ Object
Change directories for operations that require a directory.
41 42 43 44 45 46 |
# File 'lib/drydock/project.rb', line 41 def cd(path, &blk) @run_path << path blk.call ensure @run_path.pop end |
#cmd(command) ⇒ Object
Set the command to automatically execute when the image is run.
49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/drydock/project.rb', line 49 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.
When ‘chmod` is `false` (the default), the original file mode from its source file is kept when copying into the container. Otherwise, the mode provided will be used to override all file and directory modes.
When ‘no_cache` is `false` (the default), the hash digest of the source path is used as the cache key. When `true`, the image is rebuilt every time.
The ‘copy` instruction always respects the `ignorefile`.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/drydock/project.rb', line 72 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)) if source_path.start_with?('/') Drydock.logger.warn("#{source_path.inspect} is an absolute path; we recommend relative paths") end raise InvalidInstructionError, "#{source_path} does not exist" unless File.exist?(source_path) source_files = if File.directory?(source_path) FileManager.find(source_path, ignorefile, prepend_path: true, recursive: recursive) else [source_path] end source_files.sort! raise InvalidInstructionError, "#{source_path} is empty or does not match a path" if source_files.empty? buffer = StringIO.new log_info("Processing #{source_files.size} files in tree") TarWriter.new(buffer) do |tar| source_files.each do |source_file| File.open(source_file, 'r') do |input| stat = input.stat mode = chmod || stat.mode tar.add_entry(source_file, mode: stat.mode, mtime: stat.mtime) do |tar_file| tar_file.write(input.read) end end end end buffer.rewind digest = Digest::MD5.hexdigest(buffer.read) log_info("Tree digest is md5:#{digest}") chain.run("# COPY #{source_path} #{target_path} DIGEST #{digest}", no_cache: no_cache) do |container| target_stat = container.archive_head(target_path) # TODO(rpasay): cannot autocreate the target, because `container` here is already dead unless target_stat raise InvalidInstructionError, "Target path #{target_path.inspect} does not exist" end unless target_stat.directory? Drydock.logger.debug(target_stat) raise InvalidInstructionError, "Target path #{target_path.inspect} exists, but is not a directory in the container" end container.archive_put(target_path) do |output| buffer.rewind output.write(buffer.read) end end self end |
#derive(opts = {}, &blk) ⇒ Object
Derive a new project based on the current state of the build. This instruction returns a project that can be referred to elsewhere.
239 240 241 |
# File 'lib/drydock/project.rb', line 239 def derive(opts = {}, &blk) Drydock.build_on_chain(chain, opts, &blk) end |
#destroy!(force: false) ⇒ Object
131 132 133 134 |
# File 'lib/drydock/project.rb', line 131 def destroy!(force: false) chain.destroy!(force: force) if chain finalize!(force: force) end |
#done! ⇒ Object
Meta instruction to signal to the builder that the build is done.
137 138 139 |
# File 'lib/drydock/project.rb', line 137 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.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/drydock/project.rb', line 145 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.new 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 chain.run("# DOWNLOAD #{source_url} #{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 |
#env(name, value) ⇒ Object
Set an environment variable.
177 178 179 180 181 182 |
# File 'lib/drydock/project.rb', line 177 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. ‘pairs` should be a hash-like enumerable.
186 187 188 189 190 191 192 193 |
# File 'lib/drydock/project.rb', line 186 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.
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.
202 203 204 205 206 207 208 209 210 211 |
# File 'lib/drydock/project.rb', line 202 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
Finalize everything. This will be automatically invoked at the end of the build, and should not be called manually.
224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/drydock/project.rb', line 224 def finalize!(force: false) if chain chain.finalize!(force: force) end if stream_monitor stream_monitor.kill stream_monitor.join end self 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.
215 216 217 218 219 220 |
# File 'lib/drydock/project.rb', line 215 def from(repo, tag = 'latest') raise InvalidInstructionError, '`from` must only be called once per project' if chain log_step('from', repo, tag) @chain = PhaseChain.from_repo(repo, tag) self end |
#import(path, from: nil, force: false, spool: false) ⇒ Object
Import a ‘path` from a different project. The `from` option should be project, usually the result of a `derive` instruction.
TODO(rpasay): add a #load method as an alternative to #import, which allows importing a full container, including things from /etc. TODO(rpasay): do not always append /. to the #archive_get calls; must check the type of ‘path` inside the container first. TODO(rpasay): break this large method into smaller ones.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/drydock/project.rb', line 256 def import(path, from: nil, force: false, spool: false) mkdir(path) requires_from!(:import) raise InvalidInstructionError, 'cannot `import` from `/`' if path == '/' && !force raise 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
The last image object built in this project.
300 301 302 |
# File 'lib/drydock/project.rb', line 300 def last_image chain ? chain.last_image : nil end |
#logger ⇒ Object
Access to the logger object.
244 245 246 |
# File 'lib/drydock/project.rb', line 244 def logger Drydock.logger end |
#mkdir(path, chmod: nil) ⇒ Object
Create a new directory specified by ‘path`. When `chmod` is given, the new directory will be chmodded. Otherwise, the default umask is used to determine the path’s mode.
307 308 309 310 311 312 313 |
# File 'lib/drydock/project.rb', line 307 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
TODO(rpasay): on_build instructions should be deferred to the end
316 317 318 319 320 321 |
# File 'lib/drydock/project.rb', line 316 def on_build(instruction = nil, &blk) 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). 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.
337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/drydock/project.rb', line 337 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.
352 353 354 355 356 357 358 359 |
# File 'lib/drydock/project.rb', line 352 def set(key, value = nil, &blk) key = key.to_sym raise ArgumentError, "unknown option #{key.inspect}" unless build_opts.key?(key) raise ArgumentError, "one of value or block is required" if value.nil? && blk.nil? raise 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.
366 367 368 369 370 371 372 |
# File 'lib/drydock/project.rb', line 366 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
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
384 385 386 387 388 |
# File 'lib/drydock/project.rb', line 384 def with(plugin, &blk) (@plugins[plugin] ||= plugin.new(self)).tap do |instance| yield instance if block_given? end end |