diff --git a/lib/puppet/plugins.rb b/lib/puppet/plugins.rb index 090a294fc..caf1094ad 100644 --- a/lib/puppet/plugins.rb +++ b/lib/puppet/plugins.rb @@ -1,10 +1,9 @@ # The Puppet Plugins module defines extension points where plugins can be configured -# to add or modify puppet's behavior. +# to add or modify puppet's behavior. See the respective classes in this module for more +# details. # # @api public +# @since Puppet 4.0.0 # module Puppet::Plugins -# require 'puppet/plugins/binding_schemes' -# require 'puppet/plugins/syntax_checkers' -# require 'puppet/plugins/configuration' end \ No newline at end of file diff --git a/lib/puppet/plugins/binding_schemes.rb b/lib/puppet/plugins/binding_schemes.rb index 6fc18b6aa..acf0226de 100644 --- a/lib/puppet/plugins/binding_schemes.rb +++ b/lib/puppet/plugins/binding_schemes.rb @@ -1,140 +1,140 @@ require 'puppet/plugins' module Puppet::Plugins::BindingSchemes # The lookup **key** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public - SPI_BINDINGS_SCHEMES = 'puppet::binding::schemes' + BINDINGS_SCHEMES_KEY = 'puppet::binding::schemes' # The lookup **type** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public BINDINGS_SCHEMES_TYPE = 'Puppet::Plugins::BindingSchemes::BindingsSchemeHandler' # BindingsSchemeHandler is a Puppet Extension Point for the purpose of extending Puppet with a # handler of a URI scheme used in the Puppet Bindings / Injector system. # The intended use is to create a class derived from this class and then register it with the # Puppet Binder. # # Creating the Extension Class # ---------------------------- # As an example, a class for getting LDAP data and transforming into bindings based on an LDAP URI scheme (such as RFC 2255, 4516) # may be authored in say a puppet module called 'exampleorg/ldap'. The name of the class should start with `Puppetx::::`, # e.g. 'Puppetx::Exampleorg::Ldap::LdapBindingsSchemeHandler" and - # be located in `lib/puppetx/exampleorg/Ldap/LdapBindingsSchemeHandler.rb`. (These rules are not enforced, but it make the class + # be located in `lib/puppetx/exampleorg/Ldap/LdapBindingsSchemeHandler.rb`. (These rules are not enforced, but it makes the class # both auto-loadable, and guaranteed to not have a name that clashes with some other LdapBindingsSchemeHandler from some other # author/organization. # # The Puppet Binder will auto-load the file when it # has a binding to the class `Puppetx::Exampleorg::Ldap::LdapBindingsSchemeHandler' # The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.: # # @example Defining an LdapBindingsSchemeHandler # module Puppetx::Exampleorg # module Ldap # class LdapBindingsSchemeHandler < Puppetx::Puppetlabs::BindingsSchemeHandler # # implement the methods # end # end # end # # # The expand_included method # -------------------------- - # This method is given a URI (as entred by a user in a bindings configuration) and the handler's first task is to + # This method is given a URI (as entered by a user in a bindings configuration) and the handler's first task is to # perform checking, transformation, and possible expansion into multiple URIs for loading. The result is always an array # of URIs. This method allows users to enter wild-cards, or to represent something symbolic that is transformed into one or # more "real URIs" to load. (It is allowed to change scheme!). # If the "optional" feature is supported, the handler should not include the URI in the result unless it will be able to produce # bindings for the given URI (as an option it may produce an empty set of bindings). # # The expand_excluded method # --------------------------- # This method is given an URI (as entered by the user in a bindings configuration), and it is the handler's second task # to perform checking, transformation, and possible expansion into multiple URIs that should not be loaded. The result is always # an array of URIs. The user may be allowed to enter wild-cards etc. The URIs produced by this method should have the same syntax # as those produced by {#expand_included} since they are excluded by comparison. # # The contributed_bindings method # ------------------------------- # As the last step, the handler is being called once per URI that was included, and not later excluded to produce the # contributed bindings. It is given three arguments, uri (the uri to load), scope (to provide access to the rest of the # environment), and an acceptor (of issues), on which issues can be recorded. # # Reporting Errors/Issues # ----------------------- # Issues are reported by calling the given composer's acceptor, which takes a severity (e.g. `:error`, # `:warning`, or `:ignore`), an {Puppet::Pops::Issues::Issue Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter # SourcePosAdapter} (which describes details about linenumber, position, and length of the problem area). If the scheme is # not based on file, line, pos - nil can be passed. The URI itself can be passed as file. # # @example Reporting an issue # # create an issue with a symbolic name (that can serve as a reference to more details about the problem), # # make the name unique # issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_LDAP_ILLEGAL_URI) { "The URI is not a valid Ldap URI" } # source_pos = nil # # # report it # composer.acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, uri.to_s, source_pos, {})) # # Instead of reporting issues, an exception can be raised. # # @abstract # @api public # class BindingsSchemeHandler # Produces the bindings contributed to the binding system based on the given URI. # @param uri [URI] the URI to load bindings from # @param scope [Puppet::Pops::Parser::Scope] access to scope and the rest of the environment # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Puppet::Pops::Binder::Bindings::ContributedBindings] the bindings to contribute, most conveniently # created by calling {Puppet::Pops::Binder::BindingsFactory.contributed_bindings}. # @api public # def contributed_bindings(uri, scope, composer) raise NotImplementedError, "The BindingsProviderScheme for uri: '#{uri}' must implement 'contributed_bindings'" end # Expands the given URI for the purpose of including the bindings it refers to. The input may contain # wild-cards (if supported by this handler), and it is this methods responsibility to transform such into # real loadable URIs. # # A scheme handler that does not support optionality, or wildcards should simply return the given URI # in an Array. # # @param uri [URI] the uri for which bindings are to be produced. # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Array] the transformed, and possibly expanded set of URIs to include. # @api public # def expand_included(uri, composer) [uri] end # Expands the given URI for the purpose of excluding the bindings it refers to. The input may contain # wild-cards (if supported by this handler), and it is this methods responsibility to transform such into # real loadable URIs (that match those produced by {#expand_included} that should be excluded from the result. # # A scheme handler that does not support optionality, or wildcards should simply return the given URI # in an Array. # # @param uri [URI] the uri for which bindings are to be produced. # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Array] the transformed, and possibly expanded set of URIs to include- # @api public # def expand_excluded(uri, composer) [uri] end # Returns whether the uri is optional or not. A scheme handler does not have to use this method # to determine optionality, but if it supports such a feature, and there is no technical problem in supporting # it this way, it should be done the same (or at least similar) way across all scheme handlers. # # This method interprets a URI ending with `?` or has query that is '?optional' as optional. # # @return [Boolean] whether the uri is an optional reference or not. def is_optional?(uri) (query = uri.query) && query == '' || query == 'optional' end end end diff --git a/lib/puppet/plugins/configuration.rb b/lib/puppet/plugins/configuration.rb index 71d623f88..75bf54b93 100644 --- a/lib/puppet/plugins/configuration.rb +++ b/lib/puppet/plugins/configuration.rb @@ -1,67 +1,67 @@ # Configures the Puppet Plugins, by registering extension points # and default implementations. # # See the respective configured services for more information. # # @api private # require 'puppet/plugins' module Puppet::Plugins::Configuration - # TODO: This should always be true in Puppet 4.0 (the way it is done now does not allow toggling) + # TODO: This should always be true in Puppet 4.0 (the way it is done here does not allow toggling) return unless ::Puppet[:binder] || ::Puppet[:parser] == 'future' require 'puppet/plugins/binding_schemes' require 'puppet/plugins/syntax_checkers' # Extension-points are registered here: # # - If in a Ruby submodule it is best to create it here # - The class does not have to be required; it will be auto required when the binder # needs it. # - If the extension is a multibind, it can be registered here; either with a required # class or a class reference in string form. - checkers_name = Puppet::Plugins::SyntaxCheckers::SPI_SYNTAX_CHECKERS + checkers_name = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_KEY checkers_type = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_TYPE - schemes_name = Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES + schemes_name = Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_KEY schemes_type = Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE # Register extension points # ------------------------- system_bindings = ::Puppet::Pops::Binder::SystemBindings extensions = system_bindings.extensions() extensions.multibind(checkers_name).name(checkers_name).hash_of(checkers_type) extensions.multibind(schemes_name).name(schemes_name).hash_of(schemes_type) # Register injector boot bindings # ------------------------------- boot_bindings = system_bindings.injector_boot_bindings() # Register the default bindings scheme handlers { 'module' => 'ModuleScheme', 'confdir' => 'ConfdirScheme', }.each do |scheme, class_name| boot_bindings \ .bind.name(scheme) \ .instance_of(schemes_type) \ .in_multibind(schemes_name) \ .to_instance("Puppet::Pops::Binder::SchemeHandler::#{class_name}") end # Default extensions delivered in Puppet Core are included here # ------------------------------------------------------------- # Classes in this name-space are lazily loaded as they may be overridden and/or never used # (Lazy loading is done by binding to the name of a class instead of a Class instance). # Register extensions # ------------------- bindings = system_bindings.default_bindings() bindings.bind do name('json') instance_of(checkers_type) in_multibind(checkers_name) to_instance('Puppet::SyntaxCheckers::Json') end end diff --git a/lib/puppet/plugins/syntax_checkers.rb b/lib/puppet/plugins/syntax_checkers.rb index e1b5ee5cb..9ebec86a9 100644 --- a/lib/puppet/plugins/syntax_checkers.rb +++ b/lib/puppet/plugins/syntax_checkers.rb @@ -1,103 +1,103 @@ require 'puppet/plugins' module Puppet::Plugins::SyntaxCheckers # The lookup **key** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public - SPI_SYNTAX_CHECKERS = 'puppet::syntaxcheckers' + SYNTAX_CHECKERS_KEY = 'puppet::syntaxcheckers' # The lookup **type** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public SYNTAX_CHECKERS_TYPE = 'Puppet::Plugins::SyntaxCheckers::SyntaxChecker' # SyntaxChecker is a Puppet Extension Point for the purpose of extending Puppet with syntax checkers. # The intended use is to create a class derived from this class and then register it with the # Puppet Binder. # # Creating the Extension Class # ---------------------------- # As an example, a class for checking custom xml (aware of some custom schemes) may be authored in # say a puppet module called 'exampleorg/xmldata'. The name of the class should start with `Puppetx::::`, # e.g. 'Puppetx::Exampleorg::XmlData::XmlChecker" and # be located in `lib/puppetx/exampleorg/xml_data/xml_checker.rb`. The Puppet Binder will auto-load this file when it # has a binding to the class `Puppetx::Exampleorg::XmlData::XmlChecker' # The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.: # # @example Defining an XmlChecker # module Puppetx::Exampleorg # module XmlData # class XmlChecker < Puppetx::Puppetlabs::SyntaxCheckers::SyntaxChecker # def check(text, syntax_identifier, acceptor, location_hash) # # do the checking # end # end # end # end # # Implementing the check method # ----------------------------- # The implementation of the {#check} method should naturally perform syntax checking of the given text/string and # produce found issues on the given `acceptor`. These can be warnings or errors. The method should return `false` if # any warnings or errors were produced (it is up to the caller to check for error/warning conditions and report them # to the user). # # Issues are reported by calling the given `acceptor`, which takes a severity (e.g. `:error`, # or `:warning), an {Puppet::Pops::Issues::Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter} # (which describes details about linenumber, position, and length of the problem area). Note that the # `location_info` given to the check method holds information about the location of the string in its *container* # (e.g. the source position of a Heredoc); this information can be used if more detailed information is not # available, or combined if there are more details (relative to the start of the checked string). # # @example Reporting an issue # # create an issue with a symbolic name (that can serve as a reference to more details about the problem), # # make the name unique # issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_XMLDATA_ILLEGAL_XML) { "syntax error found in xml text" } # source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new() # source_pos.line = info[:line] # use this if there is no detail from the used parser # source_pos.pos = info[:pos] # use this pos if there is no detail from used parser # # # report it # acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, info[:file], source_pos, {})) # # There is usually a cap on the number of errors/warnings that are presented to the user, this is handled by the # reporting logic, but care should be taken to not generate too many as the issues are kept in memory until # the checker returns. The acceptor may set a limit and simply ignore issues past a certain (high) number of reported # issues (this number is typically higher than the cap on issues reported to the user). # # The `syntax_identifier` # ----------------------- # The extension makes use of a syntax identifier written in mime-style. This identifier can be something simple # as 'xml', or 'json', but can also consist of several segments joined with '+' where the most specific syntax variant # is placed first. When searching for a syntax checker; say for JSON having some special traits, say 'userdata', the # author of the text may indicate this as the text having the syntax "userdata+json" - when a checker is looked up it is # first checked if there is a checker for "userdata+json", if none is found, a lookup is made for "json" (since the text # must at least be valid json). The given identifier is passed to the checker (to allow the same checker to check for # several dialects/specializations). # # Use in Puppet DSL # ----------------- - # The Puppet DSL Heredoc support and Puppet Templates makes use of the syntax checker extension. A user of a + # The Puppet DSL Heredoc support makes use of the syntax checker extension. A user of a # heredoc can specify the syntax in the heredoc tag, e.g.`@(END:userdata+json)`. # # # @abstract # class SyntaxChecker # Checks the text for syntax issues and reports them to the given acceptor. # This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the # method. # # @param text [String] The text to check # @param syntax_identifier [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml' # @option location_info [String] :file The filename where the string originates # @option location_info [Integer] :line The line number identifying the location where the string is being used/checked # @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked # @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not. # @api public # def check(text, syntax_identifier, acceptor, location_info) raise NotImplementedError, "The class #{self.class.name} should have implemented the method check()" end end end diff --git a/lib/puppet/pops/binder/bindings_composer.rb b/lib/puppet/pops/binder/bindings_composer.rb index 815b38443..9cf97bc6f 100644 --- a/lib/puppet/pops/binder/bindings_composer.rb +++ b/lib/puppet/pops/binder/bindings_composer.rb @@ -1,177 +1,177 @@ # 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). # require 'puppet/plugins/binding_schemes' 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 # 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' bind do name(scheme) instance_of(Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE) - in_multibind(Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES) + in_multibind(Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_KEY) 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 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 # 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 private 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(Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE)) 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, Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES) || {} + @cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_HANDLER, Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_KEY) || {} end end end diff --git a/lib/puppet/pops/evaluator/external_syntax_support.rb b/lib/puppet/pops/evaluator/external_syntax_support.rb index 68e7f688f..6f7e47a28 100644 --- a/lib/puppet/pops/evaluator/external_syntax_support.rb +++ b/lib/puppet/pops/evaluator/external_syntax_support.rb @@ -1,52 +1,52 @@ # This module is an integral part of the evaluator. It deals with the concern of validating # external syntax in text produced by heredoc and templates. # require 'puppet/plugins/syntax_checkers' module Puppet::Pops::Evaluator::ExternalSyntaxSupport # TODO: This can be simplified if the Factory directly supported hash_of/type_of TYPES = Puppet::Pops::Types::TypeFactory SERVICE_TYPE = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_TYPE - SERVICE_NAME = Puppet::Plugins::SyntaxCheckers::SPI_SYNTAX_CHECKERS + SERVICE_NAME = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_KEY def assert_external_syntax(scope, result, syntax, reference_expr) @@HASH_OF_SYNTAX_CHECKERS ||= TYPES.hash_of(TYPES.type_of(SERVICE_TYPE)) # ignore 'unspecified syntax' return if syntax.nil? || syntax == '' checker = checker_for_syntax(scope, syntax) # ignore syntax with no matching checker return unless checker # Call checker and give it the location information from the expression # (as opposed to where the heredoc tag is (somewhere on the line above)). acceptor = Puppet::Pops::Validation::Acceptor.new() source_pos = find_closest_positioned(reference_expr) checker.check(result, syntax, acceptor, source_pos) if acceptor.error_count > 0 checker_message = "Invalid produced text having syntax: '#{syntax}'." Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => checker_message) raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end # Finds the most significant checker for the given syntax (most significant is to the right). # Returns nil if there is no registered checker. # def checker_for_syntax(scope, syntax) checkers_hash = scope.compiler.injector.lookup(scope, @@HASH_OF_SYNTAX_CHECKERS, SERVICE_NAME) || {} checkers_hash[lookup_keys_for_syntax(syntax).find {|x| checkers_hash[x] }] end # Returns an array of possible syntax names def lookup_keys_for_syntax(syntax) segments = syntax.split(/\+/) result = [] begin result << segments.join("+") segments.shift end until segments.empty? result end end