diff --git a/lib/puppet/indirector/resource_type/parser.rb b/lib/puppet/indirector/resource_type/parser.rb index 60e72250e..2d5eb040d 100644 --- a/lib/puppet/indirector/resource_type/parser.rb +++ b/lib/puppet/indirector/resource_type/parser.rb @@ -1,106 +1,111 @@ require 'puppet/resource/type' require 'puppet/indirector/code' require 'puppet/indirector/resource_type' # The main terminus for Puppet::Resource::Type # # This exposes the known resource types from within Puppet. Only find # and search are supported. When a request is received, Puppet will # attempt to load all resource types (by parsing manifests and modules) and # returns a description of the resource types found. The format of these # objects is documented at {Puppet::Resource::Type}. # # @api public class Puppet::Indirector::ResourceType::Parser < Puppet::Indirector::Code desc "Return the data-form of a resource type." # Find will return the first resource_type with the given name. It is # not possible to specify the kind of the resource type. # # @param request [Puppet::Indirector::Request] The request object. # The only parameters used from the request are `environment` and # `key`, which corresponds to the resource type's `name` field. # @return [Puppet::Resource::Type, nil] # @api public def find(request) begin # This is a fix in 3.x that will be replaced with the use of a context # (That is not available until 3.5). $squelsh_parse_errors = true - krt = request.environment.known_resource_types + krt = resource_types_in(request.environment) # This is a bit ugly. [:hostclass, :definition, :node].each do |type| # We have to us 'find_' here because it will # load any missing types from disk, whereas the plain # '' method only returns from memory. if r = krt.send("find_#{type}", [""], request.key) return r end end nil ensure $squelsh_parse_errors = false end end # Search for resource types using a regular expression. Unlike `find`, this # allows you to filter the results by the "kind" of the resource type # ("class", "defined_type", or "node"). All three are searched if no # `kind` filter is given. This also accepts the special string "`*`" # to return all resource type objects. # # @param request [Puppet::Indirector::Request] The request object. The # `key` field holds the regular expression used to search, and # `options[:kind]` holds the kind query parameter to filter the # result as described above. The `environment` field specifies the # environment used to load resources. # # @return [Array, nil] # # @api public def search(request) begin # This is a fix in 3.x that will be replaced with the use of a context # (That is not available until 3.5). $squelsh_parse_errors = true - krt = request.environment.known_resource_types + krt = resource_types_in(request.environment) # Make sure we've got all of the types loaded. krt.loader.import_all result_candidates = case request.options[:kind] when "class" krt.hostclasses.values when "defined_type" krt.definitions.values when "node" krt.nodes.values when nil result_candidates = [krt.hostclasses.values, krt.definitions.values, krt.nodes.values] else raise ArgumentError, "Unrecognized kind filter: " + "'#{request.options[:kind]}', expected one " + " of 'class', 'defined_type', or 'node'." end result = result_candidates.flatten.reject { |t| t.name == "" } return nil if result.empty? return result if request.key == "*" # Strip the regex of any wrapping slashes that might exist key = request.key.sub(/^\//, '').sub(/\/$/, '') begin regex = Regexp.new(key) rescue => detail raise ArgumentError, "Invalid regex '#{request.key}': #{detail}" end result.reject! { |t| t.name.to_s !~ regex } return nil if result.empty? result end ensure $squelsh_parse_errors = false end + + def resource_types_in(environment) + environment.check_for_reparse + environment.known_resource_types + end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index ea947fc4e..666219376 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,500 +1,468 @@ require 'puppet/util' require 'puppet/util/cacher' require 'monitor' require 'puppet/parser/parser_factory' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Puppet::Node::Environment acts as a container for all configuration # that is expected to vary between environments. # # ## Global variables # # The Puppet::Node::Environment uses a number of global variables. # # ### `$environment` # # The 'environment' global variable represents the current environment that's # being used in the compiler. # -# ### `$known_resource_types` -# -# The 'known_resource_types' global variable represents a singleton instance -# of the Puppet::Resource::TypeCollection class. The variable is discarded -# and regenerated if it is accessed by an environment that doesn't match the -# environment of the 'known_resource_types' -# -# This behavior of discarding the known_resource_types every time the -# environment changes is not ideal. In the best case this can cause valid data -# to be discarded and reloaded. If Puppet is being used with numerous -# environments then this penalty will be repeatedly incurred. -# -# In the worst case (#15106) demonstrates that if a different environment is -# accessed during catalog compilation, for whatever reason, the -# known_resource_types can be discarded which loses information that cannot -# be recovered and can cause a catalog compilation to completely fail. -# # ## The root environment # # In addition to normal environments that are defined by the user,there is a # special 'root' environment. It is defined as an instance variable on the # Puppet::Node::Environment metaclass. The environment name is `*root*` and can # be accessed by calling {Puppet::Node::Environment.root}. # # The primary purpose of the root environment is to contain parser functions # that are not bound to a specific environment. The main case for this is for # logging functions. Logging functions are attached to the 'root' environment # when {Puppet::Parser::Functions.reset} is called. # # The root environment is also used as a fallback environment when the # current environment has been requested by {Puppet::Node::Environment.current} # requested and no environment was set by {Puppet::Node::Environment.current=} class Puppet::Node::Environment # This defines a mixin for classes that have an environment. It implements # `environment` and `environment=` that respects the semantics of the # Puppet::Node::Environment class # # @api public module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher # @!attribute seen # @scope class # @api private # @return [Hash] All memoized environments @seen = {} # Create a new environment with the given name, or return an existing one # # The environment class memoizes instances so that attempts to instantiate an # environment with the same name with an existing environment will return the # existing environment. # # @overload self.new(environment) # @param environment [Puppet::Node::Environment] # @return [Puppet::Node::Environment] the environment passed as the param, # this is implemented so that a calling class can use strings or # environments interchangeably. # # @overload self.new(string) # @param string [String, Symbol] # @return [Puppet::Node::Environment] An existing environment if it exists, # else a new environment with that name # # @overload self.new() # @return [Puppet::Node::Environment] The environment as set by # Puppet.settings[:environment] # # @api public def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end # Retrieve the environment for the current thread # # @note This should only used when a catalog is being compiled. # # @api private # # @return [Puppet::Node::Environment] the currently set environment if one # has been explicitly set, else it will return the '*root*' environment def self.current $environment || root end # Set the environment for the current thread # # @note This should only set when a catalog is being compiled. Under normal # This value is initially set in {Puppet::Parser::Compiler#environment} # # @note Setting this affects global state during catalog compilation, and # changing the current environment during compilation can cause unexpected # and generally very bad behaviors. # # @api private # # @param env [Puppet::Node::Environment] def self.current=(env) $environment = new(env) end # @return [Puppet::Node::Environment] The `*root*` environment. # # This is only used for handling functions that are not attached to a # specific environment. # # @api private def self.root @root end # Clear all memoized environments and the 'current' environment # # @api private def self.clear @seen.clear $environment = nil end # @!attribute [r] name # @api public # @return [Symbol] the human readable environment name that serves as the # environment identifier attr_reader :name # Return an environment-specific Puppet setting. # # @api public # # @param param [String, Symbol] The environment setting to look up # @return [Object] The resolved setting value def [](param) Puppet.settings.value(param, self.name) end # Instantiate a new environment # # @note {Puppet::Node::Environment.new} is overridden to return memoized # objects, so this will not be invoked with the normal Ruby initialization # semantics. # # @param name [Symbol] The environment name def initialize(name) @name = name end # The current global TypeCollection # # @note The environment is loosely coupled with the {Puppet::Resource::TypeCollection} # class. While there is a 1:1 relationship between an environment and a # TypeCollection instance, there is only one TypeCollection instance # available at any given time. It is stored in `$known_resource_types`. # `$known_resource_types` is accessed as an instance method, but is global # to all environment variables. # # @api public # @return [Puppet::Resource::TypeCollection] The current global TypeCollection def known_resource_types - # This makes use of short circuit evaluation to get the right thread-safe - # per environment semantics with an efficient most common cases; we almost - # always just return our thread's known-resource types. Only at the start - # of a compilation (after our thread var has been set to nil) or when the - # environment has changed or when the known resource types have become stale - # do we delve deeper. - $known_resource_types = nil if $known_resource_types && - ($known_resource_types.environment != self || !@known_resource_types_being_imported && $known_resource_types.stale?) - $known_resource_types ||= - if @known_resource_types.nil? or @known_resource_types.require_reparse? - #set the global variable $known_resource_types immediately as it will be queried - #resursively from the parser which would set it anyway, just executing more code in vain - @known_resource_types = $known_resource_types = Puppet::Resource::TypeCollection.new(self) - - #avoid an infinite recursion (called from the parser) if Puppet[:filetimeout] is set to -1 and - #$known_resource_types.stale? returns always true; let's set a flag that we're importing - #so if this method is called recursively we'll skip testing the stale status - begin - @known_resource_types_being_imported = true - @known_resource_types.import_ast(perform_initial_import, '') - ensure - @known_resource_types_being_imported = false - end - @known_resource_types - else - @known_resource_types - end + if @known_resource_types.nil? + @known_resource_types = Puppet::Resource::TypeCollection.new(self) + @known_resource_types.import_ast(perform_initial_import(), '') + end + @known_resource_types end # Yields each modules' plugin directory if the plugin directory (modulename/lib) # is present on the filesystem. # # @yield [String] Yields the plugin directory from each module to the block. # @api public def each_plugin_directory(&block) modules.map(&:plugin_directory).each do |lib| lib = Puppet::Util::Autoload.cleanpath(lib) yield lib if File.directory?(lib) end end # Locate a module instance by the module name alone. # # @api public # # @param name [String] The module name # @return [Puppet::Module, nil] The module if found, else nil def module(name) modules.find {|mod| mod.name == name} end # Locate a module instance by the full forge name (EG authorname/module) # # @api public # # @param forge_name [String] The module name # @return [Puppet::Module, nil] The module if found, else nil def module_by_forge_name(forge_name) author, modname = forge_name.split('/') found_mod = self.module(modname) found_mod and found_mod.forge_name == forge_name ? found_mod : nil end # @!attribute [r] modulepath # Return all existent directories in the modulepath for this environment # @note This value is cached so that the filesystem doesn't have to be # re-enumerated every time this method is invoked, since that # enumeration could be a costly operation and this method is called # frequently. The cache expiry is determined by `Puppet[:filetimeout]`. # @see Puppet::Util::Cacher.cached_attr # @api public # @return [Array] All directories present in the modulepath cached_attr(:modulepath, Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # @!attribute [r] modules # Return all modules for this environment in the order they appear in the # modulepath. # @note If multiple modules with the same name are present they will # both be added, but methods like {#module} and {#module_by_forge_name} # will return the first matching entry in this list. # @note This value is cached so that the filesystem doesn't have to be # re-enumerated every time this method is invoked, since that # enumeration could be a costly operation and this method is called # frequently. The cache expiry is determined by `Puppet[:filetimeout]`. # @see Puppet::Util::Cacher.cached_attr # @api public # @return [Array] All modules for this environment cached_attr(:modules, Puppet[:filetimeout]) do module_references = [] seen_modules = {} modulepath.each do |path| Dir.entries(path).each do |name| warn_about_mistaken_path(path, name) next if module_references.include?(name) if not seen_modules[name] module_references << {:name => name, :path => File.join(path, name)} seen_modules[name] = true end end end module_references.collect do |reference| begin Puppet::Module.new(reference[:name], reference[:path], self) rescue Puppet::Module::Error nil end end.compact end # Generate a warning if the given directory in a module path entry is named `lib`. # # @api private # # @param path [String] The module directory containing the given directory # @param name [String] The directory name def warn_about_mistaken_path(path, name) if name == "lib" Puppet.debug("Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless " + "you are expecting to load a module named 'lib', your module path may be set " + "incorrectly.") end end # Modules broken out by directory in the modulepath # # @note This method _changes_ the current working directory while enumerating # the modules. This seems rather dangerous. # # @api public # # @return [Hash>] A hash whose keys are file # paths, and whose values is an array of Puppet Modules for that path def modules_by_path modules_by_path = {} modulepath.each do |path| Dir.chdir(path) do module_names = Dir.glob('*').select do |d| FileTest.directory?(d) && (File.basename(d) =~ /\A\w+(-\w+)*\Z/) end modules_by_path[path] = module_names.sort.map do |name| Puppet::Module.new(name, File.join(path, name), self) end end end modules_by_path end # All module requirements for all modules in the environment modulepath # # @api public # # @comment This has nothing to do with an environment. It seems like it was # stuffed into the first convenient class that vaguely involved modules. # # @example # environment.module_requirements # # => { # # 'username/amodule' => [ # # { # # 'name' => 'username/moduledep', # # 'version' => '1.2.3', # # 'version_requirement' => '>= 1.0.0', # # }, # # { # # 'name' => 'username/anotherdep', # # 'version' => '4.5.6', # # 'version_requirement' => '>= 3.0.0', # # } # # ] # # } # # # # @return [Hash>>] See the method example # for an explanation of the return value. def module_requirements deps = {} modules.each do |mod| next unless mod.forge_name deps[mod.forge_name] ||= [] mod.dependencies and mod.dependencies.each do |mod_dep| deps[mod_dep['name']] ||= [] dep_details = { 'name' => mod.forge_name, 'version' => mod.version, 'version_requirement' => mod_dep['version_requirement'] } deps[mod_dep['name']] << dep_details end end deps.each do |mod, mod_deps| deps[mod] = mod_deps.sort_by {|d| d['name']} end deps end + + def check_for_reparse + if @known_resource_types && @known_resource_types.require_reparse? + @known_resource_types = nil + end + end + # @return [String] The stringified value of the `name` instance variable # @api public def to_s name.to_s end # @return [Symbol] The `name` value, cast to a string, then cast to a symbol. # # @api public # # @note the `name` instance variable is a Symbol, but this casts the value # to a String and then converts it back into a Symbol which will needlessly # create an object that needs to be garbage collected def to_sym to_s.to_sym end # Return only the environment name when serializing. # # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. # # @api public def to_zaml(z) self.to_s.to_zaml(z) end # Validate a list of file paths and return the paths that are directories on the filesystem # # @api private # # @param dirs [Array] The file paths to validate # @return [Array] All file paths that exist and are directories def validate_dirs(dirs) dirs.collect do |dir| File.expand_path(dir) end.find_all do |p| FileTest.directory?(p) end end private # Reparse the manifests for the given environment # # There are two sources that can be used for the initial parse: # # 1. The value of `Puppet.settings[:code]`: Puppet can take a string from # its settings and parse that as a manifest. This is used by various # Puppet applications to read in a manifest and pass it to the # environment as a side effect. This is attempted first. # 2. The contents of `Puppet.settings[:manifest]`: Puppet will try to load # the environment manifest. By default this is `$manifestdir/site.pp` # # @note This method will return an empty hostclass if # `Puppet.settings[:ignoreimport]` is set to true. # # @return [Puppet::Parser::AST::Hostclass] The AST hostclass object # representing the 'main' hostclass def perform_initial_import return empty_parse_result if Puppet.settings[:ignoreimport] # parser = Puppet::Parser::Parser.new(self) parser = Puppet::Parser::ParserFactory.parser(self) if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, name.to_s) parser.file = file end parser.parse rescue => detail - known_resource_types.parse_failed = true + @known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end # Return an empty toplevel hostclass to indicate that no file was loaded # # This is used as the return value of {#perform_initial_import} when # `Puppet.settings[:ignoreimport]` is true. # # @return [Puppet::Parser::AST::Hostclass] def empty_parse_result return Puppet::Parser::AST::Hostclass.new('') end @root = new(:'*root*') end diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index 2ea34c1de..9d6bb0f6f 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,558 +1,557 @@ require 'forwardable' require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/util/errors' require 'puppet/resource/type_collection_helper' # Maintain a graph of scopes, along with a bunch of data # about the individual catalog we're compiling. class Puppet::Parser::Compiler extend Forwardable include Puppet::Util include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Resource::TypeCollectionHelper def self.compile(node) - $known_resource_types = nil $env_module_directories = nil + node.environment.check_for_reparse - # ...and we actually do the compile now we have caching ready. new(node).compile.to_resource rescue => detail message = "#{detail} on node #{node.name}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end attr_reader :node, :facts, :collections, :catalog, :resources, :relationships, :topscope # The injector that provides lookup services, or nil if accessed before the compiler has started compiling and # bootstrapped. The injector is initialized and available before any manifests are evaluated. # # @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services for this compiler/environment # @api public # attr_accessor :injector # The injector that provides lookup services during the creation of the {#injector}. # @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services during injector creation # for this compiler/environment # # @api private # attr_accessor :boot_injector # Add a collection to the global list. def_delegator :@collections, :<<, :add_collection def_delegator :@relationships, :<<, :add_relationship # Store a resource override. def add_override(override) # If possible, merge the override in immediately. if resource = @catalog.resource(override.ref) resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end def add_resource(scope, resource) @resources << resource # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) if not resource.class? and resource[:stage] raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" end # Stages should not be inside of classes. They are always a # top-level container, regardless of where they appear in the # manifest. return if resource.stage? # This adds a resource to the class it lexically appears in in the # manifest. unless resource.class? return @catalog.add_edge(scope.resource, resource) end end # Do we use nodes found in the code, vs. the external node sources? def_delegator :known_resource_types, :nodes?, :ast_nodes? # Store the fact that we've evaluated a class def add_class(name) @catalog.add_class(name) unless name == "" end # Return a list of all of the defined classes. def_delegator :@catalog, :classes, :classlist # Compiler our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile # Set the client's parameters into the top scope. Puppet::Util::Profiler.profile("Compile: Set node parameters") { set_node_parameters } Puppet::Util::Profiler.profile("Compile: Created settings scope") { create_settings_scope } if is_binder_active? Puppet::Util::Profiler.profile("Compile: Created injector") { create_injector } end Puppet::Util::Profiler.profile("Compile: Evaluated main") { evaluate_main } Puppet::Util::Profiler.profile("Compile: Evaluated AST node") { evaluate_ast_node } Puppet::Util::Profiler.profile("Compile: Evaluated node classes") { evaluate_node_classes } Puppet::Util::Profiler.profile("Compile: Evaluated generators") { evaluate_generators } Puppet::Util::Profiler.profile("Compile: Finished catalog") { finish } fail_on_unevaluated @catalog end def_delegator :@collections, :delete, :delete_collection # Return the node's environment. def environment unless defined?(@environment) unless node.environment.is_a? Puppet::Node::Environment raise Puppet::DevError, "node #{node} has an invalid environment!" end @environment = node.environment end Puppet::Node::Environment.current = @environment @environment end # Evaluate all of the classes specified by the node. # Classes with parameters are evaluated as if they were declared. # Classes without parameters or with an empty set of parameters are evaluated # as if they were included. This means classes with an empty set of # parameters won't conflict even if the class has already been included. def evaluate_node_classes if @node.classes.is_a? Hash classes_with_params, classes_without_params = @node.classes.partition {|name,params| params and !params.empty?} # The results from Hash#partition are arrays of pairs rather than hashes, # so we have to convert to the forms evaluate_classes expects (Hash, and # Array of class names) classes_with_params = Hash[classes_with_params] classes_without_params.map!(&:first) else classes_with_params = {} classes_without_params = @node.classes end evaluate_classes(classes_without_params, @node_scope || topscope) evaluate_classes(classes_with_params, @node_scope || topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, raise an error. This method really just creates resource objects # that point back to the classes, and then the resources are themselves # evaluated later in the process. # # Sometimes we evaluate classes with a fully qualified name already, in which # case, we tell scope.find_hostclass we've pre-qualified the name so it # doesn't need to search its namespaces again. This gets around a weird # edge case of duplicate class names, one at top scope and one nested in our # namespace and the wrong one (or both!) getting selected. See ticket #13349 # for more detail. --jeffweiss 26 apr 2012 def evaluate_classes(classes, scope, lazy_evaluate = true, fqname = false) raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source class_parameters = nil # if we are a param class, save the classes hash # and transform classes to be the keys if classes.class == Hash class_parameters = classes classes = classes.keys end classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.find_hostclass(name, :assume_fqname => fqname) # If parameters are passed, then attempt to create a duplicate resource # so the appropriate error is thrown. if class_parameters resource = klass.ensure_in_catalog(scope, class_parameters[name] || {}) else next if scope.class_scope(klass) resource = klass.ensure_in_catalog(scope) end # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate else raise Puppet::Error, "Could not find class #{name} for #{node.name}" end end end def evaluate_relationships @relationships.each { |rel| rel.evaluate(catalog) } end # Return a resource by either its ref or its type and title. def_delegator :@catalog, :resource, :findresource def initialize(node, options = {}) @node = node set_options(options) initvars end # Create a new scope, with either a specified parent scope or # using the top scope. def newscope(parent, options = {}) parent ||= topscope scope = Puppet::Parser::Scope.new(self, options) scope.parent = parent scope end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end def injector create_injector if @injector.nil? @injector end def boot_injector create_boot_injector(nil) if @boot_injector.nil? @boot_injector end # Creates the boot injector from registered system, default, and injector config. # @return [Puppet::Pops::Binder::Injector] the created boot injector # @api private Cannot be 'private' since it is called from the BindingsComposer. # def create_boot_injector(env_boot_bindings) assert_binder_active() boot_contribution = Puppet::Pops::Binder::SystemBindings.injector_boot_contribution(env_boot_bindings) final_contribution = Puppet::Pops::Binder::SystemBindings.final_contribution binder = Puppet::Pops::Binder::Binder.new() binder.define_categories(boot_contribution.effective_categories) binder.define_layers(Puppet::Pops::Binder::BindingsFactory.layered_bindings(final_contribution, boot_contribution)) @boot_injector = Puppet::Pops::Binder::Injector.new(binder) end # Answers if Puppet Binder should be active or not, and if it should and is not active, then it is activated. # @return [Boolean] true if the Puppet Binder should be activated def is_binder_active? should_be_active = Puppet[:binder] || Puppet[:parser] == 'future' if should_be_active # TODO: this should be in a central place, not just for ParserFactory anymore... Puppet::Parser::ParserFactory.assert_rgen_installed() @@binder_loaded ||= false unless @@binder_loaded require 'puppet/pops' require 'puppetx' @@binder_loaded = true end end should_be_active end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| break if astnode = known_resource_types.node(name.to_s.downcase) end unless (astnode ||= known_resource_types.node("default")) raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'" end # Create a resource to model this node, and then add it to the list # of resources. resource = astnode.ensure_in_catalog(topscope) resource.evaluate @node_scope = topscope.class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? exceptwrap do # We have to iterate over a dup of the array because # collections can delete themselves from the list, which # changes its length and causes some collections to get missed. Puppet::Util::Profiler.profile("Evaluated collections") do found_something = false @collections.dup.each do |collection| found_something = true if collection.evaluate end found_something end end end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do Puppet::Util::Profiler.profile("Evaluated definitions") do !unevaluated_resources.each do |resource| Puppet::Util::Profiler.profile("Evaluated resource #{resource}") do resource.evaluate end end.empty? end end end # Iterate over collections and resources until we're sure that the whole # compile is evaluated. This is necessary because both collections # and defined resources can generate new resources, which themselves could # be defined resources. def evaluate_generators count = 0 loop do done = true Puppet::Util::Profiler.profile("Iterated (#{count + 1}) on generators") do # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions end break if done count += 1 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog" end end end # Find and evaluate our main object, if possible. def evaluate_main @main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, "")) @topscope.source = @main @main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource add_resource(@topscope, @main_resource) @main_resource.evaluate end # Make sure the entire catalog is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = @resource_overrides.values.flatten.collect(&:ref) if !remaining.empty? fail Puppet::ParseError, "Could not find resource(s) #{remaining.join(', ')} for overriding" end end # Make sure we don't have any remaining collections that specifically # look for resources, because we want to consider those to be # parse errors. def fail_on_unevaluated_resource_collections remaining = @collections.collect(&:resources).flatten.compact if !remaining.empty? raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}" end end # Make sure all of our resources and such have done any last work # necessary. def finish evaluate_relationships resources.each do |resource| # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| resource.merge(over) end # Remove the overrides, so that the configuration knows there # are none left. overrides.clear end resource.finish if resource.respond_to?(:finish) end add_resource_metaparams end def add_resource_metaparams unless main = catalog.resource(:class, :main) raise "Couldn't find main" end names = Puppet::Type.metaparams.select do |name| !Puppet::Parser::Resource.relationship_parameter?(name) end data = {} catalog.walk(main, :out) do |source, target| if source_data = data[source] || metaparams_as_data(source, names) # only store anything in the data hash if we've actually got # data data[source] ||= source_data source_data.each do |param, value| target[param] = value if target[param].nil? end data[target] = source_data.merge(metaparams_as_data(target, names)) end target.tag(*(source.tags)) end end def metaparams_as_data(resource, params) data = nil params.each do |param| unless resource[param].nil? # Because we could be creating a hash for every resource, # and we actually probably don't often have any data here at all, # we're optimizing a bit by only creating a hash if there's # any data to put in it. data ||= {} data[param] = resource[param] end end data end # Set up all of our internal variables. def initvars # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] # The list of relationships to evaluate. @relationships = [] # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Resource::Catalog.new(@node.name) @catalog.version = known_resource_types.version @catalog.environment = @node.environment.to_s # Create our initial scope and a resource that will evaluate main. @topscope = Puppet::Parser::Scope.new(self) @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @topscope)) # local resource array to maintain resource ordering @resources = [] # Make sure any external node classes are in our class list if @node.classes.class == Hash @catalog.add_class(*@node.classes.keys) else @catalog.add_class(*@node.classes) end end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope[param.to_s] = value end # These might be nil. catalog.client_version = node.parameters["clientversion"] catalog.server_version = node.parameters["serverversion"] if Puppet[:trusted_node_data] @topscope.set_trusted(node.trusted_data) end end def create_settings_scope unless settings_type = environment.known_resource_types.hostclass("settings") settings_type = Puppet::Resource::Type.new :hostclass, "settings" environment.known_resource_types.add(settings_type) end settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope) @catalog.add_resource(settings_resource) settings_type.evaluate_code(settings_resource) scope = @topscope.class_scope(settings_type) Puppet.settings.each do |name, setting| next if name.to_s == "name" scope[name.to_s] = environment[name] end end # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources # The order of these is significant for speed due to short-circuting resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? } end # Creates the injector from bindings found in the current environment. # @return [void] # @api private # def create_injector assert_binder_active() composer = Puppet::Pops::Binder::BindingsComposer.new() layered_bindings = composer.compose(topscope) binder = Puppet::Pops::Binder::Binder.new() binder.define_categories(composer.effective_categories(topscope)) binder.define_layers(layered_bindings) @injector = Puppet::Pops::Binder::Injector.new(binder) end def assert_binder_active unless is_binder_active? raise ArgumentError, "The Puppet Binder is only available when either '--binder true' or '--parser future' is used" end end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index be8ae1a94..576e44312 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,455 +1,447 @@ #! /usr/bin/env ruby require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' require 'puppet_spec/modules' require 'puppet/parser/parser_factory' describe Puppet::Node::Environment do let(:env) { Puppet::Node::Environment.new("testing") } include PuppetSpec::Files after do Puppet::Node::Environment.clear end shared_examples_for 'the environment' do it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet[:environment] = "one" Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) - $known_resource_types = nil end it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection env.known_resource_types.stubs(:stale?).returns true env.known_resource_types.should equal(@collection) end - it "should return the current thread associated collection if there is one" do - $known_resource_types = @collection - - env.known_resource_types.should equal(@collection) - end - it "should generate a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true - $known_resource_types = nil - new_type_collection = env.known_resource_types + env.check_for_reparse + + new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) Puppet[:modulepath] = path env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do before :each do @path_one = tmpdir("path_one") @path_two = tmpdir("path_one") sep = File::PATH_SEPARATOR Puppet[:modulepath] = "#{@path_one}#{sep}#{@path_two}" end it "should not return non-directories" do FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false env.validate_dirs([@path_one, @path_two]).should == [@path_one] end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true two = File.expand_path("two") env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.set_value(:server, "myval", :testing) env[:server].should == "myval" end it "should be able to return an individual module that exists in its module path" do env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] mod = env.module('one') mod.should be_a(Puppet::Module) mod.name.should == 'one' end it "should not return a module if the module doesn't exist" do env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] env.module('two').should be_nil end it "should return nil if asked for a module that does not exist in its path" do modpath = tmpdir('modpath') env.modulepath = [modpath] env.module("one").should be_nil end describe "module data" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end describe "#modules_by_path" do it "should return an empty list if there are no modules" do env.modules_by_path.should == { @first => [], @second => [] } end it "should include modules even if they exist in multiple dirs in the modulepath" do modpath1 = File.join(@first, "foo") FileUtils.mkdir_p(modpath1) modpath2 = File.join(@second, "foo") FileUtils.mkdir_p(modpath2) env.modules_by_path.should == { @first => [Puppet::Module.new('foo', modpath1, env)], @second => [Puppet::Module.new('foo', modpath2, env)] } end it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) FileUtils.mkdir_p(File.join(@first, 'foo-bar')) FileUtils.mkdir_p(File.join(@first, 'foo_bar')) FileUtils.mkdir_p(File.join(@first, 'foo=bar')) FileUtils.mkdir_p(File.join(@first, 'foo bar')) FileUtils.mkdir_p(File.join(@first, 'foo.bar')) FileUtils.mkdir_p(File.join(@first, '-foo')) FileUtils.mkdir_p(File.join(@first, 'foo-')) FileUtils.mkdir_p(File.join(@first, 'foo--bar')) env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end end describe "#module_requirements" do it "should return a list of what modules depend on other modules" do PuppetSpec::Modules.create( 'foo', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }] } ) PuppetSpec::Modules.create( 'bar', @second, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }] } ) PuppetSpec::Modules.create( 'baz', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "3.0.0" }] } ) PuppetSpec::Modules.create( 'alpha', @first, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) env.module_requirements.should == { 'puppetlabs/alpha' => [], 'puppetlabs/foo' => [ { "name" => "puppetlabs/bar", "version" => "9.9.9", "version_requirement" => "<= 2.0.0" } ], 'puppetlabs/bar' => [ { "name" => "puppetlabs/alpha", "version" => "9.9.9", "version_requirement" => "~3.0.0" }, { "name" => "puppetlabs/baz", "version" => "9.9.9", "version_requirement" => "3.0.0" }, { "name" => "puppetlabs/foo", "version" => "9.9.9", "version_requirement" => ">= 1.0.0" } ], 'puppetlabs/baz' => [] } end end describe ".module_by_forge_name" do it "should find modules by forge_name" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'puppetlabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == mod end it "should not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', @first, :metadata => {:author => 'sneakylabs'}, :environment => env ) env.module_by_forge_name('puppetlabs/baz').should == nil end it "should return nil when the module can't be found" do env.module_by_forge_name('ima/nothere').should be_nil end end describe ".modules" do it "should return an empty list if there are no modules" do env.modules.should == [] end it "should return a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| FileUtils.mkdir_p(File.join(@first, mod_name)) end %w{bee baz}.each do |mod_name| FileUtils.mkdir_p(File.join(@second, mod_name)) end env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@second, 'foo')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) FileUtils.mkdir_p(File.join(@first, 'foo-bar')) FileUtils.mkdir_p(File.join(@first, 'foo_bar')) FileUtils.mkdir_p(File.join(@first, 'foo=bar')) FileUtils.mkdir_p(File.join(@first, 'foo bar')) env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end it "should create modules with the correct environment" do FileUtils.mkdir_p(File.join(@first, 'foo')) env.modules.each {|mod| mod.environment.should == env } end end end it "should cache the module list" do env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do @helper.environment = 'foo' @helper.environment.name.should == :foo end end describe "when performing initial import" do before do @parser = Puppet::Parser::ParserFactory.parser("test") # @parser = Puppet::Parser::EParserAdapter.new(Puppet::Parser::Parser.new("test")) # TODO: FIX PARSER FACTORY Puppet::Parser::ParserFactory.stubs(:parser).returns @parser end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do Puppet::FileSystem::File.stubs(:exist?).returns true - env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError - lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) + lambda { env.known_resource_types }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") - env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) - lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) + lambda { env.known_resource_types }.should raise_error(Puppet::Error, /Syntax error at .../) env.known_resource_types.require_reparse?.should be_true end end end describe 'with classic parser' do before :each do Puppet[:parser] = 'current' end it_behaves_like 'the environment' end describe 'with future parser' do before :each do Puppet[:parser] = 'future' end it_behaves_like 'the environment' end end