class Drydock::PhaseChain

Public Class Methods

build_commit_opts(opts = {}) click to toggle source
# File lib/drydock/phase_chain.rb, line 9
def self.build_commit_opts(opts = {})
  {}.tap do |commit|
    if opts.key?(:command)
      commit['run'] ||= {}
      commit['run'][:Cmd] = opts[:command]
    end

    if opts.key?(:entrypoint)
      commit['run'] ||= {}
      commit['run'][:Entrypoint] = opts[:entrypoint]
    end

    commit[:author]  = opts.fetch(:author, '')  if opts.key?(:author)
    commit[:comment] = opts.fetch(:comment, '') if opts.key?(:comment)
  end
end
build_container_opts(image_id, cmd, opts = {}) click to toggle source
# File lib/drydock/phase_chain.rb, line 26
def self.build_container_opts(image_id, cmd, opts = {})
  cmd = ['/bin/sh', '-c', cmd.to_s] unless cmd.is_a?(Array)

  ContainerConfig.from(
    Cmd: cmd,
    Tty: opts.fetch(:tty, false),
    Image: image_id
  ).tap do |cc|
    env = Array(opts[:env])
    cc[:Env].push(*env) unless env.empty?

    if opts.key?(:expose)
      cc[:ExposedPorts] ||= {}
      opts[:expose].each do |port|
        cc[:ExposedPorts][port] = {}
      end
    end

    (cc[:OnBuild] ||= []).push(opts[:on_build]) if opts.key?(:on_build)

    cc[:MetaOptions] ||= {}
    [:connect_timeout, :read_timeout].each do |key|
      cc[:MetaOptions][key] = opts[key] if opts.key?(key)
      cc[:MetaOptions][key] = opts[:timeout] if opts.key?(:timeout)
    end
  end
end
build_pull_opts(repo, tag = nil) click to toggle source
# File lib/drydock/phase_chain.rb, line 54
def self.build_pull_opts(repo, tag = nil)
  if tag
    {fromImage: "#{repo}:#{tag}"}
  else
    {fromImage: "#{repo}:latest"}
  end
end
create_container(cfg, &blk) click to toggle source

TODO(rpasay): Break this large method apart.

# File lib/drydock/phase_chain.rb, line 63
def self.create_container(cfg, &blk)
  meta_options = cfg[:MetaOptions] || {}
  timeout = meta_options.fetch(:read_timeout, Excon.defaults[:read_timeout]) || 60
  
  Drydock.logger.debug(message: "Create container configuration: #{cfg.inspect}")
  Docker::Container.create(cfg).tap do |c|
    # The call to Container.create merely creates a container, to be
    # scheduled to run. Start a separate thread that attaches to the
    # container's streams and mirror them to the logger.
    t = Thread.new do
      begin
        c.attach(stream: true, stdout: true, stderr: true) do |stream, chunk|
          case stream
          when :stdout
            Drydock.logger.info(message: chunk, annotation: '(O)')
          when :stderr
            Drydock.logger.info(message: chunk, annotation: '(E)')
          else
            Drydock.logger.info(message: chunk, annotation: '(?)')
          end
        end
      rescue Docker::Error::TimeoutError
        Drydock.logger.warn(message: "Lost connection to stream; retrying")
        retry
      end
    end

    # TODO(rpasay): RACE CONDITION POSSIBLE - the thread above may be
    # scheduled but not run before this block gets executed, which can
    # cause a loss of log output. However, forcing `t` to be run once
    # before this point seems to cause an endless wait (ruby 2.1.5).
    # Need to dig deeper in the future.
    #
    # TODO(rpasay): More useful `blk` handling here. This method only
    # returns after the container terminates, which isn't useful when
    # you want to do stuff to it, e.g., spawn a new exec container.
    #
    # The following block starts the container, and waits for it to finish.
    # An error is raised if no exit code is returned or if the exit code
    # is non-zero.
    begin
      c.start
      blk.call(c) if blk
      
      results = c.wait(timeout)

      unless results
        fail InvalidCommandExecutionError, {container: c.id, message: "Container did not return anything (API BUG?)"}
      end

      unless results.key?('StatusCode')
        fail InvalidCommandExecutionError, {container: c.id, message: "Container did not return a status code (API BUG?)"}
      end

      unless results['StatusCode'] == 0
        fail InvalidCommandExecutionError, {container: c.id, message: "Container exited with code #{results['StatusCode']}"}
      end
    rescue
      # on error, kill the streaming logs and reraise the exception
      t.kill
      raise
    ensure
      # always rejoin the thread
      t.join
    end
  end
end
from_repo(repo, tag = 'latest') click to toggle source
# File lib/drydock/phase_chain.rb, line 131
def self.from_repo(repo, tag = 'latest')
  new(Docker::Image.create(build_pull_opts(repo, tag)))
end
new(from, parent = nil) click to toggle source
# File lib/drydock/phase_chain.rb, line 151
def initialize(from, parent = nil)
  @chain  = []
  @from   = from
  @parent = parent
  @children = []

  @finalized = false

  @ephemeral_containers = []

  if parent
    parent.children << self
  end
end
propagate_config!(src_image, config_name, opts, opt_key) click to toggle source
# File lib/drydock/phase_chain.rb, line 135
def self.propagate_config!(src_image, config_name, opts, opt_key)
  if opts.key?(opt_key)
    Drydock.logger.info("Command override: #{opts[opt_key].inspect}")
  else
    src_image.refresh!
    if src_image.info && src_image.info.key?('Config')
      src_image_config = src_image.info['Config']
      opts[opt_key]    = src_image_config[config_name] if src_image_config.key?(config_name)
    end

    Drydock.logger.debug(message: "Command retrieval: #{opts[opt_key].inspect}")
    Drydock.logger.debug(message: "Source image info: #{src_image.info.class} #{src_image.info.inspect}")
    Drydock.logger.debug(message: "Source image config: #{src_image.info['Config'].inspect}")
  end
end

Public Instance Methods

children() click to toggle source
# File lib/drydock/phase_chain.rb, line 166
def children
  @children
end
containers() click to toggle source
# File lib/drydock/phase_chain.rb, line 170
def containers
  map(&:build_container)
end
depth() click to toggle source
# File lib/drydock/phase_chain.rb, line 174
def depth
  @parent ? @parent.depth + 1 : 1
end
derive() click to toggle source
# File lib/drydock/phase_chain.rb, line 178
def derive
  self.class.new(last_image, self)
end
destroy!(force: false) click to toggle source
# File lib/drydock/phase_chain.rb, line 182
def destroy!(force: false)
  return self if frozen?

  finalize!
  children.reverse_each { |c| c.destroy!(force: force) } if children
  reverse_each { |p| p.destroy!(force: force) }

  freeze
end
each(&blk) click to toggle source
# File lib/drydock/phase_chain.rb, line 192
def each(&blk)
  @chain.each(&blk)
end
ephemeral_containers() click to toggle source
# File lib/drydock/phase_chain.rb, line 196
def ephemeral_containers
  @ephemeral_containers
end
finalize!(force: false) click to toggle source
# File lib/drydock/phase_chain.rb, line 200
def finalize!(force: false)
  return self if finalized?

  children.reverse_each { |c| c.finalize!(force: force) } if children
  ephemeral_containers.reverse_each { |c| c.remove(force: force) }

  Drydock.logger.info("##{serial}: Final image ID is #{last_image.id}") unless empty?
  reverse_each { |p| p.finalize!(force: force) }

  @finalized = true
  self
end
finalized?() click to toggle source
# File lib/drydock/phase_chain.rb, line 213
def finalized?
  @finalized
end
images() click to toggle source
# File lib/drydock/phase_chain.rb, line 217
def images
  [root_image] + map(&:result_image)
end
last_image() click to toggle source
# File lib/drydock/phase_chain.rb, line 221
def last_image
  @chain.last ? @chain.last.result_image : nil
end
root_image() click to toggle source
# File lib/drydock/phase_chain.rb, line 225
def root_image
  @from
end
run(cmd, opts = {}) { |container| ... } click to toggle source
# File lib/drydock/phase_chain.rb, line 229
def run(cmd, opts = {}, &blk)
  src_image = last ? last.result_image : @from
  no_commit = opts.fetch(:no_commit, false)

  no_cache = opts.fetch(:no_cache, false)
  no_cache = true if no_commit

  Drydock.logger.debug(message: "Source image: #{src_image.inspect}")
  build_config = self.class.build_container_opts(src_image.id, cmd, opts)
  cached_image = ImageRepository.find_by_config(build_config)

  if cached_image && !no_cache
    Drydock.logger.info(message: "Using cached image ID #{cached_image.id.slice(0, 12)}")

    if no_commit
      Drydock.logger.info(message: "Skipping commit phase")
    else
      self << Phase.from(
        source_image: src_image,
        result_image: cached_image
      )
    end
  else
    if cached_image && no_commit
      Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_commit")
    elsif cached_image && no_cache
      Drydock.logger.info(message: "Found cached image ID #{cached_image.id.slice(0, 12)}, but skipping due to :no_cache")
    end

    container = self.class.create_container(build_config)
    yield container if block_given?

    if no_commit
      Drydock.logger.info(message: "Skipping commit phase")
      ephemeral_containers << container
    else
      self.class.propagate_config!(src_image, 'Cmd',        opts, :command)
      self.class.propagate_config!(src_image, 'Entrypoint', opts, :entrypoint)
      commit_config = self.class.build_commit_opts(opts)

      result = container.commit(commit_config)
      Drydock.logger.info(message: "Committed image ID #{result.id.slice(0, 12)}")

      self << Phase.from(
        source_image:    src_image,
        build_container: container,
        result_image:    result
      )
    end
  end

  self
end
serial() click to toggle source
# File lib/drydock/phase_chain.rb, line 283
def serial
  @parent ? "#{@parent.serial}.#{@parent.children.index(self) + 1}.#{size + 1}" : "#{size + 1}"
end
tag(repo, tag = 'latest', force: false) click to toggle source
# File lib/drydock/phase_chain.rb, line 287
def tag(repo, tag = 'latest', force: false)
  last_image.tag(repo: repo, tag: tag, force: force)
end