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
Public Class Methods
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
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
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
Performed a prioritized recursive merge of several source Hashes and returns a new merged Hash
. There are 3 sources of configuration data:
-
local config
-
project config
-
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
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
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
@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
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
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
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
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
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
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
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
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