# A LookupAdapter is a specialized DataAdapter that uses its hash to store module providers. It also remembers the environment # that it is attached to and maintains a cache of _lookup options_ retrieved from its data providers. # class Puppet::DataProviders::LookupAdapter < Puppet::DataProviders::DataAdapter LOOKUP_OPTIONS = Puppet::Pops::Lookup::LOOKUP_OPTIONS LOOKUP_OPTIONS_PREFIX = LOOKUP_OPTIONS + '.' LOOKUP_OPTIONS_PREFIX.freeze HASH = 'hash'.freeze MERGE = 'merge'.freeze def self.create_adapter(env) new(env) end def initialize(env) super() @env = env @lookup_options = {} 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 merge [Puppet::Pops::MergeStrategy,String,Hash,nil] Merge strategy or hash with strategy and options # @param lookup_invocation [Puppet::Pops::Lookup::Invocation] Invocation data containing scope, overrides, and defaults # @return [Object] The found value # @throws :no_such_key if the given key 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 lookup_invocation.top_key ||= key merge_explained = false if lookup_invocation.explain_options? catch(:no_such_key) do module_name = extract_module_name(key) unless key == Puppet::Pops::Lookup::GLOBAL lookup_invocation.module_name = module_name if lookup_invocation.only_explain_options? do_lookup(LOOKUP_OPTIONS, lookup_invocation, HASH) return nil end # Bypass cache and do a "normal" lookup of the lookup_options lookup_invocation.with(:meta, LOOKUP_OPTIONS) do key_options = do_lookup(LOOKUP_OPTIONS, lookup_invocation, HASH)[key] merge = key_options[MERGE] unless key_options.nil? merge_explained = true end end elsif merge.nil? # Used cached lookup_options merge = lookup_merge_options(key, lookup_invocation) lookup_invocation.report_merge_source('lookup_options') unless merge.nil? end if merge_explained # Merge lookup is explained in detail so we need to explain the data in a section # on the same level to avoid confusion lookup_invocation.with(:data, key) { do_lookup(key, lookup_invocation, merge) } else do_lookup(key, lookup_invocation, merge) end end # @api private def lookup_global(name, lookup_invocation, merge_strategy) terminus = Puppet[:data_binding_terminus] lookup_invocation.with(:global, terminus) do catch(:no_such_key) do return lookup_invocation.report_found(name, Puppet::DataBinding.indirection.find(name, { :environment => @env, :variables => lookup_invocation.scope, :merge => merge_strategy })) end lookup_invocation.report_not_found(name) throw :no_such_key end rescue Puppet::DataBinding::LookupError => detail error = Puppet::Error.new("Lookup of key '#{lookup_invocation.top_key}' failed: #{detail.message}") error.set_backtrace(detail.backtrace) raise error end # @api private def lookup_in_environment(name, lookup_invocation, merge_strategy) env_provider.lookup(name, lookup_invocation, merge_strategy) end # @api private def lookup_in_module(name, lookup_invocation, merge_strategy) module_name = lookup_invocation.module_name || extract_module_name(name) # Do not attempt to do a lookup in a module unless the name is qualified. throw :no_such_key if module_name.nil? lookup_invocation.with(:module, module_name) do if @env.module(module_name).nil? lookup_invocation.report_module_not_found throw :no_such_key end module_provider(module_name).lookup(name, lookup_invocation, merge_strategy) end end # Retrieve the merge options that match the given `name`. # # @param name [String] The key for which we want merge options # @param lookup_invocation [Puppet::Pops::Lookup::Invocation] the lookup invocation # @return [String,Hash,nil] The found merge options or nil # def lookup_merge_options(name, lookup_invocation) lookup_options = lookup_lookup_options(name, lookup_invocation) lookup_options.nil? ? nil : lookup_options[MERGE] end # Retrieve the lookup options that match the given `name`. # # @param name [String] 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(name, lookup_invocation) module_name = extract_module_name(name) # Retrieve the options for the module. We use nil as a key in case we have none if !@lookup_options.include?(module_name) options = retrieve_lookup_options(module_name, lookup_invocation, Puppet::Pops::MergeStrategy.strategy(HASH)) raise Puppet::DataBinding::LookupError.new("value of #{LOOKUP_OPTIONS} must be a hash") unless options.nil? || options.is_a?(Hash) @lookup_options[module_name] = options else options = @lookup_options[module_name] end options.nil? ? nil : options[name] end private def do_lookup(key, lookup_invocation, merge) merge_strategy = Puppet::Pops::MergeStrategy.strategy(merge) lookup_invocation.with(:merge, merge_strategy) do result = merge_strategy.merge_lookup([:lookup_global, :lookup_in_environment, :lookup_in_module]) { |m| send(m, key, lookup_invocation, merge_strategy) } lookup_invocation.report_result(result) result end end # 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 = Puppet::Pops::Lookup::Invocation.new(lookup_invocation.scope) meta_invocation.top_key = lookup_invocation.top_key env_opts = env_lookup_options(meta_invocation, merge_strategy) unless module_name.nil? || @env.module(module_name).nil? catch(:no_such_key) do meta_invocation.module_name = module_name options = module_provider(module_name).lookup(LOOKUP_OPTIONS, meta_invocation, merge_strategy) options = merge_strategy.merge(env_opts, options) unless env_opts.nil? return options end end env_opts end # Retrieve and cache lookup options specific to the environment that this adapter is attached to (i.e. a merge # of global and environment lookup options). def env_lookup_options(meta_invocation, merge_strategy) if !instance_variable_defined?(:@env_lookup_options) @env_lookup_options = nil catch(:no_such_key) do @env_lookup_options = merge_strategy.merge_lookup([:lookup_global, :lookup_in_environment]) do |m| send(m, LOOKUP_OPTIONS, meta_invocation, merge_strategy) end end end @env_lookup_options end def env_provider @env_provider ||= initialize_env_provider end def module_provider(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 data.include?(module_name) data[module_name] = initialize_module_provider(module_name) end data[module_name] end def initialize_module_provider(module_name) injector = Puppet.lookup(:injector) { nil } # Support running tests without an injector being configured == using a null implementation return ModuleDataProvider.new() unless injector # Get the registry of module to provider implementation name module_service_type = Registry.hash_of_per_module_data_provider module_service_name = PER_MODULE_DATA_PROVIDER_KEY module_service = injector.lookup(nil, module_service_type, module_service_name) provider_name = module_service[module_name] || 'none' service_type = Registry.hash_of_module_data_providers service_name = MODULE_DATA_PROVIDERS_KEY # Get the service (registry of known implementations) service = injector.lookup(nil, service_type, service_name) provider = service[provider_name] unless provider raise Puppet::Error.new("Environment '#{@env.name}', cannot find module_data_provider '#{provider_name}'") end # Provider is configured per module but cached using environment life cycle so it must be cloned provider.clone end def initialize_env_provider injector = Puppet.lookup(:injector) { nil } # Support running tests without an injector being configured == using a null implementation return EnvironmentDataProvider.new() unless injector # Get the name of the data provider from the environment's configuration and find the bound implementation provider_name = @env.configuration.environment_data_provider service_type = Registry.hash_of_environment_data_providers service_name = ENV_DATA_PROVIDERS_KEY # Get the service (registry of known implementations) service = injector.lookup(nil, service_type, service_name) provider = service[provider_name] unless provider raise Puppet::Error.new("Environment '#{@env.name}', cannot find environment_data_provider '#{provider_name}'") end provider end def extract_module_name(name) qual_index = name.index('::') qual_index.nil? ? nil : name[0..qual_index-1] end end