# frozen_string_literal: true # The `eyaml_lookup_key` is a hiera 5 `lookup_key` data provider function. # See [the configuration guide documentation](https://puppet.com/docs/puppet/latest/hiera_config_yaml_5.html#configuring-a-hierarchy-level-hiera-eyaml) for # how to use this function. # # @since 5.0.0 # Puppet::Functions.create_function(:eyaml_lookup_key) do unless Puppet.features.hiera_eyaml? raise Puppet::DataBinding::LookupError, 'Lookup using eyaml lookup_key function is only supported when the hiera_eyaml library is present' end require 'hiera/backend/eyaml/encryptor' require 'hiera/backend/eyaml/utils' require 'hiera/backend/eyaml/options' require 'hiera/backend/eyaml/parser/parser' dispatch :eyaml_lookup_key do param 'String[1]', :key param 'Hash[String[1],Any]', :options param 'Puppet::LookupContext', :context end def eyaml_lookup_key(key, options, context) return context.cached_value(key) if context.cache_has_key(key) # Can't do this with an argument_mismatch dispatcher since there is no way to declare a struct that at least # contains some keys but may contain other arbitrary keys. unless options.include?('path') # TRANSLATORS 'eyaml_lookup_key':, 'path', 'paths' 'glob', 'globs', 'mapped_paths', and lookup_key should not be translated raise ArgumentError, _("'eyaml_lookup_key': one of 'path', 'paths' 'glob', 'globs' or 'mapped_paths' must be declared in hiera.yaml"\ " when using this lookup_key function") end # nil key is used to indicate that the cache contains the raw content of the eyaml file raw_data = context.cached_value(nil) if raw_data.nil? raw_data = load_data_hash(options, context) context.cache(nil, raw_data) end context.not_found unless raw_data.include?(key) context.cache(key, decrypt_value(raw_data[key], context, options, key)) end def load_data_hash(options, context) path = options['path'] context.cached_file_data(path) do |content| data = Puppet::Util::Yaml.safe_load(content, [Symbol], path) if data.is_a?(Hash) Puppet::Pops::Lookup::HieraConfig.symkeys_to_string(data) else msg = _("%{path}: file does not contain a valid yaml hash") % { path: path } raise Puppet::DataBinding::LookupError, msg if Puppet[:strict] == :error && data != false Puppet.warning(msg) {} end rescue Puppet::Util::Yaml::YamlLoadError => ex # YamlLoadErrors include the absolute path to the file, so no need to add that raise Puppet::DataBinding::LookupError, _("Unable to parse %{message}") % { message: ex.message } end end def decrypt_value(value, context, options, key) case value when String decrypt(value, context, options, key) when Hash result = {} value.each_pair { |k, v| result[context.interpolate(k)] = decrypt_value(v, context, options, key) } result when Array value.map { |v| decrypt_value(v, context, options, key) } else value end end def decrypt(data, context, options, key) if encrypted?(data) # Options must be set prior to each call to #parse since they end up as static variables in # the Options class. They cannot be set once before #decrypt_value is called, since each #decrypt # might cause a new lookup through interpolation. That lookup in turn, might use a different eyaml # config. # Hiera::Backend::Eyaml::Options.set(options) begin tokens = Hiera::Backend::Eyaml::Parser::ParserFactory.hiera_backend_parser.parse(data) data = tokens.map(&:to_plain_text).join.chomp rescue StandardError => ex raise Puppet::DataBinding::LookupError, _("hiera-eyaml backend error decrypting %{data} when looking up %{key} in %{path}. Error was %{message}") % { data: data, key: key, path: options['path'], message: ex.message } end end context.interpolate(data) end def encrypted?(data) /.*ENC\[.*?\]/ =~ data ? true : false end end