class Kitchen::Loader::YAML

YAML file loader for Test Kitchen configuration. This class is responisble for parsing the main YAML file and the local YAML if it exists. Local file configuration will win over the default configuration. The client of this class should not require any YAML loading or parsing logic.

@author Fletcher Nichol <fnichol@nichol.ca>

Attributes

config_file[R]

@return [String] the absolute path to the Kitchen config YAML file @api private

global_config_file[R]

@return [String] the absolute path to the Kitchen global config YAML

file

@api private

local_config_file[R]

@return [String] the absolute path to the Kitchen local config YAML

file

@api private

Public Class Methods

new(options = {}) click to toggle source

Creates a new loader that can parse and load YAML files.

@param options [Hash] configuration for a new loader @option options [String] :project_config path to the Kitchen

config YAML file (default: `./kitchen.yml`)

@option options [String] :local_config path to the Kitchen local

config YAML file (default: `./kitchen.local.yml`)

@option options [String] :global_config path to the Kitchen global

config YAML file (default: `$HOME/.kitchen/config.yml`)

@option options [String] :process_erb whether or not to process YAML

through an ERB processor (default: `true`)

@option options [String] :process_local whether or not to process a

local kitchen YAML file, if it exists (default: `true`)
# File lib/kitchen/loader/yaml.rb, line 46
def initialize(options = {})
  @config_file =
    File.expand_path(options[:project_config] || default_config_file)
  @local_config_file =
    File.expand_path(options[:local_config] || default_local_config_file)
  @global_config_file =
    File.expand_path(options[:global_config] || default_global_config_file)

  @process_erb = options.fetch(:process_erb, true)
  @process_local = options.fetch(:process_local, true)
  @process_global = options.fetch(:process_global, true)
end

Public Instance Methods

diagnose() click to toggle source

Returns a Hash of configuration and other useful diagnostic information.

@return [Hash] a diagnostic hash

# File lib/kitchen/loader/yaml.rb, line 74
def diagnose
  result = {}
  result[:process_erb] = @process_erb
  result[:process_local] = @process_local
  result[:process_global] = @process_global
  result[:global_config] = diagnose_component(:global_yaml, global_config_file)
  result[:project_config] = diagnose_component(:yaml, config_file)
  result[:local_config] = diagnose_component(:local_yaml, local_config_file)
  result[:combined_config] = diagnose_component(:combined_hash)
  result
end
read() click to toggle source

Reads, parses, and merges YAML configuration files and returns a Hash of tne merged data.

@return [Hash] merged configuration data

# File lib/kitchen/loader/yaml.rb, line 63
def read
  unless File.exist?(config_file)
    raise UserError, "Kitchen YAML file #{config_file} does not exist."
  end

  Util.symbolized_hash(combined_hash)
end

Private Instance Methods

combined_hash() click to toggle source

Performed a prioritized recursive merge of several source Hashes and returns a new merged Hash. There are 3 sources of configuration data:

  1. local config

  2. project config

  3. global config

The merge order is local -> project -> global, meaning that elements at the top of the above list will be merged last, and have greater precedence than elements at the bottom of the list.

@return [Hash] a new merged Hash @api private

# File lib/kitchen/loader/yaml.rb, line 115
def combined_hash
  y = if @process_global
        normalize(global_yaml).rmerge(normalize(yaml))
      else
        normalize(yaml)
      end
  @process_local ? y.rmerge(normalize(local_yaml)) : y
end
default_config_file() click to toggle source

Determines the default absolute path to the Kitchen config YAML file, based on current working directory. We prefer `kitchen.yml` to the older `.kitchen.yml`.

@return [String] an absolute path to a Kitchen config YAML file @api private

# File lib/kitchen/loader/yaml.rb, line 194
def default_config_file
  if File.exist?(kitchen_yml) && File.exist?(dot_kitchen_yml)
    raise UserError, "Both #{kitchen_yml} and #{dot_kitchen_yml} found. Please use the un-dotted variant: #{kitchen_yml}."
  end

  if !File.exist?(kitchen_yml) && !File.exist?(dot_kitchen_yml)
    return kitchen_yml
  end

  File.exist?(kitchen_yml) ? kitchen_yml : dot_kitchen_yml
end
default_global_config_file() click to toggle source

Determines the default absolute path to the Kitchen global YAML file, based on the base Kitchen config YAML file.

@return [String] an absolute path to a Kitchen global YAML file @api private

# File lib/kitchen/loader/yaml.rb, line 241
def default_global_config_file
  File.join(File.expand_path(ENV["HOME"]), ".kitchen", "config.yml")
end
default_local_config_file() click to toggle source

@return [String] an absolute path to a Kitchen local YAML file @raise [UserError] if both dotted and undotted versions of the default

local YAML file exist, e.g. both kitchen.local.yml and .kitchen.local.yml

@api private

# File lib/kitchen/loader/yaml.rb, line 223
def default_local_config_file
  config_dir, default_local_config = File.split(config_file.sub(/(#{File.extname(config_file)})$/, '.local\1'))

  undot_config = default_local_config.sub(/^\./, "")
  dot_config = ".#{undot_config}"

  if File.exist?(File.join(config_dir, undot_config)) && File.exist?(File.join(config_dir, dot_config))
    raise UserError, "Both #{undot_config} and #{dot_config} found in #{config_dir}. Please use #{default_local_config} which matches your #{config_file}."
  end

  File.exist?(File.join(config_dir, dot_config)) ? File.join(config_dir, dot_config) : File.join(config_dir, undot_config)
end
diagnose_component(component, file = nil) click to toggle source

Generate a diganose Hash for a particular YAML file Hash. If an error occurs when loading the data, then a failure hash will be inserted into the `:raw_data` sub-hash.

@param component [Symbol] a YAML source component @param file [String] the absolute path to a file which is used for

reporting (default: `nil`)

@return [Hash] a hash data structure @api private

# File lib/kitchen/loader/yaml.rb, line 254
def diagnose_component(component, file = nil)
  return if file && !File.exist?(file)

  hash = begin
    send(component)
         rescue => e
           failure_hash(e, file)
  end

  { filename: file, raw_data: hash }
end
dot_kitchen_yml() click to toggle source

The absolute path to an hidden Kitchen config YAML file.

# File lib/kitchen/loader/yaml.rb, line 212
def dot_kitchen_yml
  File.join(Dir.pwd, ".kitchen.yml")
end
failure_hash(e, file = nil) click to toggle source

Generates a Hash respresenting a failure, given an Exception object.

@param e [Exception] an exception @param file [String] the absolute path to a file (default: `nil`) @return [Hash] a hash data structure @api private

# File lib/kitchen/loader/yaml.rb, line 272
def failure_hash(e, file = nil)
  result = {
    error: {
      exception: e.inspect,
      message: e.message,
      backtrace: e.backtrace,
    },
  }
  result[:error][:raw_file] = IO.read(file) unless file.nil?
  result
end
global_yaml() click to toggle source

Loads and returns the Kitchen global config YAML as a Hash.

@return [Hash] the config hash @api private

# File lib/kitchen/loader/yaml.rb, line 144
def global_yaml
  parse_yaml_string(yaml_string(global_config_file), global_config_file)
end
kitchen_yml() click to toggle source

The absolute path to an un-hidden Kitchen config YAML file.

# File lib/kitchen/loader/yaml.rb, line 207
def kitchen_yml
  File.join(Dir.pwd, "kitchen.yml")
end
local_yaml() click to toggle source

Loads and returns the Kitchen local config YAML as a Hash.

@return [Hash] the config hash @api private

# File lib/kitchen/loader/yaml.rb, line 136
def local_yaml
  parse_yaml_string(yaml_string(local_config_file), local_config_file)
end
normalize(obj) click to toggle source

Destructively modify an object containing one or more hashes so that the resulting formatted data can be consumed upstream.

@param obj [Object] an object @return [Object] an object @api private

# File lib/kitchen/loader/yaml.rb, line 290
def normalize(obj)
  if obj.is_a?(Hash)
    obj.inject({}) { |h, (k, v)| normalize_hash(h, k, v); h }
  else
    obj
  end
end
normalize_hash(hash, key, value) click to toggle source

Normalizes certain keys in the root of a data hash to be a proper sub-hash in all cases. Specifically handled are the following cases:

  • If the value for certain keys (`“driver”`, `“provisioner”`, `“busser”`) are set to `nil`, a new Hash will be put in its place.

  • If the value for certain keys is a String, then the value is converted to a new Hash with a default key pointing to the original String.

Given a hash:

{ "driver" => nil }

this method would return:

{ "driver" => {} }

Given a hash:

{ :driver => "coolbeans" }

this method would return:

{ :name => { "driver" => "coolbeans" } }

@param hash [Hash] the Hash to normalize @param key [Symbol] the key to normalize @param value [Object] the value to normalize @api private

# File lib/kitchen/loader/yaml.rb, line 328
def normalize_hash(hash, key, value)
  case key
  when "driver", "provisioner", "busser"
    hash[key] = if value.nil?
                  {}
                elsif value.is_a?(String)
                  default_key = key == "busser" ? "version" : "name"
                  { default_key => value }
                else
                  normalize(value)
                end
  else
    hash[key] = normalize(value)
  end
end
parse_yaml_string(string, file_name) click to toggle source

Parses a YAML string and returns a Hash.

@param string [String] a yaml document as a string @param file_name [String] an absolute path to the file represented as

the passed in string, used for error reporting

@return [Hash] a hash @raise [UserError] if the string document cannot be parsed @api private

# File lib/kitchen/loader/yaml.rb, line 352
def parse_yaml_string(string, file_name)
  return {} if string.nil? || string.empty?

  result =
    if Gem::Requirement.new(">= 3.1.0").satisfied_by?(Gem::Version.new(Psych::VERSION))
      # ruby >= 2.6.0
      ::YAML.safe_load(string, permitted_classes: [Symbol], permitted_symbols: [], aliases: true) || {}
    else
      # ruby < 2.6.0
      ::YAML.safe_load(string, [Symbol], [], true) || {}
    end
  unless result.is_a?(Hash)
    raise UserError, "Error parsing #{file_name} as YAML " \
      "(Result of parse was not a Hash, but was a #{result.class}).\n" \
      "Please run `kitchen diagnose --no-instances --loader' to help " \
      "debug your issue."
  end
  result
rescue SyntaxError, Psych::SyntaxError, Psych::DisallowedClass
  raise UserError, "Error parsing #{file_name} as YAML.\n" \
    "Please run `kitchen diagnose --no-instances --loader' to help " \
    "debug your issue."
end
process_erb(string, file) click to toggle source

Passes a string through ERb to evaulate any ERb blocks.

@param string [String] the string to process @param file [String] an absolute path to the file represented as the

passed in string, used for error reporting

@return [String] a new string, passed through an ERb process @raise [UserError] if an ERb parsing error occurs @api private

# File lib/kitchen/loader/yaml.rb, line 167
def process_erb(string, file)
  tpl = ERB.new(string)
  tpl.filename = file
  tpl.result
rescue => e
  raise UserError, "Error parsing ERB content in #{file} " \
    "(#{e.class}: #{e.message}).\n" \
    "Please run `kitchen diagnose --no-instances --loader' to help " \
    "debug your issue."
end
read_file(file) click to toggle source

Reads a file and returns its contents as a string.

@param file [String] a path to a file @return [String] the files contents, or an empty string if the file

does not exist

@api private

# File lib/kitchen/loader/yaml.rb, line 184
def read_file(file)
  File.exist?(file.to_s) ? IO.read(file) : ""
end
yaml() click to toggle source

Loads and returns the Kitchen config YAML as a Hash.

@return [Hash] the config hash @api private

# File lib/kitchen/loader/yaml.rb, line 128
def yaml
  parse_yaml_string(yaml_string(config_file), config_file)
end
yaml_string(file) click to toggle source

Loads a file to a string and optionally passes it through an ERb process.

@return [String] a file's contents as a string @api private

# File lib/kitchen/loader/yaml.rb, line 153
def yaml_string(file)
  string = read_file(file)

  @process_erb ? process_erb(string, file) : string
end