diff --git a/lib/puppet/pops/loader/dependency_loader.rb b/lib/puppet/pops/loader/dependency_loader.rb index 0cc1df67f..3b0a0f259 100644 --- a/lib/puppet/pops/loader/dependency_loader.rb +++ b/lib/puppet/pops/loader/dependency_loader.rb @@ -1,60 +1,60 @@ # =DependencyLoader # This loader provides visibility into a set of other loaders. It is used as a child of a ModuleLoader (or other # loader) to make its direct dependencies visible for loading from contexts that have access to this dependency loader. # Access is typically given to logic that resides inside of the module, but not to those that just depend on the module. # # It is instantiated with a name, and with a set of dependency_loaders. # # @api private # class Puppet::Pops::Loader::DependencyLoader < Puppet::Pops::Loader::BaseLoader # An index of module_name to module loader used to speed up lookup of qualified names attr_reader :index # Creates a DependencyLoader for one parent loader # - # @param parent_loader [Puppet::Pops::Loader] - typically a module loader for the root - # @param name [String] - the name of the dependency-loader (used for debugging and tracing only) - # @param depedency_loaders [Array] - array of loaders for modules this module depends on + # @param parent_loader [Puppet::Pops::Loader] typically a module loader for the root + # @param name [String] the name of the dependency-loader (used for debugging and tracing only) + # @param depedency_loaders [Array] array of loaders for modules this module depends on # def initialize(parent_loader, name, dependency_loaders) super parent_loader, name @dependency_loaders = dependency_loaders end # Finds name in a loader this loader depends on / can see # def find(typed_name) if typed_name.qualified if loader = index()[typed_name.name_parts[0]] loader.load_typed(typed_name) else # no module entered as dependency with name matching first segment of wanted name nil end else # a non name-spaced name, have to search since it can be anywhere. # (Note: superclass caches the result in this loader as it would have to repeat this search for every # lookup otherwise). loaded = @dependency_loaders.reduce(nil) do |previous, loader| break previous if !previous.nil? loader.load_typed(typed_name) end if loaded promote_entry(loaded) end loaded end end def to_s() "(DependencyLoader '#{@name}' [" + @dependency_loaders.map {|loader| loader.to_s }.join(' ,') + "])" end private def index() @index ||= @dependency_loaders.reduce({}) { |index, loader| index[loader.module_name] = loader; index } end end diff --git a/lib/puppet/pops/loader/module_loader_configurator.rb b/lib/puppet/pops/loader/module_loader_configurator.rb deleted file mode 100644 index 879e5f118..000000000 --- a/lib/puppet/pops/loader/module_loader_configurator.rb +++ /dev/null @@ -1,289 +0,0 @@ -require 'puppet/pops/impl/loader/uri_helper' -require 'delegator' - -# A ModuleLoaderConfigurator is responsible for configuring module loaders given a module path -# NOTE: Exploratory code (not yet fully featured/functional) showing how a configurator loads and configures -# ModuleLoaders for a given set of modules on a given module path. -# -# ==Usage -# Create an instance and for each wanted entry call one of the methods #add_all_modules, -# or #add_module. A call must be made to #add_root (once only) to define where the root is. -# -# The next step is to produce loaders for the modules. This involves resolving the modules dependencies -# to know what is visible to each module (and later to create a real loader). This can be a quite heavy -# operation and there may be many more modules available than what will actually be used. -# The creation of loaders is therefore performed lazily. -# -# A call to #create_loaders sets up lazy loaders for all modules and creates the real loader for -# the root. -# -class ModuleLoaderConfigurator - include Puppet::Pops::Impl::Loader::UriHelper - - def initialize(modulepath) - @root_module = nil - # maps module name to list of ModuleData (different versions of module) - @module_name_to_data = {} - @modules = [] # list in search path order - end - - # =ModuleData - # Reference to a module's data - # TODO: should have reference to real model element containing all module data; this is faking it - # - class ModuleData - attr_accessor :name, :version, :state, :loader, :path, :module_element, :resolutions - def initialize name, version, path - @name = name - @version = version - @path = path - @state = :unresolved - @module_element = nil # should be a model element describing the module - @resolutions = [] - @loader = nil - end - def requirements - nil # FAKE: this says "wants to see everything" - end - def is_resolved? - @state == :resolved - end - end - - # Produces module loaders for all modules and returns the loader for the root. - # All other module loaders are parented by this loader. The root loader is parented by - # the given parent_loader. - # - def create_loaders(parent_loader) - # TODO: If no root was configured - what to do? Fail? Or just run with a bunch of modules? - # Configure a null module? - # Create a lazy loader first, all the other modules needs to refer to something, and - # the real module loader needs to know about the other loaders when created. - @root_module.loader = SimpleDelegator.new(LazyLoader.new(parent_loader, @root_module, self)) - @modules.each { |md| md.loader = SimpleDelegator.new(LazyLoader.new(@root_module.loader, md, self)) } - - # Since we can be almost certain that the root loader will be used, resolve it and - # replace with a real loader. Also, since the root module does not have a name, it can not - # use the optimizing scheme in LazyLoader where the loader stays unresolved until a name in the - # module's namespace is actually requested. - @root_module.loader = @root_module.loader.create_real_loader - end - - # Lazy loader is used via a Delegator. When invoked to do some real work, it checks - # if the requested name is for this module or not - such a request can never come from within - # logic in the module itself (since that would have required it to be resolved in the first place). - # - # TODO: must handle file based as well as gem based module when creating the real module. - # - class LazyLoader - def initialize(parent, module_data, configurator) - @module_data = module_data - @parent = parent - @configurator = configurator - @miss_cache = Set.new() - - # TODO: Should check wich non namespaced paths exists within the module, there is not need to - # check if non-namespaced entities exist if their respective root does not exist (check once instead of each - # request. This is a join of non-namespaced types and their paths and the existing paths. - # Later when a request is made and a check is needed, the available paths should be given to the - # PathBasedInstantiatorConfig (horrible name) to get the actual paths (if any). - # Alternative approach - since modules typically have very few functions and types (typically 0 - dozen) - # the paths can be obtained once - although this somewhat defeates the purpose of loading quickly since if there - # are hundreds of modules, there will be 2x that number of stats to see if the respective directories exist. - # The best combination is to do nothing on startup. When the first request is made, the check for the corresponding - # directory is made, and the answer is remembered. - - @smart_paths = {} - end - - def [](typed_name) - # Since nothing has ever been loaded, this can be answered truthfully. - nil - end - - def load(typed_name) - matching_name?(typed_name) ? create_real_loader.load(typed_name) : nil - end - - def find(name, executor) - # Calls should always go to #load first, and since that always triggers the - # replacement and delegation to the real thing, this should never happen. - raise "Should never have been called" - end - - def parent - # This can be answered truthfully without resolving the loader. - @parent - end - - def matching_name?(typed_name) - segments = typed_name.name_parts - (segments.size > 1 && @module_data.name == segments[0]) || non_namespace_name_exists?(segments[0]) - end - - def non_namespace_name_exists?(typed_name) - type = typed_name.type - case type - when :function - when :resource_type - else - return false - end - - unless effective_paths = @smart_paths[type] - # Don't know yet, does the various directories for the type exist ? - # Get the relative dirs for the type - paths_for_type = PathBasedInstantiatorConfig.relative_paths_for_type(type) - root_path = @module_data.path - # Check which directories exist and update them with the root if the they do - effective_paths = @smart_paths[type_name.type] = paths_for_type.collect do |sp| - FileSystem.directory?(File.join(root_path, sp.relative_path)) - end.each {|sp| sp.root_path = root_path } - end - # if there are no directories for this type... - return false if effective_paths.empty? - - # have we looked before ? - name = typed_name.name - return false if @miss_cache.include?(name) - - # Does it have the name? - if effective_paths.find {|sp| FileSystem.exists?(sp.absolute_path(name, 0)) } - true - else - @miss_cache.add(name) - false - end - end - - # Creates the real ModuleLoader, updates the Delegator handed out to other loaders earlier - # TODO: The smart paths and miss cache are valid and can be transfered to the real loader - it will need - # to repeat the setup and checks otherwise. - # - def create_real_loader - md = @module_data - @configurator.resolve_module md - loaders_for_resolved = md.resolutions.collect { |m| m.loader } - real = ModuleLoader.new(parent, md.name, md.path, loaders_for_resolved) - md.loader.__setobj__(real) - real - end - end - - # Path should refer to a directory where there are sub-directories for 'manifests', and - # other loadable things under puppet/type, puppet/function/... - # This does *not* add any modules found under this root. - # - def add_root path - data= ModuleData.new('', :unversioned, path) - @root_module = data - end - - # Path should refer to a directory of 'modules', where each directory is named after the module it contains. - # - def add_all_modules path - path = path_for_uri(path, '') - raise "Path to modules is not a directory" unless File.directory? path - # Process content of directory in sorted order to make it deterministic - # (Needed because types and functions are not in a namespace in Puppet; thus order matters) - # - Dir[file_name + '/*'].sort.each do |f| - next unless File.directory? f - add_module File.basename(f), f - end - end - - # Path should refer to the root directory of a module. Allows a gem:, file: or nil scheme (file). - # The path may be a URI or a String. - # - def add_module name, path - # Allows specification of gem or file - path = path_for_uri(path, '') - - # TODO: - # Is there a modulefile.json - # Load it, and get the metadata - # Describe the module, its dependencies etc. - # - - # If there is no Modulefile, or modulefile.json to load - it is still a module - # But its version and dependencies are not known. Create a model for it - # Mark it as "depending on everything in the configuration - - # Beware of circular dependencies; they may require special loading ? - - # Beware of same module appearing more than once (different versions ok, same version, or no - # version is not). - - # Remember the data - # Fake :unversioned etc. - data = ModuleData.new(name, :unversioned, path) - @modules << data # list in order module paths are added - if entries = @module_name_to_data[name] - entries << data - else - @module_name_to_data[name] = [data] - end - end - - def validate - # Scan the remembered modules/versions and check if there are duplicate versions - # and what not... - # TODO: Decide when module validity is determined; tradeoff between startup performance and when - # errors are detected - - # Validate - # - Refers to itself, or different version of itself - # - Metadata and path (name) are not in sync - # - end - - def resolve_all - @module_name_to_data.each { |k, v| v.each { |m| resolve_module m } } - end - - # Resolves a module by looking up all of its requirements and picking the best version - # matching the requirements, alternatively if requirement is "everything", pick the first found - # version of each module by name in path order. - # - def resolve_module md - # list of first added (on module path) by module name - @first_of_each ||= @modules.collect {|m| m.name }.uniq.collect {|k| @module_name_to_data[k][0] } - - # # Alternative Collect latest, modules in order they were found on path - # - # @modules.collect {|m| m.name }.uniq.collect do |k| - # v = theResolver.best_match(">=0", @module_name_to_data[name].collect {|x| x.version}) - # md.resolutions << @module_name_to_data[k].find {|x| x.version == v } - # end - - unless md.is_resolved? - if reqs = md.requirements - reqs.each do |r| - # TODO: This is pseudo code - will fail if used - name = r.name - version_requirements = r.version_requirements - # Ask a (now fictitious) resolver to compute the best matching version - v = theResolver.best_match(version_requirements, @module_name_to_data[name].collect {|x| x.version }) - if v - md.resolutions << @module_name_to_data[name].find {|x| x.version == v } - else - raise "TODO: Unresolved" - end - end - else - # nil requirements means "wants to see all" - # Possible solutions: - # - pick the latest version of each named module if there is more than one version - # - pick the first found module on the path (this is probably what Puppet 3x does) - - # Resolutions are all modules (except the current) - md.resolutions += @first_of_each.reject { |m| m == md } - end - md.status = :resolved - end - end - - def configure_loaders - end -end diff --git a/lib/puppet/pops/loader/module_loaders.rb b/lib/puppet/pops/loader/module_loaders.rb index 57d618862..fb54df70e 100644 --- a/lib/puppet/pops/loader/module_loaders.rb +++ b/lib/puppet/pops/loader/module_loaders.rb @@ -1,228 +1,228 @@ # =ModuleLoaders # A ModuleLoader loads items from a single module. # The ModuleLoaders (ruby) module contains various such loaders. There is currently one concrete # implementation, ModuleLoaders::FileBased that loads content from the file system. # Other implementations can be created - if they are based on name to path mapping where the path # is relative to a root path, they can derive the base behavior from the ModuleLoaders::AbstractPathBasedModuleLoader class. # # Examples of such extensions could be a zip/jar/compressed file base loader. # # Notably, a ModuleLoader does not configure itself - it is given the information it needs (the root, its name etc.) # Logic higher up in the loader hierarchy of things makes decisions based on the "shape of modules", and "available # modules" to determine which module loader to use for each individual module. (There could be differences in # internal layout etc.) # # A module loader is also not aware of the mapping of name to relative paths - this is performed by the # included module Puppet::Pops::Loader::PathBasedInstantatorConfig which knows about the map from type/name to # relative path, and the logic that can instantiate what is expected to be found in the content of that path. # # @api private # module Puppet::Pops::Loader::ModuleLoaders class AbstractPathBasedModuleLoader < Puppet::Pops::Loader::BaseLoader # The name of the module, or nil, if this is a global "component" attr_reader :module_name # The path to the location of the module/component - semantics determined by subclass attr_reader :path # A map of type to smart-paths that help with minimizing the number of paths to scan attr_reader :smart_paths # Initialize a kind of ModuleLoader for one module # @param parent_loader [Puppet::Pops::Loader] loader with higher priority # @param module_name [String] the name of the module (non qualified name), may be nil for a global "component" # @param path [String] the path to the root of the module (semantics defined by subclass) # @param loader_name [String] a name that is used for human identification (useful when module_name is nil) # def initialize(parent_loader, module_name, path, loader_name) super parent_loader, loader_name # Irrespective of the path referencing a directory or file, the path must exist. unless Puppet::FileSystem.exist?(path) raise ArgumentError, "The given path '#{path}' does not exist!" end @module_name = module_name @path = path @smart_paths = Puppet::Pops::Loader::LoaderPaths::SmartPaths.new(self) end # Finds typed/named entity in this module # @param typed_name [Puppet::Pops::Loader::TypedName] the type/name to find # @return [Puppet::Pops::Loader::Loader::NamedEntry, nil found/created entry, or nil if not found # def find(typed_name) # Assume it is a global name, and that all parts of the name should be used when looking up name_part_index = 0 name_parts = typed_name.name_parts # Certain types and names can be disqualified up front if name_parts.size > 1 # The name is in a name space. # Then entity cannot possible be in this module unless the name starts with the module name. # Note: If "module" represents a "global component", the module_name is nil and cannot match which is # ok since such a "module" cannot have namespaced content). # return nil unless name_parts[0] == module_name # Skip the first part of the name when computing the path since the path already contains the name of the # module name_part_index = 1 else # The name is in the global name space. # The only globally name-spaced elements that may be loaded from modules are functions and resource types case typed_name.type when :function when :resource_type else # anything else cannot possibly be in this module # TODO: should not be allowed anyway... may have to revisit this decision return nil end end # Get the paths that actually exist in this module (they are lazily processed once and cached). # The result is an array (that may be empty). # Find the file to instantiate, and instantiate the entity if file is found origin = nil if (smart_path = smart_paths.effective_paths(typed_name.type).find do |sp| origin = sp.effective_path(typed_name, name_part_index) existing_path(origin) end) value = smart_path.instantiator.create(self, typed_name, origin, get_contents(origin)) # cache the entry and return it set_entry(typed_name, value, origin) else nil end end # Abstract method that subclasses override that checks if it is meaningful to search using a generic smart path. # This optimization is performed to not be tricked into searching an empty directory over and over again. # The implementation may perform a deep search for file content other than directories and cache this in # and index. It is guaranteed that a call to meaningful_to_search? takes place before checking any other # path with relative_path_exists?. # # This optimization exists because many modules have been created from a template and they have # empty directories for functions, types, etc. (It is also the place to create a cached index of the content). # # @param relative_path [String] a path relative to the module's root # @return [Boolean] true if there is content in the directory appointed by the relative path # def meaningful_to_search?(smart_path) raise NotImplementedError.new end # Abstract method that subclasses override to answer if the given relative path exists, and if so returns that path # # @param relative_path [String] a path resolved by a smart path against the loader's root (if it has one) # @return [Boolean] true if the file exists # def existing_path(resolved_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce the content of the effective path. # It should either succeed and return a String or fail with an exception. # # @param relative_path [String] a path as resolved by a smart path # @return [String] the content of the file # def get_contents(effective_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce a source reference String used to identify the # system resource (resource in the URI sense). # # @param relative_path [String] a path relative to the module's root # @return [String] a reference to the source file (in file system, zip file, or elsewhere). # def get_source_ref(relative_path) raise NotImplementedError.new end end # @api private # class FileBased < AbstractPathBasedModuleLoader attr_reader :smart_paths attr_reader :path_index - # Create a kind of ModuleLoader for one module - # The parameters are: - # * parent_loader - typically the loader for the root - # * module_name - the name of the module (non qualified name) - # * path - the path to the root of the module (semantics defined by subclass) + # Create a kind of ModuleLoader for one module (Puppet Module, or module like) + # + # @param parent_loader [Puppet::Pops::Loader::Loader] typically the loader for the environment or root + # @param module_name [String] the name of the module (non qualified name), may be nil for "modules" only containing globals + # @param path [String] the path to the root of the module (semantics defined by subclass) + # @param loader_name [String] a name that identifies the loader # def initialize(parent_loader, module_name, path, loader_name) super unless Puppet::FileSystem.directory?(path) - raise ArgumentError, "The given module root path '#{path}' is not a directory (required for filesystem based module path entry)" + raise ArgumentError, "The given module root path '#{path}' is not a directory (required for file system based module path entry)" end @path_index = Set.new() end def existing_path(effective_path) # Optimized, checks index instead of visiting file system @path_index.include?(effective_path) ? effective_path : nil end def meaningful_to_search?(smart_path) ! add_to_index(smart_path).empty? end def to_s() "(ModuleLoader::FileBased '#{loader_name()}' '#{module_name()}')" end def add_to_index(smart_path) found = Dir.glob(File.join(smart_path.generic_path, '**', "*#{smart_path.extension}")) @path_index.merge(found) found end def get_contents(effective_path) Puppet::FileSystem.read(effective_path) end end # Loads from a gem specified as a URI, gem://gemname/optional/path/in/gem, or just a String gemname. # The source reference (shown in errors etc.) is the expanded path of the gem as this is believed to be more # helpful - given the location it should be quite obvious which gem it is, without the location, the user would # need to go on a hunt for where the file actually is located. # # TODO: How does this get instantiated? Does the gemname refelect the name of the module (the namespace) # or is that specified a different way? Can a gem be the container of multiple modules? # # @api private # class GemBased < FileBased include Puppet::Pops::Loader::GemSupport attr_reader :gem_ref # Create a kind of ModuleLoader for one module # The parameters are: # * parent_loader - typically the loader for the root # * module_name - the name of the module (non qualified name) # * gem_ref - [URI, String] gem reference to the root of the module (URI, gem://gemname/optional/path/in/gem), or # just the gem's name as a String. # def initialize(parent_loader, module_name, gem_ref, loader_name) @gem_ref = gem_ref super parent_loader, module_name, gem_dir(gem_ref), loader_name end def to_s() "(ModuleLoader::GemBased '#{loader_name()}' '#{@gem_ref}' [#{module_name()}])" end end - -end \ No newline at end of file +end diff --git a/lib/puppet/pops/loaders.rb b/lib/puppet/pops/loaders.rb index 7e09f59be..8a59d9740 100644 --- a/lib/puppet/pops/loaders.rb +++ b/lib/puppet/pops/loaders.rb @@ -1,102 +1,225 @@ class Puppet::Pops::Loaders + class LoaderError < Puppet::Error; end attr_reader :static_loader attr_reader :puppet_system_loader attr_reader :environment_loader def initialize() # The static loader can only be changed after a reboot @@static_loader ||= Puppet::Pops::Loader::StaticLoader.new() # Create the set of loaders # 1. Puppet, loads from the "running" puppet - i.e. bundled functions, types, extension points and extensions # Does not change without rebooting the service running puppet. # @@puppet_system_loader ||= create_puppet_system_loader() # 2. Environment loader - i.e. what is bound across the environment, may change for each setup # TODO: loaders need to work when also running in an agent doing catalog application. There is no # concept of environment the same way as when running as a master (except when doing apply). # The creation mechanisms should probably differ between the two. # @environment_loader = create_environment_loader() # 3. module loaders are set up from the create_environment_loader, they register themselves end # Clears the cached static and puppet_system loaders (to enable testing) # def self.clear @@static_loader = nil @@puppet_system_loader = nil end def static_loader @@static_loader end def puppet_system_loader @@puppet_system_loader end def self.create_loaders() self.new() end private def create_puppet_system_loader() module_name = nil loader_name = 'puppet_system' # Puppet system may be installed in a fixed location via RPM, installed as a Gem, via source etc. # The only way to find this across the different ways puppet can be installed is # to search up the path from this source file's __FILE__ location until it finds the parent of # lib/puppet... e.g.. dirname(__FILE__)/../../.. (i.e. /lib/puppet/pops/loaders.rb). # puppet_lib = File.join(File.dirname(__FILE__), '../../..') Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, module_name, puppet_lib, loader_name) end def create_environment_loader() # This defines where to start parsing/evaluating - the "initial import" (to use 3x terminology) # Is either a reference to a single .pp file, or a directory of manifests. If the environment becomes # a module and can hold functions, types etc. then these are available across all other modules without # them declaring this dependency - it is however valuable to be able to treat it the same way # bindings and other such system related configuration. # This is further complicated by the many options available: # - The environment may not have a directory, the code comes from one appointed 'manifest' (site.pp) # - The environment may have a directory and also point to a 'manifest' # - The code to run may be set in settings (code) # Further complication is that there is nothing specifying what the visibility is into # available modules. (3x is everyone sees everything). # Puppet binder currently reads confdir/bindings - that is bad, it should be using the new environment support. current_environment = Puppet.lookup(:current_environment) # The environment is not a namespace, so give it a nil "module_name" module_name = nil loader_name = "environment:#{current_environment.name}" env_dir = Puppet[:environmentdir] if env_dir.nil? loader = Puppet::Pops::Loader::NullLoader.new(puppet_system_loader, loader_name) else envdir_path = File.join(env_dir, current_environment.name.to_s) # TODO: Representing Environment as a Module - needs something different (not all types are supported), # and it must be able to import .pp code from 3x manifest setting, or from code setting as well as from # a manifests directory under the environment's root. The below is cheating... # loader = Puppet::Pops::Loader::ModuleLoaders::FileBased(puppet_system_loader, module_name, envdir_path, loader_name) end # An environment has a module path even if it has a null loader - configure_loaders_for_modulepath(loader, current_environment.modulepath) + configure_loaders_for_modules(loader, current_environment) loader end - def configure_loaders_for_modulepath(loader, modulepath) - # TODO: For each module on the modulepath, create a lazy loader - # TODO: Register the module's external and internal loaders (the loader for the module itself, and the loader - # for its dependencies. + def configure_loaders_for_modules(loader, current_environment) + @module_resolver = mr = ModuleResolver.new() + + current_environment.modules.each do |puppet_module| + # Create data about this module + md = new LoaderModuleData(puppet_module) + mr[puppet_module.name] = md + md.loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, md_name, md.path, md.name) + end + end + + def public_loader_for_module(module_name) + md = @module_resolver[module_name] || (return nil) + # Note, this loader is not resolved until it is asked to load something it may contain + md.public_loader + end + + def private_loader_for_module(module_name) + md = @module_resolver[module_name] || (return nil) + unless md.resolved? + @module_resolver.resolve(md) + end + md.private_loader + end + + # =LoaderModuleData + # Information about a Module and its loaders. + # TODO: should have reference to real model element containing all module data; this is faking it + # TODO: Should use Puppet::Module to get the metadata (as a hash) - a somewhat blunt instrument, but that is + # what is available with a reasonable API. + # + class LoaderModuleData + + attr_accessor :state + attr_accessor :public_loader + attr_accessor :private_loader + attr_accessor :resolutions + + # The Puppet::Module this LoaderModuleData represents in the loader configuration + attr_reader :puppet_module + + # @param puppet_module [Puppet::Module] the module instance for the module being represented + # + def initialize(puppet_module) + @state = :initial + @puppet_module = puppet_module + @resolutions = [] + @loader = nil + @private_loader = nil + end + + def name + @puppet_module.name + end + + def version + @puppet_module.version + end + + def path + @puppet_module.path + end + + def requirements + nil # FAKE: this says "wants to see everything" + end + + def resolved? + @state == :resolved + end + end + + # Resolves module loaders - resolution of model dependencies is done by Puppet::Module + # + class ModuleResolver + + def initialize() + @index = {} + @all_module_loaders = nil + end + + def [](name) + @index[name] + end + + def []=(name, module_data) + @index[name] = module_data + end + + def all_module_loaders + @all_module_loaders ||= @index.map {|md| md.loader } + end + + def resolve(module_data) + return if module_data.resolved? + pm = module_data.puppet_module + # Resolution rules + # If dependencies.nil? means "see all other modules" (This to make older modules work, and modules w/o metadata) + # TODO: Control via flag/feature ? + module_data.private_loader = + if pm.dependencies.nil? + # see everything + if Puppet::Util::Log.level == :debug + Puppet.debug("ModuleLoader: module '#{module_data.name}' has unknown dependencies - it will have all other modules visible") + end + + Puppet::Pops::Loader::DependencyLoader.new(module_data.loader, module_data.name, all_module_loaders()) + else + # If module has resolutions they must resolve - it will not see into other modules otherwise + # TODO: possible give errors if there are unresolved references + # i.e. !pm.unmet_dependencies.empty? (if module lacks metadata it is considered to have met all). + # The face "module" can display error information. + # Here, we are just giving up without explaining - the user can check with the module face (or console) + # + unless pm.unmet_dependencies.empty? + # TODO: Exception or just warning? + Puppet.warning("ModuleLoader: module '#{module_data.name}' has unresolved dependencies"+ + " - it will only see those that are resolved."+ + " Use 'puppet module list --tree' to see information about modules") + # raise Puppet::Pops::Loader::Loader::Error, "Loader Error: Module '#{module_data.name}' has unresolved dependencies - use 'puppet module list --tree' to see information" + end + dependency_loaders = pm.dependencies_as_modules.map { |dep| @index[dep.name].loader } + Puppet::Pops::Loader::DependencyLoader.new(module_data.loader, module_data.name, dependency_loaders) + end + + end end end \ No newline at end of file