diff --git a/lib/puppet/environments.rb b/lib/puppet/environments.rb index 13739579a..8c804a092 100644 --- a/lib/puppet/environments.rb +++ b/lib/puppet/environments.rb @@ -1,390 +1,389 @@ # @api private module Puppet::Environments class EnvironmentNotFound < Puppet::Error def initialize(environment_name, original = nil) environmentpath = Puppet[:environmentpath] super("Could not find a directory environment named '#{environment_name}' anywhere in the path: #{environmentpath}. Does the directory exist?", original) end end # @api private module EnvironmentCreator # Create an anonymous environment. # # @param module_path [String] A list of module directories separated by the # PATH_SEPARATOR # @param manifest [String] The path to the manifest # @return A new environment with the `name` `:anonymous` # # @api private def for(module_path, manifest) Puppet::Node::Environment.create(:anonymous, module_path.split(File::PATH_SEPARATOR), manifest) end end # Provide any common methods that loaders should have. It requires that any # classes that include this module implement get # @api private module EnvironmentLoader # @!macro loader_get_or_fail def get!(name) environment = get(name) if environment environment else raise EnvironmentNotFound, name end end end # @!macro [new] loader_search_paths # A list of indicators of where the loader is getting its environments from. # @return [Array] The URIs of the load locations # # @!macro [new] loader_list # @return [Array] All of the environments known # to the loader # # @!macro [new] loader_get # Find a named environment # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment, nil] the requested environment or nil # if it wasn't found # # @!macro [new] loader_get_conf # Attempt to obtain the initial configuration for the environment. Not all # loaders can provide this. # # @param name [String,Symbol] The name of the environment whose configuration # we are looking up # @return [Puppet::Setting::EnvironmentConf, nil] the configuration for the # requested environment, or nil if not found or no configuration is available # # @!macro [new] loader_get_or_fail # Find a named environment or raise # Puppet::Environments::EnvironmentNotFound when the named environment is # does not exist. # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment] the requested environment # A source of pre-defined environments. # # @api private class Static include EnvironmentCreator include EnvironmentLoader def initialize(*environments) @environments = environments end # @!macro loader_search_paths def search_paths ["data:text/plain,internal"] end # @!macro loader_list def list @environments end # @!macro loader_get def get(name) @environments.find do |env| env.name == name.intern end end # Returns a basic environment configuration object tied to the environment's # implementation values. Will not interpolate. # # @!macro loader_get_conf def get_conf(name) env = get(name) if env Puppet::Settings::EnvironmentConf.static_for(env) else nil end end end # A source of unlisted pre-defined environments. # # Used only for internal bootstrapping environments which are not relevant # to an end user (such as the fall back 'configured' environment). # # @api private class StaticPrivate < Static # Unlisted # # @!macro loader_list def list [] end end # Reads environments from a directory on disk. Each environment is # represented as a sub-directory. The environment's manifest setting is the # `manifest` directory of the environment directory. The environment's # modulepath setting is the global modulepath (from the `[master]` section # for the master) prepended with the `modules` directory of the environment # directory. # # @api private class Directories include EnvironmentLoader def initialize(environment_dir, global_module_path) @environment_dir = environment_dir @global_module_path = global_module_path end # Generate an array of directory loaders from a path string. # @param path [String] path to environment directories # @param global_module_path [Array] the global modulepath setting # @return [Array] An array # of configured directory loaders. def self.from_path(path, global_module_path) environments = path.split(File::PATH_SEPARATOR) environments.map do |dir| Puppet::Environments::Directories.new(dir, global_module_path) end end # @!macro loader_search_paths def search_paths ["file://#{@environment_dir}"] end # @!macro loader_list def list valid_directories.collect do |envdir| name = Puppet::FileSystem.basename_string(envdir).intern setting_values = Puppet.settings.values(name, Puppet.settings.preferred_run_mode) env = Puppet::Node::Environment.create( name, Puppet::Node::Environment.split_path(setting_values.interpolate(:modulepath)), setting_values.interpolate(:manifest), setting_values.interpolate(:config_version) ) - env.watching = false env end end # @!macro loader_get def get(name) list.find { |env| env.name == name.intern } end # @!macro loader_get_conf def get_conf(name) valid_directories.each do |envdir| envname = Puppet::FileSystem.basename_string(envdir) if envname == name.to_s return Puppet::Settings::EnvironmentConf.load_from(envdir, @global_module_path) end end nil end private def valid_directories if Puppet::FileSystem.directory?(@environment_dir) Puppet::FileSystem.children(@environment_dir).select do |child| name = Puppet::FileSystem.basename_string(child) Puppet::FileSystem.directory?(child) && Puppet::Node::Environment.valid_name?(name) end else [] end end end # Combine together multiple loaders to act as one. # @api private class Combined include EnvironmentLoader def initialize(*loaders) @loaders = loaders end # @!macro loader_search_paths def search_paths @loaders.collect(&:search_paths).flatten end # @!macro loader_list def list @loaders.collect(&:list).flatten end # @!macro loader_get def get(name) @loaders.each do |loader| if env = loader.get(name) return env end end nil end # @!macro loader_get_conf def get_conf(name) @loaders.each do |loader| if conf = loader.get_conf(name) return conf end end nil end end class Cached include EnvironmentLoader class DefaultCacheExpirationService def created(env) end def expired?(env_name) false end def evicted(env_name) end end def self.cache_expiration_service=(service) @cache_expiration_service = service end def self.cache_expiration_service @cache_expiration_service || DefaultCacheExpirationService.new end def initialize(loader) @loader = loader @cache = {} @cache_expiration_service = Puppet::Environments::Cached.cache_expiration_service end # @!macro loader_list def list @loader.list end # @!macro loader_search_paths def search_paths @loader.search_paths end # @!macro loader_get def get(name) evict_if_expired(name) if result = @cache[name] return result.value elsif (result = @loader.get(name)) @cache[name] = entry(result) result end end # Clears the cache of the environment with the given name. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear(name) @cache.delete(name) end # Clears all cached environments. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear_all() @cache = {} end # This implementation evicts the cache, and always gets the current # configuration of the environment # # TODO: While this is wasteful since it # needs to go on a search for the conf, it is too disruptive to optimize # this. # # @!macro loader_get_conf def get_conf(name) evict_if_expired(name) @loader.get_conf(name) end # Creates a suitable cache entry given the time to live for one environment # def entry(env) @cache_expiration_service.created(env) ttl = (conf = get_conf(env.name)) ? conf.environment_timeout : Puppet.settings.value(:environment_timeout) Puppet.debug("Caching environment '#{env.name}' (cache ttl: #{ttl})") case ttl when 0 NotCachedEntry.new(env) # Entry that is always expired (avoids syscall to get time) when Float::INFINITY Entry.new(env) # Entry that never expires (avoids syscall to get time) else TTLEntry.new(env, ttl) end end # Evicts the entry if it has expired # Also clears caches in Settings that may prevent the entry from being updated def evict_if_expired(name) if (result = @cache[name]) && (result.expired? || @cache_expiration_service.expired?(name)) Puppet.debug("Evicting cache entry for environment '#{name}'") @cache.delete(name) @cache_expiration_service.evicted(name) Puppet.settings.clear_environment_settings(name) end end # Never evicting entry class Entry attr_reader :value def initialize(value) @value = value end def expired? false end end # Always evicting entry class NotCachedEntry < Entry def expired? true end end # Time to Live eviction policy entry class TTLEntry < Entry def initialize(value, ttl_seconds) super value @ttl = Time.now + ttl_seconds end def expired? Time.now > @ttl end end end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index bae205df4..957b1a21b 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,552 +1,524 @@ 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. # # ## 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 looking up the `:root_environment` using {Puppet.lookup}. # # 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. class Puppet::Node::Environment include Puppet::Util::Cacher NO_MANIFEST = :no_manifest # The create() factory method should be used instead. # # @api private def self.new(*args) create(*args) end private_class_method :new # Create a new environment with the given name # # @param name [Symbol] the name of the environment # @param modulepath [Array] the list of paths from which to load modules # @param manifest [String] the path to the manifest for the environment or # the constant Puppet::Node::Environment::NO_MANIFEST if there is none. # @param config_version [String] path to a script whose output will be added # to report logs (optional) # @return [Puppet::Node::Environment] # # @api public def self.create(name, modulepath, manifest = NO_MANIFEST, config_version = nil) obj = self.allocate obj.send(:initialize, name.intern, expand_dirs(extralibs() + modulepath), manifest == NO_MANIFEST ? manifest : File.expand_path(manifest), config_version) obj end # A "reference" to a remote environment. The created environment instance # isn't expected to exist on the local system, but is instead a reference to # environment information on a remote system. For instance when a catalog is # being applied, this will be used on the agent. # # @note This does not provide access to the information of the remote # environment's modules, manifest, or anything else. It is simply a value # object to pass around and use as an environment. # # @param name [Symbol] The name of the remote environment # def self.remote(name) create(name, [], NO_MANIFEST) end # Instantiate a new environment # # @note {Puppet::Node::Environment.new} is private for historical reasons, as # previously it had been overridden to return memoized objects and was # replaced with {Puppet::Node::Environment.create}, so this will not be # invoked with the normal Ruby initialization semantics. # # @param name [Symbol] The environment name def initialize(name, modulepath, manifest, config_version) @name = name @modulepath = modulepath @manifest = manifest @config_version = config_version - # set watching to true for legacy environments - the directory based environment loaders will set this to - # false for directory based environments after the environment has been created. - @watching = true - end - - # Returns if files are being watched or not. - # @api private - # - def watching? - @watching - end - - # Turns watching of files on or off - # @param flag [TrueClass, FalseClass] if files should be watched or not - # @ api private - def watching=(flag) - @watching = flag end # Creates a new Puppet::Node::Environment instance, overriding any of the passed # parameters. # # @param env_params [Hash<{Symbol => String,Array}>] new environment # parameters (:modulepath, :manifest, :config_version) # @return [Puppet::Node::Environment] def override_with(env_params) return self.class.create(name, env_params[:modulepath] || modulepath, env_params[:manifest] || manifest, env_params[:config_version] || config_version) end # Creates a new Puppet::Node::Environment instance, overriding :manifest, # :modulepath, or :config_version from the passed settings if they were # originally set from the commandline, or returns self if there is nothing to # override. # # @param settings [Puppet::Settings] an initialized puppet settings instance # @return [Puppet::Node::Environment] new overridden environment or self if # there are no commandline changes from settings. def override_from_commandline(settings) overrides = {} if settings.set_by_cli?(:modulepath) overrides[:modulepath] = self.class.split_path(settings.value(:modulepath)) end if settings.set_by_cli?(:config_version) overrides[:config_version] = settings.value(:config_version) end if settings.set_by_cli?(:manifest) overrides[:manifest] = settings.value(:manifest) end overrides.empty? ? self : self.override_with(overrides) end # Retrieve the environment for the current process. # # @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 Puppet.deprecation_warning("Puppet::Node::Environment.current has been replaced by Puppet.lookup(:current_environment), see http://links.puppetlabs.com/current-env-deprecation") Puppet.lookup(:current_environment) end # @param [String] name Environment name to check for valid syntax. # @return [Boolean] true if name is valid # @api public def self.valid_name?(name) !!name.match(/\A\w+\Z/) end # @!attribute [r] name # @api public # @return [Symbol] the human readable environment name that serves as the # environment identifier attr_reader :name # @api public # @return [Array] All directories present on disk in the modulepath def modulepath @modulepath.find_all do |p| Puppet::FileSystem.directory?(p) end end # @api public # @return [Array] All directories in the modulepath (even if they are not present on disk) def full_modulepath @modulepath end # @!attribute [r] manifest # @api public # @return [String] path to the manifest file or directory. attr_reader :manifest # @!attribute [r] config_version # @api public # @return [String] path to a script whose output will be added to report logs # (optional) attr_reader :config_version # Checks to make sure that this environment did not have a manifest set in # its original environment.conf if Puppet is configured with # +disable_per_environment_manifest+ set true. If it did, the environment's # modules may not function as intended by the original authors, and we may # seek to halt a puppet compilation for a node in this environment. # # The only exception to this would be if the environment.conf manifest is an exact, # uninterpolated match for the current +default_manifest+ setting. # # @return [Boolean] true if using directory environments, and # Puppet[:disable_per_environment_manifest] is true, and this environment's # original environment.conf had a manifest setting that is not the # Puppet[:default_manifest]. # @api private def conflicting_manifest_settings? return false if !Puppet[:disable_per_environment_manifest] environment_conf = Puppet.lookup(:environments).get_conf(name) original_manifest = environment_conf.raw_setting(:manifest) !original_manifest.nil? && !original_manifest.empty? && original_manifest != Puppet[:default_manifest] end # Checks the environment and settings for any conflicts # @return [Array] an array of validation errors # @api public def validation_errors errors = [] if conflicting_manifest_settings? errors << "The 'disable_per_environment_manifest' setting is true, and the '#{name}' environment has an environment.conf manifest that conflicts with the 'default_manifest' setting." end errors end # 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 # @api public # @return [Puppet::Resource::TypeCollection] The current global TypeCollection def known_resource_types 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] 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| next if name == "." || name == ".." warn_about_mistaken_path(path, 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 => e Puppet.log_exception(e) 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| dep_name = mod_dep['name'].tr('-', '/') (deps[dep_name] ||= []) << { 'name' => mod.forge_name, 'version' => mod.version, 'version_requirement' => mod_dep['version_requirement'] } end end deps.each do |mod, mod_deps| deps[mod] = mod_deps.sort_by { |d| d['name'] } end deps end - # Set a periodic watcher on the file, so we can tell if it has changed. - # If watching has been turned off, this call has no effect. - # @param file[File,String] File instance or filename - # @api private - def watch_file(file) - if watching? - known_resource_types.watch_file(file.to_s) - end - end - # Checks if a reparse is required (cache of files is stale). - # This call does nothing unless files are being watched. # def check_for_reparse - if (Puppet[:code] != @parsed_code) || (watching? && @known_resource_types && @known_resource_types.require_reparse?) + if (Puppet[:code] != @parsed_code || @known_resource_types.require_reparse?) @parsed_code = nil @known_resource_types = nil end end # @return [String] The YAML interpretation of the object # Return the name of the environment as a string interpretation of the object def to_yaml to_s.to_yaml 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 def self.split_path(path_string) path_string.split(File::PATH_SEPARATOR) end def ==(other) return true if other.kind_of?(Puppet::Node::Environment) && self.name == other.name && self.full_modulepath == other.full_modulepath && self.manifest == other.manifest end alias eql? == def hash [self.class, name, full_modulepath, manifest].hash end private def self.extralibs() if ENV["PUPPETLIB"] split_path(ENV["PUPPETLIB"]) else [] end end def self.expand_dirs(dirs) dirs.collect do |dir| File.expand_path(dir) end end # Reparse the manifests for the given environment # # There are two sources that can be used for the initial parse: # # 1. The value of `Puppet[: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 this environment's +manifest+ attribute: Puppet will # try to load the environment manifest. # # @note This method will return an empty hostclass if # `Puppet[: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[:ignoreimport] - parser = Puppet::Parser::ParserFactory.parser(self) + parser = Puppet::Parser::ParserFactory.parser @parsed_code = Puppet[:code] if @parsed_code != "" parser.string = @parsed_code parser.parse else file = self.manifest # if the manifest file is a reference to a directory, parse and combine # all .pp files in that directory if file == NO_MANIFEST empty_parse_result elsif File.directory?(file) parse_results = Puppet::FileSystem::PathPattern.absolute(File.join(file, '**/*.pp')).glob.sort.map do | file_to_parse | parser.file = file_to_parse parser.parse end # Use a parser type specific merger to concatenate the results Puppet::Parser::AST::Hostclass.new('', :code => Puppet::Parser::ParserFactory.code_merger.concatenate(parse_results)) else parser.file = file parser.parse end end rescue => detail @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 # A special "null" environment # # This environment should be used when there is no specific environment in # effect. NONE = create(:none, []) end diff --git a/lib/puppet/parser/e4_parser_adapter.rb b/lib/puppet/parser/e4_parser_adapter.rb index e74903751..373741df9 100644 --- a/lib/puppet/parser/e4_parser_adapter.rb +++ b/lib/puppet/parser/e4_parser_adapter.rb @@ -1,72 +1,60 @@ require 'puppet/pops' module Puppet; module Parser; end; end; # Adapts an egrammar/eparser to respond to the public API of the classic parser # and makes use of the new evaluator. # class Puppet::Parser::E4ParserAdapter - # Empty adapter fulfills watch_file contract without doing anything. - # @api private - class NullFileWatcher - def watch_file(file) - #nop - end - end - - # @param file_watcher [#watch_file] something that can watch a file - def initialize(file_watcher = nil) - @file_watcher = file_watcher || NullFileWatcher.new + def initialize @file = '' @string = '' @use = :unspecified @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser.new() end def file=(file) @file = file @use = :file - # watch if possible, but only if the file is something worth watching - @file_watcher.watch_file(file) if !file.nil? && file != '' end def parse(string = nil) self.string= string if string parse_result = if @use == :string # Parse with a source_file to set in created AST objects (it was either given, or it may be unknown # if caller did not set a file and the present a string. # @@evaluating_parser.parse_string(@string, @file || "unknown-source-location") else @@evaluating_parser.parse_file(@file) end # the parse_result may be # * empty / nil (no input) # * a Model::Program # * a Model::Expression # model = parse_result.nil? ? nil : parse_result.current args = {} Puppet::Pops::Model::AstTransformer.new(@file).merge_location(args, model) ast_code = if model.is_a? Puppet::Pops::Model::Program Puppet::Parser::AST::PopsBridge::Program.new(model, args) else args[:value] = model Puppet::Parser::AST::PopsBridge::Expression.new(args) end # Create the "main" class for the content - this content will get merged with all other "main" content Puppet::Parser::AST::Hostclass.new('', :code => ast_code) end def string=(string) @string = string @use = :string end end diff --git a/lib/puppet/parser/parser_factory.rb b/lib/puppet/parser/parser_factory.rb index 9f4adce08..1cb819757 100644 --- a/lib/puppet/parser/parser_factory.rb +++ b/lib/puppet/parser/parser_factory.rb @@ -1,60 +1,60 @@ module Puppet; end module Puppet::Parser # The ParserFactory makes selection of parser possible. # Currently, it is possible to switch between two different parsers: # * classic_parser, the parser in 3.1 # * eparser, the Expression Based Parser # class ParserFactory # Produces a parser instance for the given environment - def self.parser(environment) - evaluating_parser(environment) + def self.parser + evaluating_parser end # Creates an instance of an E4ParserAdapter that adapts an # EvaluatingParser to the 3x way of parsing. # - def self.evaluating_parser(file_watcher) + def self.evaluating_parser # Since RGen is optional, test that it is installed assert_rgen_installed() unless defined?(Puppet::Pops::Parser::E4ParserAdapter) require 'puppet/parser/e4_parser_adapter' require 'puppet/pops/parser/code_merger' end - E4ParserAdapter.new(file_watcher) + E4ParserAdapter.new end # Asserts that RGen >= 0.6.6 is installed by checking that certain behavior is available. # Note that this assert is expensive as it also requires puppet/pops (if not already loaded). # def self.assert_rgen_installed @@asserted ||= false return if @@asserted @@asserted = true begin require 'rgen/metamodel_builder' rescue LoadError raise Puppet::DevError.new("The gem 'rgen' version >= 0.7.0 is required when using the setting '--parser future'. Please install 'rgen'.") end # Since RGen is optional, there is nothing specifying its version. # It is not installed in any controlled way, so not possible to use gems to check (it may be installed some other way). # Instead check that "eContainer, and eContainingFeature" has been installed. require 'puppet/pops' begin litstring = Puppet::Pops::Model::LiteralString.new(); container = Puppet::Pops::Model::ArithmeticExpression.new(); container.left_expr = litstring raise "no eContainer" if litstring.eContainer() != container raise "no eContainingFeature" if litstring.eContainingFeature() != :left_expr rescue => e # TODO: RGen can raise exceptions for other reasons! raise Puppet::DevError.new("The gem 'rgen' version >= 0.7.0 is required when using '--parser future'. An older version is installed, please update.") end end def self.code_merger Puppet::Pops::Parser::CodeMerger.new end end end diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index ae36e43ec..00576c861 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,103 +1,100 @@ require 'puppet/parser/files' require 'erb' # A simple wrapper for templates, so they don't have full access to # the scope objects. # # @api private class Puppet::Parser::TemplateWrapper include Puppet::Util Puppet::Util.logmethods(self) def initialize(scope) @__scope__ = scope end # @return [String] The full path name of the template that is being executed # @api public def file @__file__ end # @return [Puppet::Parser::Scope] The scope in which the template is evaluated # @api public def scope @__scope__ end # Find which line in the template (if any) we were called from. # @return [String] the line number # @api private def script_line identifier = Regexp.escape(@__file__ || "(erb)") (caller.find { |l| l =~ /#{identifier}:/ }||"")[/:(\d+):/,1] end private :script_line # Should return true if a variable is defined, false if it is not # @api public def has_variable?(name) scope.include?(name.to_s) end # @return [Array] The list of defined classes # @api public def classes scope.catalog.classes end # @return [Array] The tags defined in the current scope # @api public def tags scope.tags end # @return [Array] All the defined tags # @api public def all_tags scope.catalog.tags end # @api private def file=(filename) unless @__file__ = Puppet::Parser::Files.find_template(filename, scope.compiler.environment) raise Puppet::ParseError, "Could not find template '#{filename}'" end - - # We'll only ever not have a parser in testing, but, eh. - scope.known_resource_types.watch_file(@__file__) end # @api private def result(string = nil) if string template_source = "inline template" else string = File.read(@__file__) template_source = @__file__ end # Expose all the variables in our scope as instance variables of the # current object, making it possible to access them without conflict # to the regular methods. benchmark(:debug, "Bound template variables for #{template_source}") do scope.to_hash.each do |name, value| realname = name.gsub(/[^\w]/, "_") instance_variable_set("@#{realname}", value) end end result = nil benchmark(:debug, "Interpolated template #{template_source}") do template = ERB.new(string, 0, "-") template.filename = @__file__ result = template.result(binding) end result end def to_s "template[#{(@__file__ ? @__file__ : "inline")}]" end end diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 9ed1806a4..01dde7494 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -1,152 +1,151 @@ require 'find' require 'forwardable' -require 'puppet/node/environment' require 'puppet/parser/parser_factory' class Puppet::Parser::TypeLoader extend Forwardable # Import manifest files that match a given file glob pattern. # # @param pattern [String] the file glob to apply when determining which files # to load # @param dir [String] base directory to use when the file is not # found in a module # @api private def import(pattern, dir) return if Puppet[:ignoreimport] modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? abspat = File.expand_path(pattern, dir) file_pattern = abspat + (File.extname(abspat).empty? ? '.pp' : '' ) files = Dir.glob(file_pattern).uniq.reject { |f| FileTest.directory?(f) } modname = nil if files.empty? raise_no_files_found(pattern) end end load_files(modname, files) end # Load all of the manifest files in all known modules. # @api private def import_all # And then load all files from each module, but (relying on system # behavior) only load files from the first module of a given name. E.g., # given first/foo and second/foo, only files from first/foo will be loaded. environment.modules.each do |mod| load_files(mod.name, mod.all_manifests) end end def_delegator :environment, :known_resource_types def initialize(env) self.environment = env end def environment @environment end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = Puppet.lookup(:environments).get!(env) else @environment = env end end # Try to load the object with the given fully qualified name. def try_load_fqname(type, fqname) return nil if fqname == "" # special-case main. files_to_try_for(fqname).each do |filename| begin imported_types = import_from_modules(filename) if result = imported_types.find { |t| t.type == type and t.name == fqname } Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}" return result end rescue Puppet::ImportError => detail # I'm not convienced we should just drop these errors, but this # preserves existing behaviours. end end # Nothing found. return nil end def parse_file(file) Puppet.debug("importing '#{file}' in environment #{environment}") - parser = Puppet::Parser::ParserFactory.parser(environment) + parser = Puppet::Parser::ParserFactory.parser parser.file = file return parser.parse end private def import_from_modules(pattern) modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? raise_no_files_found(pattern) end load_files(modname, files) end def raise_no_files_found(pattern) raise Puppet::ImportError, "No file(s) found for import of '#{pattern}'" end def load_files(modname, files) @loaded ||= {} loaded_asts = [] files.reject { |file| @loaded[file] }.each do |file| # NOTE: This ugly implementation will be replaced in Puppet 3.5. # The implementation now makes use of a global variable because the context support is # not available until Puppet 3.5. # The use case is that parsing for the purpose of searching for information # should not abort. There is currently one such use case in indirector/resourcetype/parser # if Puppet.lookup(:squelch_parse_errors) {|| false } begin loaded_asts << parse_file(file) rescue => e # Resume from errors so that all parseable files would # still be parsed. Mark this file as loaded so that # it would not be parsed next time (handle it as if # it was successfully parsed). Puppet.debug("Unable to parse '#{file}': #{e.message}") end else loaded_asts << parse_file(file) end @loaded[file] = true end loaded_asts.collect do |ast| known_resource_types.import_ast(ast, modname) end.flatten end # Return a list of all file basenames that should be tried in order # to load the object with the given fully qualified name. def files_to_try_for(qualified_name) qualified_name.split('::').inject([]) do |paths, name| add_path_for_name(paths, name) end end def add_path_for_name(paths, name) if paths.empty? [name] else paths.unshift(File.join(paths.first, name)) end end end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index f5b0bda31..ed8637983 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,230 +1,215 @@ require 'puppet/parser/type_loader' require 'puppet/util/file_watcher' require 'puppet/util/warnings' class Puppet::Resource::TypeCollection attr_reader :environment attr_accessor :parse_failed include Puppet::Util::Warnings def clear @hostclasses.clear @definitions.clear @nodes.clear - @watched_files.clear @notfound.clear end def initialize(env) @environment = env @hostclasses = {} @definitions = {} @nodes = {} @notfound = {} # So we can keep a list and match the first-defined regex @node_list = [] - - @watched_files = Puppet::Util::FileWatcher.new end def import_ast(ast, modname) ast.instantiate(modname).each do |instance| add(instance) end end def inspect "TypeCollection" + { :hostclasses => @hostclasses.keys, :definitions => @definitions.keys, :nodes => @nodes.keys }.inspect end def <<(thing) add(thing) self end def add(instance) if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass other.merge(instance) return other end method = "add_#{instance.type}" send(method, instance) instance.resource_type_collection = self instance end def add_hostclass(instance) dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" } @hostclasses[instance.name] = instance instance end def hostclass(name) @hostclasses[munge_name(name)] end def add_node(instance) dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } @node_list << instance @nodes[instance.name] = instance instance end def loader @loader ||= Puppet::Parser::TypeLoader.new(environment) end def node(name) name = munge_name(name) if node = @nodes[name] return node end @node_list.each do |node| next unless node.name_is_regex? return node if node.match(name) end nil end def node_exists?(name) @nodes[munge_name(name)] end def nodes? @nodes.length > 0 end def add_definition(instance) dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" } @definitions[instance.name] = instance end def definition(name) @definitions[munge_name(name)] end def find_node(namespaces, name) @nodes[munge_name(name)] end def find_hostclass(namespaces, name, options = {}) find_or_load(namespaces, name, :hostclass, options) end def find_definition(namespaces, name) find_or_load(namespaces, name, :definition) end [:hostclasses, :nodes, :definitions].each do |m| define_method(m) do instance_variable_get("@#{m}").dup end end def require_reparse? - @parse_failed || stale? - end - - def stale? - @watched_files.changed? + @parse_failed end def version if !defined?(@version) if environment.config_version.nil? || environment.config_version == "" @version = Time.now.to_i else @version = Puppet::Util::Execution.execute([environment.config_version]).strip end end @version rescue Puppet::ExecutionFailure => e raise Puppet::ParseError, "Execution of config_version command `#{environment.config_version}` failed: #{e.message}", e.backtrace end - def watch_file(filename) - @watched_files.watch(filename) - end - - def watching_file?(filename) - @watched_files.watching?(filename) - end - private # Return a list of all possible fully-qualified names that might be # meant by the given name, in the context of namespaces. def resolve_namespaces(namespaces, name) name = name.downcase if name =~ /^::/ # name is explicitly fully qualified, so just return it, sans # initial "::". return [name.sub(/^::/, '')] end if name == "" # The name "" has special meaning--it always refers to a "main" # hostclass which contains all toplevel resources. return [""] end namespaces = [namespaces] unless namespaces.is_a?(Array) namespaces = namespaces.collect { |ns| ns.downcase } result = [] namespaces.each do |namespace| ary = namespace.split("::") # Search each namespace nesting in innermost-to-outermost order. while ary.length > 0 result << "#{ary.join("::")}::#{name}" ary.pop end # Finally, search the toplevel namespace. result << name end return result.uniq end # Resolve namespaces and find the given object. Autoload it if # necessary. def find_or_load(namespaces, name, type, options = {}) searchspace = options[:assume_fqname] ? [name].flatten : resolve_namespaces(namespaces, name) searchspace.each do |fqname| result = send(type, fqname) unless result if @notfound[fqname] and Puppet[:ignoremissingtypes] # do not try to autoload if we already tried and it wasn't conclusive # as this is a time consuming operation. Warn the user. debug_once "Not attempting to load #{type} #{fqname} as this object was missing during a prior compilation" else result = loader.try_load_fqname(type, fqname) @notfound[fqname] = result.nil? end end return result if result end return nil end def munge_name(name) name.to_s.downcase end def dupe_check(instance, hash) return unless dupe = hash[instance.name] message = yield dupe instance.fail Puppet::ParseError, message end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 41a98cb39..1f211bb1d 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,510 +1,495 @@ #! /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.create("testing", []) } include PuppetSpec::Files context 'the environment' do it "converts an environment to string when converting to YAML" do env.to_yaml.should match(/--- testing/) end it "uses the filetimeout for the ttl for the module list" do expect(Puppet::Node::Environment.attr_ttl(:modules)).to eq(Integer(Puppet[:filetimeout])) end describe ".create" do it "creates equivalent environments whether specifying name as a symbol or a string" do expect(Puppet::Node::Environment.create(:one, [])).to eq(Puppet::Node::Environment.create("one", [])) end it "interns name" do expect(Puppet::Node::Environment.create("one", []).name).to equal(:one) end it "does not produce environment singletons" do expect(Puppet::Node::Environment.create("one", [])).to_not equal(Puppet::Node::Environment.create("one", [])) end end it "returns its name when converted to a string" do expect(env.to_s).to eq("testing") end describe "equality" do it "works as a hash key" do base = Puppet::Node::Environment.create(:first, ["modules"], "manifests") same = Puppet::Node::Environment.create(:first, ["modules"], "manifests") different = Puppet::Node::Environment.create(:first, ["different"], "manifests") hash = {} hash[base] = "base env" hash[same] = "same env" hash[different] = "different env" expect(hash[base]).to eq("same env") expect(hash[different]).to eq("different env") expect(hash).to have(2).item end it "is equal when name, modules, and manifests are the same" do base = Puppet::Node::Environment.create(:base, ["modules"], "manifests") different_name = Puppet::Node::Environment.create(:different, base.full_modulepath, base.manifest) expect(base).to_not eq("not an environment") expect(base).to eq(base) expect(base.hash).to eq(base.hash) expect(base.override_with(:modulepath => ["different"])).to_not eq(base) expect(base.override_with(:modulepath => ["different"]).hash).to_not eq(base.hash) expect(base.override_with(:manifest => "different")).to_not eq(base) expect(base.override_with(:manifest => "different").hash).to_not eq(base.hash) expect(different_name).to_not eq(base) expect(different_name.hash).to_not eq(base.hash) end end describe "overriding an existing environment" do let(:original_path) { [tmpdir('original')] } let(:new_path) { [tmpdir('new')] } let(:environment) { Puppet::Node::Environment.create(:overridden, original_path, 'orig.pp', '/config/script') } it "overrides modulepath" do overridden = environment.override_with(:modulepath => new_path) expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('orig.pp')) expect(overridden.modulepath).to eq(new_path) expect(overridden.config_version).to eq('/config/script') end it "overrides manifest" do overridden = environment.override_with(:manifest => 'new.pp') expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('new.pp')) expect(overridden.modulepath).to eq(original_path) expect(overridden.config_version).to eq('/config/script') end it "overrides config_version" do overridden = environment.override_with(:config_version => '/new/script') expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('orig.pp')) expect(overridden.modulepath).to eq(original_path) expect(overridden.config_version).to eq('/new/script') end end - describe "watching a file" do - let(:filename) { "filename" } - - it "accepts a File" do - file = tmpfile(filename) - env.known_resource_types.expects(:watch_file).with(file.to_s) - env.watch_file(file) - end - - it "accepts a String" do - env.known_resource_types.expects(:watch_file).with(filename) - env.watch_file(filename) - end - end - describe "when managing known resource types" do before do env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) end it "creates a resource type collection if none exists" do expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end it "memoizes resource type collection" do expect(env.known_resource_types).to equal(env.known_resource_types) end it "performs 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 "generates 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 env.check_for_reparse new_type_collection = env.known_resource_types expect(new_type_collection).to be_a Puppet::Resource::TypeCollection expect(new_type_collection).to_not equal(old_type_collection) end end it "validates the modulepath directories" do real_file = tmpdir('moduledir') path = ['/one', '/two', real_file] env = Puppet::Node::Environment.create(:test, path) expect(env.modulepath).to eq([real_file]) end it "prefixes the value of the 'PUPPETLIB' environment variable to the module path if present" do first_puppetlib = tmpdir('puppetlib1') second_puppetlib = tmpdir('puppetlib2') first_moduledir = tmpdir('moduledir1') second_moduledir = tmpdir('moduledir2') Puppet::Util.withenv("PUPPETLIB" => [first_puppetlib, second_puppetlib].join(File::PATH_SEPARATOR)) do env = Puppet::Node::Environment.create(:testing, [first_moduledir, second_moduledir]) expect(env.modulepath).to eq([first_puppetlib, second_puppetlib, first_moduledir, second_moduledir]) end end describe "validating manifest settings" do before(:each) do Puppet[:default_manifest] = "/default/manifests/site.pp" end it "has no validation errors when disable_per_environment_manifest is false" do expect(Puppet::Node::Environment.create(:directory, [], '/some/non/default/manifest.pp').validation_errors).to be_empty end context "when disable_per_environment_manifest is true" do let(:config) { mock('config') } let(:global_modulepath) { ["/global/modulepath"] } let(:envconf) { Puppet::Settings::EnvironmentConf.new("/some/direnv", config, global_modulepath) } before(:each) do Puppet[:disable_per_environment_manifest] = true end def assert_manifest_conflict(expectation, envconf_manifest_value) config.expects(:setting).with(:manifest).returns( mock('setting', :value => envconf_manifest_value) ) environment = Puppet::Node::Environment.create(:directory, [], '/default/manifests/site.pp') loader = Puppet::Environments::Static.new(environment) loader.stubs(:get_conf).returns(envconf) Puppet.override(:environments => loader) do if expectation expect(environment.validation_errors).to have_matching_element(/The 'disable_per_environment_manifest' setting is true.*and the.*environment.*conflicts/) else expect(environment.validation_errors).to be_empty end end end it "has conflicting_manifest_settings when environment.conf manifest was set" do assert_manifest_conflict(true, '/some/envconf/manifest/site.pp') end it "does not have conflicting_manifest_settings when environment.conf manifest is empty" do assert_manifest_conflict(false, '') end it "does not have conflicting_manifest_settings when environment.conf manifest is nil" do assert_manifest_conflict(false, nil) end it "does not have conflicting_manifest_settings when environment.conf manifest is an exact, uninterpolated match of default_manifest" do assert_manifest_conflict(false, '/default/manifests/site.pp') end end end describe "when modeling a specific environment" do let(:first_modulepath) { tmpdir('firstmodules') } let(:second_modulepath) { tmpdir('secondmodules') } let(:env) { Puppet::Node::Environment.create(:modules_test, [first_modulepath, second_modulepath]) } let(:module_options) { { :environment => env, :metadata => { :author => 'puppetlabs', }, } } describe "module data" do describe ".module" do it "returns an individual module that exists in its module path" do one = PuppetSpec::Modules.create('one', first_modulepath, module_options) expect(env.module('one')).to eq(one) end it "returns nil if asked for a module that does not exist in its path" do expect(env.module("doesnotexist")).to be_nil end end describe "#modules_by_path" do it "returns an empty list if there are no modules" do expect(env.modules_by_path).to eq({ first_modulepath => [], second_modulepath => [] }) end it "includes modules even if they exist in multiple dirs in the modulepath" do one = PuppetSpec::Modules.create('one', first_modulepath, module_options) two = PuppetSpec::Modules.create('two', second_modulepath, module_options) expect(env.modules_by_path).to eq({ first_modulepath => [one], second_modulepath => [two], }) end it "ignores modules with invalid names" do PuppetSpec::Modules.generate_files('foo', first_modulepath) PuppetSpec::Modules.generate_files('foo2', first_modulepath) PuppetSpec::Modules.generate_files('foo-bar', first_modulepath) PuppetSpec::Modules.generate_files('foo_bar', first_modulepath) PuppetSpec::Modules.generate_files('foo=bar', first_modulepath) PuppetSpec::Modules.generate_files('foo bar', first_modulepath) PuppetSpec::Modules.generate_files('foo.bar', first_modulepath) PuppetSpec::Modules.generate_files('-foo', first_modulepath) PuppetSpec::Modules.generate_files('foo-', first_modulepath) PuppetSpec::Modules.generate_files('foo--bar', first_modulepath) expect(env.modules_by_path[first_modulepath].collect{|mod| mod.name}.sort).to eq(%w{foo foo-bar foo2 foo_bar}) end end describe "#module_requirements" do it "returns a list of what modules depend on other modules" do PuppetSpec::Modules.create( 'foo', first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }] } ) PuppetSpec::Modules.create( 'bar', second_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }] } ) PuppetSpec::Modules.create( 'baz', first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs-bar', "version_requirement" => "3.0.0" }] } ) PuppetSpec::Modules.create( 'alpha', first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) expect(env.module_requirements).to eq({ '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 "finds modules by forge_name" do mod = PuppetSpec::Modules.create( 'baz', first_modulepath, module_options, ) expect(env.module_by_forge_name('puppetlabs/baz')).to eq(mod) end it "does not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', first_modulepath, :metadata => {:author => 'sneakylabs'}, :environment => env ) expect(env.module_by_forge_name('puppetlabs/baz')).to eq(nil) end it "returns nil when the module can't be found" do expect(env.module_by_forge_name('ima/nothere')).to be_nil end end describe ".modules" do it "returns an empty list if there are no modules" do expect(env.modules).to eq([]) end it "returns a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| PuppetSpec::Modules.generate_files(mod_name, first_modulepath) end %w{bee baz}.each do |mod_name| PuppetSpec::Modules.generate_files(mod_name, second_modulepath) end expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo bar bee baz}.sort) end it "removes duplicates" do PuppetSpec::Modules.generate_files('foo', first_modulepath) PuppetSpec::Modules.generate_files('foo', second_modulepath) expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo}) end it "ignores modules with invalid names" do PuppetSpec::Modules.generate_files('foo', first_modulepath) PuppetSpec::Modules.generate_files('foo2', first_modulepath) PuppetSpec::Modules.generate_files('foo-bar', first_modulepath) PuppetSpec::Modules.generate_files('foo_bar', first_modulepath) PuppetSpec::Modules.generate_files('foo=bar', first_modulepath) PuppetSpec::Modules.generate_files('foo bar', first_modulepath) expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo foo-bar foo2 foo_bar}) end it "creates modules with the correct environment" do PuppetSpec::Modules.generate_files('foo', first_modulepath) env.modules.each do |mod| expect(mod.environment).to eq(env) end end it "logs an exception if a module contains invalid metadata" do PuppetSpec::Modules.generate_files( 'foo', first_modulepath, :metadata => { :author => 'puppetlabs' # missing source, version, etc } ) Puppet.expects(:log_exception).with(is_a(Puppet::Module::MissingMetadata)) env.modules end end end end describe "when performing initial import" do it "loads from Puppet[:code]" do Puppet[:code] = "define foo {}" krt = env.known_resource_types expect(krt.find_definition('', 'foo')).to be_kind_of(Puppet::Resource::Type) end it "parses from the the environment's manifests if Puppet[:code] is not set" do filename = tmpfile('a_manifest.pp') File.open(filename, 'w') do |f| f.puts("define from_manifest {}") end env = Puppet::Node::Environment.create(:testing, [], filename) krt = env.known_resource_types expect(krt.find_definition('', 'from_manifest')).to be_kind_of(Puppet::Resource::Type) end it "prefers Puppet[:code] over manifest files" do Puppet[:code] = "define from_code_setting {}" filename = tmpfile('a_manifest.pp') File.open(filename, 'w') do |f| f.puts("define from_manifest {}") end env = Puppet::Node::Environment.create(:testing, [], filename) krt = env.known_resource_types expect(krt.find_definition('', 'from_code_setting')).to be_kind_of(Puppet::Resource::Type) end it "initial import proceeds even if manifest file does not exist on disk" do filename = tmpfile('a_manifest.pp') env = Puppet::Node::Environment.create(:testing, [], filename) expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end it "returns an empty TypeCollection if neither code nor manifests is present" do expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end it "fails helpfully if there is an error importing" do Puppet[:code] = "oops {" expect do env.known_resource_types end.to raise_error(Puppet::Error, /Could not parse for environment #{env.name}/) end it "does not do anything if the ignore_import settings is set" do Puppet[:ignoreimport] = true expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end it "should mark the type collection as needing a reparse when there is an error parsing" do Puppet[:code] = "oops {" expect do env.known_resource_types end.to raise_error(Puppet::Error, /Syntax error at .../) expect(env.known_resource_types.require_reparse?).to be_true end end end describe '#current' do it 'should return the current context' do env = Puppet::Node::Environment.create(:test, []) Puppet::Context.any_instance.expects(:lookup).with(:current_environment).returns(env) Puppet.expects(:deprecation_warning).once Puppet::Node::Environment.current.should equal(env) end end end diff --git a/spec/unit/parser/templatewrapper_spec.rb b/spec/unit/parser/templatewrapper_spec.rb index 2b1388d0a..38dcb7000 100755 --- a/spec/unit/parser/templatewrapper_spec.rb +++ b/spec/unit/parser/templatewrapper_spec.rb @@ -1,108 +1,101 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/templatewrapper' describe Puppet::Parser::TemplateWrapper do let(:known_resource_types) { Puppet::Resource::TypeCollection.new("env") } let(:scope) do compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) compiler.environment.stubs(:known_resource_types).returns known_resource_types Puppet::Parser::Scope.new compiler end let(:tw) { Puppet::Parser::TemplateWrapper.new(scope) } - it "marks the file for watching" do - full_file_name = given_a_template_file("fake_template", "content") - - known_resource_types.expects(:watch_file).with(full_file_name) - tw.file = "fake_template" - end - it "fails if a template cannot be found" do Puppet::Parser::Files.expects(:find_template).returns nil expect { tw.file = "fake_template" }.to raise_error(Puppet::ParseError) end it "stringifies as template[] for a file based template" do Puppet::Parser::Files.stubs(:find_template).returns("/tmp/fake_template") tw.file = "fake_template" tw.to_s.should eql("template[/tmp/fake_template]") end it "stringifies as template[inline] for a string-based template" do tw.to_s.should eql("template[inline]") end it "reads and evaluates a file-based template" do given_a_template_file("fake_template", "template contents") tw.file = "fake_template" tw.result.should eql("template contents") end it "provides access to the name of the template via #file" do full_file_name = given_a_template_file("fake_template", "<%= file %>") tw.file = "fake_template" tw.result.should == full_file_name end it "evaluates a given string as a template" do tw.result("template contents").should eql("template contents") end it "provides the defined classes with #classes" do catalog = mock 'catalog', :classes => ["class1", "class2"] scope.expects(:catalog).returns( catalog ) tw.classes.should == ["class1", "class2"] end it "provides all the tags with #all_tags" do catalog = mock 'catalog', :tags => ["tag1", "tag2"] scope.expects(:catalog).returns( catalog ) tw.all_tags.should == ["tag1","tag2"] end it "provides the tags defined in the current scope with #tags" do scope.expects(:tags).returns( ["tag1", "tag2"] ) tw.tags.should == ["tag1","tag2"] end it "raises error on access to removed in-scope variables via method calls" do scope["in_scope_variable"] = "is good" expect { tw.result("<%= in_scope_variable %>") }.to raise_error(/undefined local variable or method `in_scope_variable'/ ) end it "reports that variable is available when it is in scope" do scope["in_scope_variable"] = "is good" tw.result("<%= has_variable?('in_scope_variable') %>").should == "true" end it "reports that a variable is not available when it is not in scope" do tw.result("<%= has_variable?('not_in_scope_variable') %>").should == "false" end it "provides access to in-scope variables via instance variables" do scope["one"] = "foo" tw.result("<%= @one %>").should == "foo" end %w{! . ; :}.each do |badchar| it "translates #{badchar} to _ in instance variables" do scope["one#{badchar}"] = "foo" tw.result("<%= @one_ %>").should == "foo" end end def given_a_template_file(name, contents) full_name = "/full/path/to/#{name}" Puppet::Parser::Files.stubs(:find_template). with(name, anything()). returns(full_name) File.stubs(:read).with(full_name).returns(contents) full_name end end diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index 4ddd9c247..502f93348 100755 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -1,210 +1,210 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/type_loader' require 'puppet/parser/parser_factory' require 'puppet_spec/modules' require 'puppet_spec/files' describe Puppet::Parser::TypeLoader do include PuppetSpec::Modules include PuppetSpec::Files let(:empty_hostclass) { Puppet::Parser::AST::Hostclass.new('') } let(:loader) { Puppet::Parser::TypeLoader.new(:myenv) } let(:my_env) { Puppet::Node::Environment.create(:myenv, []) } around do |example| envs = Puppet::Environments::Static.new(my_env) Puppet.override(:environments => envs) do example.run end end it "should support an environment" do loader = Puppet::Parser::TypeLoader.new(:myenv) loader.environment.name.should == :myenv end it "should delegate its known resource types to its environment" do loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection) end describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do loader.try_load_fqname(:hostclass, "").should be_nil end it "should attempt to import each generated name" do loader.expects(:import_from_modules).with("foo/bar").returns([]) loader.expects(:import_from_modules).with("foo").returns([]) loader.try_load_fqname(:hostclass, "foo::bar") end it "should attempt to load each possible name going from most to least specific" do path_order = sequence('path') ['foo/bar/baz', 'foo/bar', 'foo'].each do |path| Puppet::Parser::Files.expects(:find_manifests_in_modules).with(path, anything).returns([nil, []]).in_sequence(path_order) end loader.try_load_fqname(:hostclass, 'foo::bar::baz') end end describe "when importing" do let(:stub_parser) { stub 'Parser', :file= => nil, :parse => empty_hostclass } before(:each) do Puppet::Parser::ParserFactory.stubs(:parser).with(anything).returns(stub_parser) end it "should return immediately when imports are being ignored" do Puppet::Parser::Files.expects(:find_manifests_in_modules).never Puppet[:ignoreimport] = true loader.import("foo", "/path").should be_nil end it "should find all manifests matching the file or pattern" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with("myfile", anything).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should pass the environment when looking for files" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with(anything, loader.environment).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should fail if no files are found" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns [nil, []] lambda { loader.import("myfile", "/path") }.should raise_error(Puppet::ImportError) end it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns ["modname", [make_absolute("/one")]] loader.expects(:parse_file).with(make_absolute("/one")).returns(Puppet::Parser::AST::Hostclass.new('')) loader.import("myfile", "/path") end it "should not attempt to import files that have already been imported" do loader = Puppet::Parser::TypeLoader.new(:myenv) Puppet::Parser::Files.expects(:find_manifests_in_modules).twice.returns ["modname", %w{/one}] loader.import("myfile", "/path").should_not be_empty loader.import("myfile", "/path").should be_empty end end describe "when importing all" do let(:base) { tmpdir("base") } let(:modulebase1) { File.join(base, "first") } let(:modulebase2) { File.join(base, "second") } let(:my_env) { Puppet::Node::Environment.create(:myenv, [modulebase1, modulebase2]) } before do # Create two module path directories FileUtils.mkdir_p(modulebase1) FileUtils.mkdir_p(modulebase2) end def mk_module(basedir, name) PuppetSpec::Modules.create(name, basedir) end # We have to pass the base path so that we can # write to modules that are in the second search path def mk_manifests(base, mod, files) files.collect do |file| name = mod.name + "::" + file.gsub("/", "::") path = File.join(base, mod.name, "manifests", file + ".pp") FileUtils.mkdir_p(File.split(path)[0]) # write out the class File.open(path, "w") { |f| f.print "class #{name} {}" } name end end it "should load all puppet manifests from all modules in the specified environment" do module1 = mk_module(modulebase1, "one") module2 = mk_module(modulebase2, "two") mk_manifests(modulebase1, module1, %w{a b}) mk_manifests(modulebase2, module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end it "should not load manifests from duplicate modules later in the module path" do module1 = mk_module(modulebase1, "one") # duplicate module2 = mk_module(modulebase2, "one") mk_manifests(modulebase1, module1, %w{a}) mk_manifests(modulebase2, module2, %w{c}) loader.import_all loader.environment.known_resource_types.hostclass("one::c").should be_nil end it "should load manifests from subdirectories" do module1 = mk_module(modulebase1, "one") mk_manifests(modulebase1, module1, %w{a a/b a/b/c}) loader.import_all loader.environment.known_resource_types.hostclass("one::a::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::a::b::c").should be_instance_of(Puppet::Resource::Type) end it "should skip modules that don't have manifests" do module1 = mk_module(modulebase1, "one") module2 = mk_module(modulebase2, "two") mk_manifests(modulebase2, module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_nil loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end end describe "when parsing a file" do - it "should create a new parser instance for each file using the current environment" do + it "requests a new parser instance for each file" do parser = stub 'Parser', :file= => nil, :parse => empty_hostclass - Puppet::Parser::ParserFactory.expects(:parser).twice.with(loader.environment).returns(parser) + Puppet::Parser::ParserFactory.expects(:parser).twice.returns(parser) loader.parse_file("/my/file") loader.parse_file("/my/other_file") end - it "should assign the parser its file and parse" do + it "assigns the parser its file and then parses" do parser = mock 'parser' - Puppet::Parser::ParserFactory.expects(:parser).with(loader.environment).returns(parser) + Puppet::Parser::ParserFactory.expects(:parser).returns(parser) parser.expects(:file=).with("/my/file") parser.expects(:parse).returns(empty_hostclass) loader.parse_file("/my/file") end end it "should be able to add classes to the current resource type collection" do file = tmpfile("simple_file.pp") File.open(file, "w") { |f| f.puts "class foo {}" } loader.import(File.basename(file), File.dirname(file)) loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type) end end diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb index 58f7a5cd4..1d76cbfba 100755 --- a/spec/unit/resource/type_collection_spec.rb +++ b/spec/unit/resource/type_collection_spec.rb @@ -1,418 +1,374 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource/type_collection' require 'puppet/resource/type' describe Puppet::Resource::TypeCollection do include PuppetSpec::Files let(:environment) { Puppet::Node::Environment.create(:testing, []) } before do @instance = Puppet::Resource::Type.new(:hostclass, "foo") @code = Puppet::Resource::TypeCollection.new(environment) end it "should consider '<<' to be an alias to 'add' but should return self" do @code.expects(:add).with "foo" @code.expects(:add).with "bar" @code << "foo" << "bar" end it "should set itself as the code collection for added resource types" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) node.resource_type_collection.should equal(@code) end it "should store node resource types as nodes" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) end it "should fail if a duplicate node is added" do @code.add(Puppet::Resource::Type.new(:node, "foo")) expect do @code.add(Puppet::Resource::Type.new(:node, "foo")) end.to raise_error(Puppet::ParseError, /cannot redefine/) end it "should store hostclasses as hostclasses" do klass = Puppet::Resource::Type.new(:hostclass, "foo") @code.add(klass) @code.hostclass("foo").should equal(klass) end it "merge together hostclasses of the same name" do klass1 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "first") klass2 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "second") @code.add(klass1) @code.add(klass2) @code.hostclass("foo").doc.should == "firstsecond" end it "should store definitions as definitions" do define = Puppet::Resource::Type.new(:definition, "foo") @code.add(define) @code.definition("foo").should equal(define) end it "should fail if a duplicate definition is added" do @code.add(Puppet::Resource::Type.new(:definition, "foo")) expect do @code.add(Puppet::Resource::Type.new(:definition, "foo")) end.to raise_error(Puppet::ParseError, /cannot be redefined/) end it "should remove all nodes, classes, and definitions when cleared" do loader = Puppet::Resource::TypeCollection.new(environment) loader.add Puppet::Resource::Type.new(:hostclass, "class") loader.add Puppet::Resource::Type.new(:definition, "define") loader.add Puppet::Resource::Type.new(:node, "node") - watched_file = tmpfile('watched_file') - loader.watch_file(watched_file) loader.clear loader.hostclass("class").should be_nil loader.definition("define").should be_nil loader.node("node").should be_nil - loader.should_not be_watching_file(watched_file) end describe "when resolving namespaces" do [ ['', '::foo', ['foo']], ['a', '::foo', ['foo']], ['a::b', '::foo', ['foo']], [['a::b'], '::foo', ['foo']], [['a::b', 'c'], '::foo', ['foo']], [['A::B', 'C'], '::Foo', ['foo']], ['', '', ['']], ['a', '', ['']], ['a::b', '', ['']], [['a::b'], '', ['']], [['a::b', 'c'], '', ['']], [['A::B', 'C'], '', ['']], ['', 'foo', ['foo']], ['a', 'foo', ['a::foo', 'foo']], ['a::b', 'foo', ['a::b::foo', 'a::foo', 'foo']], ['A::B', 'Foo', ['a::b::foo', 'a::foo', 'foo']], [['a::b'], 'foo', ['a::b::foo', 'a::foo', 'foo']], [['a', 'b'], 'foo', ['a::foo', 'foo', 'b::foo']], [['a::b', 'c::d'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'c::d::foo', 'c::foo']], [['a::b', 'a::c'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'a::c::foo']], ].each do |namespaces, name, expected_result| it "should resolve #{name.inspect} in namespaces #{namespaces.inspect} correctly" do @code.instance_eval { resolve_namespaces(namespaces, name) }.should == expected_result end end end describe "when looking up names" do before do @type = Puppet::Resource::Type.new(:hostclass, "ns::klass") end it "should support looking up with multiple namespaces" do @code.add @type @code.find_hostclass(%w{boo baz ns}, "klass").should equal(@type) end it "should not attempt to import anything when the type is already defined" do @code.add @type @code.loader.expects(:import).never @code.find_hostclass(%w{ns}, "klass").should equal(@type) end describe "that need to be loaded" do it "should use the loader to load the files" do @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass") @code.loader.expects(:try_load_fqname).with(:hostclass, "klass") @code.find_hostclass(["ns"], "klass") end it "should downcase the name and downcase and array-fy the namespaces before passing to the loader" do @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass") @code.loader.expects(:try_load_fqname).with(:hostclass, "klass") @code.find_hostclass("Ns", "Klass") end it "should use the class returned by the loader" do @code.loader.expects(:try_load_fqname).returns(:klass) @code.expects(:hostclass).with("ns::klass").returns(false) @code.find_hostclass("ns", "klass").should == :klass end it "should return nil if the name isn't found" do @code.loader.stubs(:try_load_fqname).returns(nil) @code.find_hostclass("Ns", "Klass").should be_nil end it "already-loaded names at broader scopes should not shadow autoloaded names" do @code.add Puppet::Resource::Type.new(:hostclass, "bar") @code.loader.expects(:try_load_fqname).with(:hostclass, "foo::bar").returns(:foobar) @code.find_hostclass("foo", "bar").should == :foobar end it "should not try to autoload names that we couldn't autoload in a previous step if ignoremissingtypes is enabled" do Puppet[:ignoremissingtypes] = true @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass").returns(nil) @code.loader.expects(:try_load_fqname).with(:hostclass, "klass").returns(nil) @code.find_hostclass("Ns", "Klass").should be_nil Puppet.expects(:debug).at_least_once.with {|msg| msg =~ /Not attempting to load hostclass/} @code.find_hostclass("Ns", "Klass").should be_nil end end end %w{hostclass node definition}.each do |data| describe "behavior of add for #{data}" do it "should return the added #{data}" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(data, "foo") loader.add(instance).should equal(instance) end it "should retrieve #{data} insensitive to case" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(data, "Bar") loader.add instance loader.send(data, "bAr").should equal(instance) end it "should return nil when asked for a #{data} that has not been added" do Puppet::Resource::TypeCollection.new(environment).send(data, "foo").should be_nil end end end describe "when finding a qualified instance" do it "should return any found instance if the instance name is fully qualified" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("namespace", "::foo::bar").should equal(instance) end it "should return nil if the instance name is fully qualified and no such instance exists" do loader = Puppet::Resource::TypeCollection.new(environment) loader.find_hostclass("namespace", "::foo::bar").should be_nil end it "should be able to find classes in the base namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo") loader.add instance loader.find_hostclass("", "foo").should equal(instance) end it "should return the partially qualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo", "bar::baz").should equal(instance) end it "should be able to find partially qualified objects in any of the provided namespaces" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass(["nons", "foo", "otherns"], "bar::baz").should equal(instance) end it "should return the unqualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("foo", "bar").should equal(instance) end it "should return the unqualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("foo::bar::baz", "bar").should equal(instance) end it "should should return the partially qualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "bar::baz").should equal(instance) end it "should return the qualified object if it exists in the root namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "foo::bar::baz").should equal(instance) end it "should return nil if the object cannot be found" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "eh").should be_nil end describe "when topscope has a class that has the same name as a local class" do before do @loader = Puppet::Resource::TypeCollection.new(environment) [ "foo::bar", "bar" ].each do |name| @loader.add Puppet::Resource::Type.new(:hostclass, name) end end it "should favor the local class, if the name is unqualified" do @loader.find_hostclass("foo", "bar").name.should == 'foo::bar' end it "should only look in the topclass, if the name is qualified" do @loader.find_hostclass("foo", "::bar").name.should == 'bar' end it "should only look in the topclass, if we assume the name is fully qualified" do @loader.find_hostclass("foo", "bar", :assume_fqname => true).name.should == 'bar' end end it "should not look in the local scope for classes when the name is qualified" do @loader = Puppet::Resource::TypeCollection.new(environment) @loader.add Puppet::Resource::Type.new(:hostclass, "foo::bar") @loader.find_hostclass("foo", "::bar").should == nil end end it "should be able to find nodes" do node = Puppet::Resource::Type.new(:node, "bar") loader = Puppet::Resource::TypeCollection.new(environment) loader.add(node) loader.find_node(stub("ignored"), "bar").should == node end it "should indicate whether any nodes are defined" do loader = Puppet::Resource::TypeCollection.new(environment) loader.add_node(Puppet::Resource::Type.new(:node, "foo")) loader.should be_nodes end it "should indicate whether no nodes are defined" do Puppet::Resource::TypeCollection.new(environment).should_not be_nodes end describe "when finding nodes" do before :each do @loader = Puppet::Resource::TypeCollection.new(environment) end it "should return any node whose name exactly matches the provided node name" do node = Puppet::Resource::Type.new(:node, "foo") @loader << node @loader.node("foo").should equal(node) end it "should return the first regex node whose regex matches the provided node name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, /\d/) @loader << node1 << node2 @loader.node("foo10").should equal(node1) end it "should preferentially return a node whose name is string-equal over returning a node whose regex matches a provided name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, "foo") @loader << node1 << node2 @loader.node("foo").should equal(node2) end end - describe "when managing files" do - before do - @loader = Puppet::Resource::TypeCollection.new(environment) - Puppet::Util::WatchedFile.stubs(:new).returns stub("watched_file") - end - - it "should have a method for specifying a file should be watched" do - @loader.should respond_to(:watch_file) - end - - it "should have a method for determining if a file is being watched" do - @loader.watch_file("/foo/bar") - @loader.should be_watching_file("/foo/bar") - end - - it "should use WatchedFile to watch files" do - Puppet::Util::WatchedFile.expects(:new).with("/foo/bar").returns stub("watched_file") - @loader.watch_file("/foo/bar") - end - - it "should be considered stale if any files have changed" do - file1 = stub 'file1', :changed? => false - file2 = stub 'file2', :changed? => true - Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2) - @loader.watch_file("/foo/bar") - @loader.watch_file("/other/bar") - - @loader.should be_stale - end - - it "should not be considered stable if no files have changed" do - file1 = stub 'file1', :changed? => false - file2 = stub 'file2', :changed? => false - Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2) - @loader.watch_file("/foo/bar") - @loader.watch_file("/other/bar") - - @loader.should_not be_stale - end - end - describe "when determining the configuration version" do before do @code = Puppet::Resource::TypeCollection.new(environment) end it "should default to the current time" do time = Time.now Time.stubs(:now).returns time @code.version.should == time.to_i end context "when config_version script is specified" do let(:environment) { Puppet::Node::Environment.create(:testing, [], '', '/my/foo') } it "should use the output of the environment's config_version setting if one is provided" do Puppet::Util::Execution.expects(:execute).with(["/my/foo"]).returns "output\n" @code.version.should == "output" end it "should raise a puppet parser error if executing config_version fails" do Puppet::Util::Execution.expects(:execute).raises(Puppet::ExecutionFailure.new("msg")) lambda { @code.version }.should raise_error(Puppet::ParseError) end end end end