ripta/drydock

View on GitHub
lib/drydock/container_config.rb

Summary

Maintainability
C
1 day
Test Coverage

module Drydock
  # A comparable way of storing the `ContainerConfig` hash of a `Docker::Image#info`.
  #
  # @example
  #   ContainerConfig.from(Cmd: '/bin/ls -l')
  class ContainerConfig < ::Hash

    # The available options and their default values.
    DEFAULTS = {
      'MetaOptions'  => {},
      'OpenStdin'    => false,
      'AttachStdin'  => false,
      'AttachStdout' => false,
      'AttachStderr' => false,
      'User'         => '',
      'Tty'          => false,
      'Cmd'          => nil,
      'Env'          => ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'],
      'Labels'       => nil,
      'Entrypoint'   => nil,
      'ExposedPorts' => nil,
      'Volumes'      => nil
    }

    # Build a new ContainerConfig from a hash. Returns `nil` if the `hash` is nil.
    # If a key is not provided, its default value is used.
    #
    # Each key may be a camel-cased string or a camel-cased symbol.
    #
    # @param [#each_pair, nil] hash the source hash, which may be sparse
    # @return [ContainerConfig, nil]
    # @example The following examples produce the same object:
    #   ContainerConfig.from(AttachStdout: true)
    #   ContainerConfig.from('AttachStdout' => true)
    #   ContainerConfig.new.tap do |cfg|
    #     cfg.attach_stdout = true
    #   end
    def self.from(hash)
      return nil if hash.nil?

      new.tap do |cfg|
        hash.each_pair do |k, v|
          cfg[k] = v
        end
      end
    end

    # Create a new ContainerConfig with the default values pre-populated.
    def initialize
      DEFAULTS.each_pair do |k, v|
        begin
          self[k] = v.dup
        rescue TypeError
          self[k] = v
        end
      end
    end

    # Logic taken from https://github.com/docker/docker/blob/master/runconfig/compare.go
    # Last updated to conform to docker v1.9.1
    #
    # @param [ContainerConfig, nil] other the other object to compare to
    def ==(other)
      return false if other.nil?

      return false if self['OpenStdin'] || other['OpenStdin']
      return false if self['AttachStdout'] != other['AttachStdout']
      return false if self['AttachStderr'] != other['AttachStderr']

      return false if self['User'] != other['User']
      return false if self['Tty'] != other['Tty']

      return false if self['Cmd'] != other['Cmd']
      return false if Array(self['Env']).sort != Array(other['Env']).sort
      return false if (self['Labels'] || {}) != (other['Labels'] || {})
      return false if self['Entrypoint'] != other['Entrypoint']

      my_ports = self['ExposedPorts'] || {}
      other_ports = other['ExposedPorts'] || {}
      return false if my_ports.keys.size != other_ports.keys.size
      my_ports.keys.each do |my_port|
        return false unless other_ports.key?(my_port)
      end

      my_vols = self['Volumes'] || {}
      other_vols = other['Volumes'] || {}
      return false if my_vols.keys.size != other_vols.keys.size
      my_vols.keys.each do |my_vol|
        return false unless other_vols.key?(my_vol)
      end

      return true
    end

    # Retrieve the value of option `key`. The `key` must be in camel case, e.g.,
    # `AttachStdout`. See DEFAULTS for the correct capitalization.
    def [](key)
      super(key.to_s)
    end

    # Set an option `key` to `value`. The `key` must be in camel case, e.g.,
    # `AttachStdout`. See DEFAULTS for the correct capitalization.
    def []=(key, value)
      super(key.to_s, value)
    end

    # Handle convenience methods in `snake_case`. The following pairs of lines
    # achieve the same result:
    #
    #   cfg[:AttachStdout] = true
    #   cfg.attach_stdout = true
    #
    #   cfg[:ExposedPorts]
    #   cfg.exposed_ports
    def method_missing(name, *args, &_block)
      is_setter, attr_name = normalize(name)

      if DEFAULTS.key?(attr_name)
        self[attr_name] = args.first if is_setter
        self[attr_name]
      else
        super
      end
    end

    def respond_to_missing?(name, _include_private = false)
      _, attr_name = normalize(name)
      DEFAULTS.key?(attr_name)
    end

    private

    def camelize(name)
      name = name.to_s.dup
      return name if name.match(/^[A-Z]/)

      name.sub(/^[a-z\d]*/) { $&.capitalize }
          .gsub(/(?:_)([a-z\d]*)/i) { $1.capitalize }
    end

    def normalize(method_name)
      [
        method_name.to_s.end_with?('='),
        camelize(method_name.to_s.sub(/=$/, ''))
      ]
    end

  end
end