# frozen_string_literal: true require_relative 'data_adapter' require_relative 'lookup_key' module Puppet::Pops module Lookup # A LookupAdapter is a specialized DataAdapter that uses its hash to store data providers. It also remembers the compiler # that it is attached to and maintains a cache of _lookup options_ retrieved from the data providers associated with the # compiler's environment. # # @api private class LookupAdapter < DataAdapter LOOKUP_OPTIONS_PREFIX = LOOKUP_OPTIONS + '.' LOOKUP_OPTIONS_PREFIX.freeze LOOKUP_OPTIONS_PATTERN_START = '^' HASH = 'hash' MERGE = 'merge' CONVERT_TO = 'convert_to' NEW = 'new' def self.create_adapter(compiler) new(compiler) end def initialize(compiler) super() @compiler = compiler @lookup_options = {} # Get a KeyRecorder from context, and set a "null recorder" if not defined @key_recorder = Puppet.lookup(:lookup_key_recorder) { KeyRecorder.singleton } end # Performs a lookup using global, environment, and module data providers. Merge the result using the given # _merge_ strategy. If the merge strategy is nil, then an attempt is made to find merge options in the # `lookup_options` hash for an entry associated with the key. If no options are found, the no merge is performed # and the first found entry is returned. # # @param key [String] The key to lookup # @param lookup_invocation [Invocation] the lookup invocation # @param merge [MergeStrategy,String,Hash{String => Object},nil] Merge strategy, merge strategy name, strategy and options hash, or nil (implies "first found") # @return [Object] the found object # @throw :no_such_key when the object is not found # def lookup(key, lookup_invocation, merge) # The 'lookup_options' key is reserved and not found as normal data if key == LOOKUP_OPTIONS || key.start_with?(LOOKUP_OPTIONS_PREFIX) lookup_invocation.with(:invalid_key, LOOKUP_OPTIONS) do throw :no_such_key end end # Record that the key was looked up. This will record all keys for which a lookup is performed # except 'lookup_options' (since that is illegal from a user perspective, # and from an impact perspective is always looked up). @key_recorder.record(key) key = LookupKey.new(key) lookup_invocation.lookup(key, key.module_name) do if lookup_invocation.only_explain_options? catch(:no_such_key) { do_lookup(LookupKey::LOOKUP_OPTIONS, lookup_invocation, HASH) } nil else lookup_options = lookup_lookup_options(key, lookup_invocation) || {} if merge.nil? # Used cached lookup_options # merge = lookup_merge_options(key, lookup_invocation) merge = lookup_options[MERGE] lookup_invocation.report_merge_source(LOOKUP_OPTIONS) unless merge.nil? end convert_result(key.to_s, lookup_options, lookup_invocation, lambda do lookup_invocation.with(:data, key.to_s) do catch(:no_such_key) { return do_lookup(key, lookup_invocation, merge) } throw :no_such_key if lookup_invocation.global_only? key.dig(lookup_invocation, lookup_default_in_module(key, lookup_invocation)) end end) end end end # Performs a possible conversion of the result of calling `the_lookup` lambda # The conversion takes place if there is a 'convert_to' key in the lookup_options # If there is no conversion, the result of calling `the_lookup` is returned # otherwise the successfully converted value. # Errors are raised if the convert_to is faulty (bad type string, or if a call to # new(T, ) fails. # # @param key [String] The key to lookup # @param lookup_options [Hash] a hash of options # @param lookup_invocation [Invocation] the lookup invocation # @param the_lookup [Lambda] zero arg lambda that performs the lookup of a value # @return [Object] the looked up value, or converted value if there was conversion # @throw :no_such_key when the object is not found (if thrown by `the_lookup`) # def convert_result(key, lookup_options, lookup_invocation, the_lookup) result = the_lookup.call convert_to = lookup_options[CONVERT_TO] return result if convert_to.nil? convert_to = convert_to.is_a?(Array) ? convert_to : [convert_to] if convert_to[0].is_a?(String) begin convert_to[0] = Puppet::Pops::Types::TypeParser.singleton.parse(convert_to[0]) rescue StandardError => e raise Puppet::DataBinding::LookupError, _("Invalid data type in lookup_options for key '%{key}' could not parse '%{source}', error: '%{msg}") % { key: key, source: convert_to[0], msg: e.message} end end begin result = lookup_invocation.scope.call_function(NEW, [convert_to[0], result, *convert_to[1..-1]]) # TRANSLATORS 'lookup_options', 'convert_to' and args_string variable should not be translated, args_string = Puppet::Pops::Types::StringConverter.singleton.convert(convert_to) lookup_invocation.report_text { _("Applying convert_to lookup_option with arguments %{args}") % { args: args_string } } rescue StandardError => e raise Puppet::DataBinding::LookupError, _("The convert_to lookup_option for key '%{key}' raised error: %{msg}") % { key: key, msg: e.message} end result end def lookup_global(key, lookup_invocation, merge_strategy) # hiera_xxx will always use global_provider regardless of data_binding_terminus setting terminus = lookup_invocation.hiera_xxx_call? ? :hiera : Puppet[:data_binding_terminus] case terminus when :hiera, 'hiera' provider = global_provider(lookup_invocation) throw :no_such_key if provider.nil? provider.key_lookup(key, lookup_invocation, merge_strategy) when :none, 'none', '', nil # If global lookup is disabled, immediately report as not found lookup_invocation.report_not_found(key) throw :no_such_key else lookup_invocation.with(:global, terminus) do catch(:no_such_key) do return lookup_invocation.report_found(key, Puppet::DataBinding.indirection.find(key.root_key, {:environment => environment, :variables => lookup_invocation.scope, :merge => merge_strategy})) end lookup_invocation.report_not_found(key) throw :no_such_key end end rescue Puppet::DataBinding::LookupError => detail raise detail unless detail.issue_code.nil? error = Puppet::Error.new(_("Lookup of key '%{key}' failed: %{detail}") % { key: lookup_invocation.top_key, detail: detail.message }) error.set_backtrace(detail.backtrace) raise error end def lookup_in_environment(key, lookup_invocation, merge_strategy) provider = env_provider(lookup_invocation) throw :no_such_key if provider.nil? provider.key_lookup(key, lookup_invocation, merge_strategy) end def lookup_in_module(key, lookup_invocation, merge_strategy) module_name = lookup_invocation.module_name # Do not attempt to do a lookup in a module unless the name is qualified. throw :no_such_key if module_name.nil? provider = module_provider(lookup_invocation, module_name) if provider.nil? if environment.module(module_name).nil? lookup_invocation.report_module_not_found(module_name) else lookup_invocation.report_module_provider_not_found(module_name) end throw :no_such_key end provider.key_lookup(key, lookup_invocation, merge_strategy) end def lookup_default_in_module(key, lookup_invocation) module_name = lookup_invocation.module_name # Do not attempt to do a lookup in a module unless the name is qualified. throw :no_such_key if module_name.nil? provider = module_provider(lookup_invocation, module_name) throw :no_such_key if provider.nil? || !provider.config(lookup_invocation).has_default_hierarchy? lookup_invocation.with(:scope, "Searching default_hierarchy of module \"#{module_name}\"") do merge_strategy = nil if merge_strategy.nil? @module_default_lookup_options ||= {} options = @module_default_lookup_options.fetch(module_name) do |k| meta_invocation = Invocation.new(lookup_invocation.scope) meta_invocation.lookup(LookupKey::LOOKUP_OPTIONS, k) do opts = nil lookup_invocation.with(:scope, "Searching for \"#{LookupKey::LOOKUP_OPTIONS}\"") do catch(:no_such_key) do opts = compile_patterns( validate_lookup_options( provider.key_lookup_in_default(LookupKey::LOOKUP_OPTIONS, meta_invocation, MergeStrategy.strategy(HASH)), k)) end end @module_default_lookup_options[k] = opts end end lookup_options = extract_lookup_options_for_key(key, options) merge_strategy = lookup_options[MERGE] unless lookup_options.nil? end lookup_invocation.with(:scope, "Searching for \"#{key}\"") do provider.key_lookup_in_default(key, lookup_invocation, merge_strategy) end end end # Retrieve the merge options that match the given `name`. # # @param key [LookupKey] The key for which we want merge options # @param lookup_invocation [Invocation] the lookup invocation # @return [String,Hash,nil] The found merge options or nil # def lookup_merge_options(key, lookup_invocation) lookup_options = lookup_lookup_options(key, lookup_invocation) lookup_options.nil? ? nil : lookup_options[MERGE] end # Retrieve the lookup options that match the given `name`. # # @param key [LookupKey] The key for which we want lookup options # @param lookup_invocation [Puppet::Pops::Lookup::Invocation] the lookup invocation # @return [String,Hash,nil] The found lookup options or nil # def lookup_lookup_options(key, lookup_invocation) module_name = key.module_name # Retrieve the options for the module. We use nil as a key in case we have no module if !@lookup_options.include?(module_name) options = retrieve_lookup_options(module_name, lookup_invocation, MergeStrategy.strategy(HASH)) @lookup_options[module_name] = options else options = @lookup_options[module_name] end extract_lookup_options_for_key(key, options) end def extract_lookup_options_for_key(key, options) return nil if options.nil? rk = key.root_key key_opts = options[0] unless key_opts.nil? key_opt = key_opts[rk] return key_opt unless key_opt.nil? end patterns = options[1] patterns.each_pair { |pattern, value| return value if pattern =~ rk } unless patterns.nil? nil end # @param lookup_invocation [Puppet::Pops::Lookup::Invocation] the lookup invocation # @return [Boolean] `true` if an environment data provider version 5 is configured def has_environment_data_provider?(lookup_invocation) ep = env_provider(lookup_invocation) ep.nil? ? false : ep.config(lookup_invocation).version >= 5 end # @return [Pathname] the full path of the hiera.yaml config file def global_hiera_config_path @global_hiera_config_path ||= Pathname.new(Puppet.settings[:hiera_config]) end # @param path [String] the absolute path name of the global hiera.yaml file. # @return [LookupAdapter] self def set_global_hiera_config_path(path) @global_hiera_config_path = Pathname.new(path) self end def global_only? instance_variable_defined?(:@global_only) ? @global_only : false end # Instructs the lookup framework to only perform lookups in the global layer # @return [LookupAdapter] self def set_global_only @global_only = true self end private PROVIDER_STACK = [:lookup_global, :lookup_in_environment, :lookup_in_module].freeze def validate_lookup_options(options, module_name) return nil if options.nil? raise Puppet::DataBinding::LookupError.new(_("value of %{opts} must be a hash") % { opts: LOOKUP_OPTIONS }) unless options.is_a?(Hash) return options if module_name.nil? pfx = "#{module_name}::" options.each_pair do |key, value| if key.start_with?(LOOKUP_OPTIONS_PATTERN_START) unless key[1..pfx.length] == pfx raise Puppet::DataBinding::LookupError.new(_("all %{opts} patterns must match a key starting with module name '%{module_name}'") % { opts: LOOKUP_OPTIONS, module_name: module_name }) end else unless key.start_with?(pfx) raise Puppet::DataBinding::LookupError.new(_("all %{opts} keys must start with module name '%{module_name}'") % { opts: LOOKUP_OPTIONS, module_name: module_name }) end end end end def compile_patterns(options) return nil if options.nil? key_options = {} pattern_options = {} options.each_pair do |key, value| if key.start_with?(LOOKUP_OPTIONS_PATTERN_START) pattern_options[Regexp.compile(key)] = value else key_options[key] = value end end [key_options.empty? ? nil : key_options, pattern_options.empty? ? nil : pattern_options] end def do_lookup(key, lookup_invocation, merge) if lookup_invocation.global_only? key.dig(lookup_invocation, lookup_global(key, lookup_invocation, merge)) else merge_strategy = Puppet::Pops::MergeStrategy.strategy(merge) key.dig(lookup_invocation, merge_strategy.lookup(PROVIDER_STACK, lookup_invocation) { |m| send(m, key, lookup_invocation, merge_strategy) }) end end GLOBAL_ENV_MERGE = 'Global and Environment' # Retrieve lookup options that applies when using a specific module (i.e. a merge of the pre-cached # `env_lookup_options` and the module specific data) def retrieve_lookup_options(module_name, lookup_invocation, merge_strategy) meta_invocation = Invocation.new(lookup_invocation.scope) meta_invocation.lookup(LookupKey::LOOKUP_OPTIONS, lookup_invocation.module_name) do meta_invocation.with(:meta, LOOKUP_OPTIONS) do if meta_invocation.global_only? compile_patterns(global_lookup_options(meta_invocation, merge_strategy)) else opts = env_lookup_options(meta_invocation, merge_strategy) unless module_name.nil? # Store environment options at key nil. This removes the need for an additional lookup for keys that are not prefixed. @lookup_options[nil] = compile_patterns(opts) unless @lookup_options.include?(nil) catch(:no_such_key) do module_opts = validate_lookup_options(lookup_in_module(LookupKey::LOOKUP_OPTIONS, meta_invocation, merge_strategy), module_name) opts = if opts.nil? module_opts elsif module_opts merge_strategy.lookup([GLOBAL_ENV_MERGE, "Module #{lookup_invocation.module_name}"], meta_invocation) do |n| meta_invocation.with(:scope, n) { meta_invocation.report_found(LOOKUP_OPTIONS, n == GLOBAL_ENV_MERGE ? opts : module_opts) } end end end end compile_patterns(opts) end end end end # Retrieve and cache the global lookup options def global_lookup_options(lookup_invocation, merge_strategy) if !instance_variable_defined?(:@global_lookup_options) @global_lookup_options = nil catch(:no_such_key) { @global_lookup_options = validate_lookup_options(lookup_global(LookupKey::LOOKUP_OPTIONS, lookup_invocation, merge_strategy), nil) } end @global_lookup_options end # Retrieve and cache lookup options specific to the environment of the compiler that this adapter is attached to (i.e. a merge # of global and environment lookup options). def env_lookup_options(lookup_invocation, merge_strategy) if !instance_variable_defined?(:@env_lookup_options) global_options = global_lookup_options(lookup_invocation, merge_strategy) @env_only_lookup_options = nil catch(:no_such_key) { @env_only_lookup_options = validate_lookup_options(lookup_in_environment(LookupKey::LOOKUP_OPTIONS, lookup_invocation, merge_strategy), nil) } if global_options.nil? @env_lookup_options = @env_only_lookup_options elsif @env_only_lookup_options.nil? @env_lookup_options = global_options else @env_lookup_options = merge_strategy.merge(global_options, @env_only_lookup_options) end end @env_lookup_options end def global_provider(lookup_invocation) @global_provider = GlobalDataProvider.new unless instance_variable_defined?(:@global_provider) @global_provider end def env_provider(lookup_invocation) @env_provider = initialize_env_provider(lookup_invocation) unless instance_variable_defined?(:@env_provider) @env_provider end def module_provider(lookup_invocation, module_name) # Test if the key is present for the given module_name. It might be there even if the # value is nil (which indicates that no module provider is configured for the given name) unless self.include?(module_name) self[module_name] = initialize_module_provider(lookup_invocation, module_name) end self[module_name] end def initialize_module_provider(lookup_invocation, module_name) mod = environment.module(module_name) return nil if mod.nil? metadata = mod.metadata provider_name = metadata.nil? ? nil : metadata['data_provider'] mp = nil if mod.has_hiera_conf? mp = ModuleDataProvider.new(module_name) # A version 5 hiera.yaml trumps a data provider setting in the module mp_config = mp.config(lookup_invocation) if mp_config.nil? mp = nil elsif mp_config.version >= 5 unless provider_name.nil? || Puppet[:strict] == :off Puppet.warn_once('deprecations', "metadata.json#data_provider-#{module_name}", _("Defining \"data_provider\": \"%{name}\" in metadata.json is deprecated. It is ignored since a '%{config}' with version >= 5 is present") % { name: provider_name, config: HieraConfig::CONFIG_FILE_NAME }, mod.metadata_file) end provider_name = nil end end if provider_name.nil? mp else unless Puppet[:strict] == :off msg = _("Defining \"data_provider\": \"%{name}\" in metadata.json is deprecated.") % { name: provider_name } msg += " " + _("A '%{hiera_config}' file should be used instead") % { hiera_config: HieraConfig::CONFIG_FILE_NAME } if mp.nil? Puppet.warn_once('deprecations', "metadata.json#data_provider-#{module_name}", msg, mod.metadata_file) end case provider_name when 'none' nil when 'hiera' mp || ModuleDataProvider.new(module_name) when 'function' mp = ModuleDataProvider.new(module_name) mp.config = HieraConfig.v4_function_config(Pathname(mod.path), "#{module_name}::data", mp) mp else raise Puppet::Error.new(_("Environment '%{env}', cannot find module_data_provider '%{provider}'")) % { env: environment.name, provider: provider_name } end end end def initialize_env_provider(lookup_invocation) env_conf = environment.configuration return nil if env_conf.nil? || env_conf.path_to_env.nil? # Get the name of the data provider from the environment's configuration provider_name = env_conf.environment_data_provider env_path = Pathname(env_conf.path_to_env) config_path = env_path + HieraConfig::CONFIG_FILE_NAME ep = nil if config_path.exist? ep = EnvironmentDataProvider.new # A version 5 hiera.yaml trumps any data provider setting in the environment.conf ep_config = ep.config(lookup_invocation) if ep_config.nil? ep = nil elsif ep_config.version >= 5 unless provider_name.nil? || Puppet[:strict] == :off Puppet.warn_once('deprecations', 'environment.conf#data_provider', _("Defining environment_data_provider='%{provider_name}' in environment.conf is deprecated") % { provider_name: provider_name }, env_path + 'environment.conf') unless provider_name == 'hiera' Puppet.warn_once('deprecations', 'environment.conf#data_provider_overridden', _("The environment_data_provider='%{provider_name}' setting is ignored since '%{config_path}' version >= 5") % { provider_name: provider_name, config_path: config_path }, env_path + 'environment.conf') end end provider_name = nil end end if provider_name.nil? ep else unless Puppet[:strict] == :off msg = _("Defining environment_data_provider='%{provider_name}' in environment.conf is deprecated.") % { provider_name: provider_name } msg += " " + _("A '%{hiera_config}' file should be used instead") % { hiera_config: HieraConfig::CONFIG_FILE_NAME } if ep.nil? Puppet.warn_once('deprecations', 'environment.conf#data_provider', msg, env_path + 'environment.conf') end case provider_name when 'none' nil when 'hiera' # Use hiera.yaml or default settings if it is missing ep || EnvironmentDataProvider.new when 'function' ep = EnvironmentDataProvider.new ep.config = HieraConfigV5.v4_function_config(env_path, 'environment::data', ep) ep else raise Puppet::Error.new(_("Environment '%{env}', cannot find environment_data_provider '%{provider}'") % { env: environment.name, provider: provider_name }) end end end # @return [Puppet::Node::Environment] the environment of the compiler that this adapter is associated with def environment @compiler.environment end end end end require_relative 'invocation' require_relative 'global_data_provider' require_relative 'environment_data_provider' require_relative 'module_data_provider'