# The BindingsComposer handles composition of multiple bindings sources # It is directed by a {Puppet::Pops::Binder::Config::BinderConfig BinderConfig} that indicates how # the final composition should be layered, and what should be included/excluded in each layer # # The bindings composer is intended to be used once per environment as the compiler starts its work. # # TODO: Possibly support envdir: scheme / relative to environment root (== same as confdir if there is only one environment). # This is probably easier to do after ENC changes described in ARM-8 have been implemented. # TODO: If same config is loaded in a higher layer, skip it in the lower (since it is meaningless to load it again with lower # precedence. (Optimization, or possibly an error, should produce a warning). # class Puppet::Pops::Binder::BindingsComposer # The BindingsConfig instance holding the read and parsed, but not evaluated configuration # @api public # attr_reader :config # map of scheme name to handler # @api private attr_reader :scheme_handlers # @return Hash<String, Puppet::Module> map of module name to module instance # @api private attr_reader :name_to_module # @api private attr_reader :confdir # @api private attr_reader :diagnostics # Container of all warnings and errors produced while initializing and loading bindings # # @api public attr_reader :acceptor # @api public def initialize() @acceptor = Puppet::Pops::Validation::Acceptor.new() @diagnostics = Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) @config = Puppet::Pops::Binder::Config::BinderConfig.new(@diagnostics) if acceptor.errors? Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => 'Binding Composer: error while reading config.') raise Puppet::DevError.new("Internal Error: IssueReporter did not raise exception for errors in bindings config.") end end # Configures and creates the boot injector. # The read config may optionally contain mapping of bindings scheme handler name to handler class, and # mapping of biera2 backend symbolic name to backend class. # If present, these are turned into bindings in the category 'extension' (which is only used in the boot injector) which # has higher precedence than 'default'. This is done to allow users to override the default bindings for # schemes and backends. # @param scope [Puppet::Parser:Scope] the scope (used to find compiler and injector for the environment) # @api private # def configure_and_create_injector(scope) # create the injector (which will pick up the bindings registered above) @scheme_handlers = SchemeHandlerHelper.new(scope) # get extensions from the config # ------------------------------ scheme_extensions = @config.scheme_extensions hiera_backends = @config.hiera_backends # Define a named bindings that are known by the SystemBindings boot_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings(Puppet::Pops::Binder::SystemBindings::ENVIRONMENT_BOOT_BINDINGS_NAME) do scheme_extensions.each_pair do |scheme, class_name| # turn each scheme => class_name into a binding (contribute to the buildings-schemes multibind). # do this in category 'extensions' to allow them to override the 'default' when_in_category('extension', 'true').bind do name(scheme) instance_of(Puppetx::BINDINGS_SCHEMES_TYPE) in_multibind(Puppetx::BINDINGS_SCHEMES) to_instance(class_name) end end hiera_backends.each_pair do |symbolic, class_name| # turn each symbolic => class_name into a binding (contribute to the hiera backends multibind). # do this in category 'extensions' to allow them to override the 'default' when_in_category('extension', 'true').bind do name(symbolic) instance_of(Puppetx::HIERA2_BACKENDS_TYPE) in_multibind(Puppetx::HIERA2_BACKENDS) to_instance(class_name) end end end @injector = scope.compiler.create_boot_injector(boot_bindings.model) end # @return [Puppet::Pops::Binder::Bindings::LayeredBindings] def compose(scope) # The boot injector is used to lookup scheme-handlers configure_and_create_injector(scope) # get all existing modules and their root path @name_to_module = {} scope.environment.modules.each {|mod| name_to_module[mod.name] = mod } # setup the confdir @confdir = Puppet.settings[:confdir] factory = Puppet::Pops::Binder::BindingsFactory contributions = [] configured_layers = @config.layering_config.collect do | layer_config | # get contributions with effective categories contribs = configure_layer(layer_config, scope, diagnostics) # collect the contributions separately for later checking of category precedence contributions.concat(contribs) # create a named layer with all the bindings for this layer factory.named_layer(layer_config['name'], *contribs.collect {|c| c.bindings }.flatten) end # must check all contributions are based on compatible category precedence # (Note that contributions no longer contains the bindings as a side effect of setting them in the collected # layer. The effective categories and the name remains in the contributed model; this is enough for checking # and error reporting). check_contribution_precedence(contributions) # Add the two system layers; the final - highest ("can not be overridden" layer), and the lowest # Everything here can be overridden 'default' layer. # configured_layers.insert(0, Puppet::Pops::Binder::SystemBindings.final_contribution) configured_layers.insert(-1, Puppet::Pops::Binder::SystemBindings.default_contribution) # and finally... create the resulting structure factory.layered_bindings(*configured_layers) end # Evaluates configured categorization and returns the result. # The result is not cached. # @api public # def effective_categories(scope) unevaluated_categories = @config.categorization parser = Puppet::Pops::Parser::EvaluatingParser.new() file_source = @config.config_file or "defaults in: #{__FILE__}" evaluated_categories = unevaluated_categories.collect do |category_tuple| evaluated_categories = [ category_tuple[0], parser.evaluate_string( scope, parser.quote( category_tuple[1] ), file_source ) ] if evaluated_categories[1].is_a?(String) # category values are always in lower case evaluated_categories[1] = evaluated_categories[1].downcase else raise ArgumentError, "Categorization value must be a string, category #{evaluated_categories[0]} evaluation resulted in a: '#{result[1].class}'" end evaluated_categories end Puppet::Pops::Binder::BindingsFactory::categories(evaluated_categories) end private # Checks that contribution's effective categorization is in the same relative order as in the overall # categorization precedence. # def check_contribution_precedence(contributions) cat_prec = { } @config.categorization.each_with_index {|c, i| cat_prec[ c[0] ] = i } contributions.each() do |contrib| # Contributions that do not specify their opinion about categorization silently accepts the precedence # set in the root configuration - and may thus produce an unexpected result # next unless ec = contrib.effective_categories next unless categories = ec.categories prev_prec = -1 categories.each do |c| prec = cat_prec[c.categorization] issues = Puppet::Pops::Binder::BinderIssues unless prec diagnostics.accept(issues::MISSING_CATEGORY_PRECEDENCE, c, :categorization => c.categorization) next end unless prec > prev_prec diagnostics.accept(issues::PRECEDENCE_MISMATCH_IN_CONTRIBUTION, c, :categorization => c.categorization) end prev_prec = prec end end end def configure_layer(layer_description, scope, diagnostics) name = layer_description['name'] # compute effective set of uris to load (and get rid of any duplicates in the process included_uris = array_of_uris(layer_description['include']) excluded_uris = array_of_uris(layer_description['exclude']) effective_uris = Set.new(expand_included_uris(included_uris)).subtract(Set.new(expand_excluded_uris(excluded_uris))) # Each URI should result in a ContributedBindings effective_uris.collect { |uri| scheme_handlers[uri.scheme].contributed_bindings(uri, scope, self) } end def array_of_uris(descriptions) return [] unless descriptions descriptions = [descriptions] unless descriptions.is_a?(Array) descriptions.collect {|d| URI.parse(d) } end def expand_included_uris(uris) result = [] uris.each do |uri| unless handler = scheme_handlers[uri.scheme] raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'" end result.concat(handler.expand_included(uri, self)) end result end def expand_excluded_uris(uris) result = [] uris.each do |uri| unless handler = scheme_handlers[uri.scheme] raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'" end result.concat(handler.expand_excluded(uri, self)) end result end class SchemeHandlerHelper T = Puppet::Pops::Types::TypeFactory HASH_OF_HANDLER = T.hash_of(T.type_of('Puppetx::Puppet::BindingsSchemeHandler')) def initialize(scope) @scope = scope @cache = nil end def [] (scheme) load_schemes unless @cache @cache[scheme] end def load_schemes @cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_HANDLER, Puppetx::BINDINGS_SCHEMES) || {} end end end