diff --git a/lib/puppet/environments.rb b/lib/puppet/environments.rb index 392f4b755..6d5bd2661 100644 --- a/lib/puppet/environments.rb +++ b/lib/puppet/environments.rb @@ -1,430 +1,445 @@ # @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 # Old-style environments that come either from explicit stanzas in # puppet.conf or from dynamic environments created from use of `$environment` # in puppet.conf. # # @example Explicit Stanza # [environment_name] # modulepath=/var/my_env/modules # # @example Dynamic Environments # [master] # modulepath=/var/$environment/modules # # @api private class Legacy include EnvironmentCreator # @!macro loader_search_paths def search_paths ["file://#{Puppet[:config]}"] end # @note The list of environments for the Legacy environments is always # empty. # # @!macro loader_list def list [] end # @note Because the Legacy system cannot list out all of its environments, # get is able to return environments that are not returned by a call to # {#list}. # # @!macro loader_get def get(name) Puppet::Node::Environment.new(name) end # @note Because the Legacy system cannot list out all of its environments, # this method will never fail and is only calling get directly. # # @!macro loader_get_or_fail def get!(name) get(name) end # @note we could return something here, but since legacy environments # are deprecated, there is no point. # # @!macro loader_get_conf def get_conf(name) nil 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 INFINITY = 1.0 / 0.0 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 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) case ttl when 0 NotCachedEntry.new(env) # Entry that is always expired (avoids syscall to get time) when 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)) @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/settings/environment_conf.rb b/lib/puppet/settings/environment_conf.rb index 63bb2cb68..8894100e0 100644 --- a/lib/puppet/settings/environment_conf.rb +++ b/lib/puppet/settings/environment_conf.rb @@ -1,174 +1,174 @@ # Configuration settings for a single directory Environment. # @api private class Puppet::Settings::EnvironmentConf VALID_SETTINGS = [:modulepath, :manifest, :config_version, :environment_timeout].freeze # Given a path to a directory environment, attempts to load and parse an # environment.conf in ini format, and return an EnvironmentConf instance. # # An environment.conf is optional, so if the file itself is missing, or # empty, an EnvironmentConf with default values will be returned. # # @note logs warnings if the environment.conf contains any ini sections, # or has settings other than the three handled for directory environments # (:manifest, :modulepath, :config_version) # # @param path_to_env [String] path to the directory environment # @param global_module_path [Array] the installation's base modulepath # setting, appended to default environment modulepaths # @return [EnvironmentConf] the parsed EnvironmentConf object def self.load_from(path_to_env, global_module_path) path_to_env = File.expand_path(path_to_env) conf_file = File.join(path_to_env, 'environment.conf') config = nil begin config = Puppet.settings.parse_file(conf_file) validate(conf_file, config) section = config.sections[:main] rescue Errno::ENOENT # environment.conf is an optional file end new(path_to_env, section, global_module_path) end # Provides a configuration object tied directly to the passed environment. # Configuration values are exactly those returned by the environment object, # without interpolation. This is a special case for the default configured # environment returned by the Puppet::Environments::StaticPrivate loader. def self.static_for(environment, environment_timeout = 0) Static.new(environment, environment_timeout) end - attr_reader :section + attr_reader :section, :path_to_env, :global_modulepath # Create through EnvironmentConf.load_from() def initialize(path_to_env, section, global_module_path) @path_to_env = path_to_env @section = section @global_module_path = global_module_path end def manifest puppet_conf_manifest = Pathname.new(Puppet.settings.value(:default_manifest)) disable_per_environment_manifest = Puppet.settings.value(:disable_per_environment_manifest) fallback_manifest_directory = if puppet_conf_manifest.absolute? puppet_conf_manifest.to_s else File.join(@path_to_env, puppet_conf_manifest.to_s) end if disable_per_environment_manifest environment_conf_manifest = absolute(raw_setting(:manifest)) if environment_conf_manifest && fallback_manifest_directory != environment_conf_manifest errmsg = ["The 'disable_per_environment_manifest' setting is true, but the", "environment located at #{@path_to_env} has a manifest setting in its", "environment.conf of '#{environment_conf_manifest}' which does not match", "the default_manifest setting '#{puppet_conf_manifest}'. If this", "environment is expecting to find modules in", "'#{environment_conf_manifest}', they will not be available!"] Puppet.err(errmsg.join(' ')) end fallback_manifest_directory.to_s else get_setting(:manifest, fallback_manifest_directory) do |manifest| absolute(manifest) end end end def environment_timeout # gen env specific config or use the default value get_setting(:environment_timeout, Puppet.settings.value(:environment_timeout)) do |ttl| # munges the string form statically without really needed the settings system, only # its ability to munge "4s, 3m, 5d, and 'unlimited' into seconds - if already munged into # numeric form, the TTLSetting handles that. Puppet::Settings::TTLSetting.munge(ttl, 'environment_timeout') end end def modulepath default_modulepath = [File.join(@path_to_env, "modules")] + @global_module_path get_setting(:modulepath, default_modulepath) do |modulepath| path = modulepath.kind_of?(String) ? modulepath.split(File::PATH_SEPARATOR) : modulepath path.map { |p| absolute(p) }.join(File::PATH_SEPARATOR) end end def config_version get_setting(:config_version) do |config_version| absolute(config_version) end end def raw_setting(setting_name) setting = section.setting(setting_name) if section setting.value if setting end private def self.validate(path_to_conf_file, config) valid = true section_keys = config.sections.keys main = config.sections[:main] if section_keys.size > 1 Puppet.warning("Invalid sections in environment.conf at '#{path_to_conf_file}'. Environment conf may not have sections. The following sections are being ignored: '#{(section_keys - [:main]).join(',')}'") valid = false end extraneous_settings = main.settings.map(&:name) - VALID_SETTINGS if !extraneous_settings.empty? Puppet.warning("Invalid settings in environment.conf at '#{path_to_conf_file}'. The following unknown setting(s) are being ignored: #{extraneous_settings.join(', ')}") valid = false end return valid end def get_setting(setting_name, default = nil) value = raw_setting(setting_name) value ||= default yield value end def absolute(path) return nil if path.nil? if path =~ /^\$/ # Path begins with $something interpolatable path else File.expand_path(path, @path_to_env) end end # Models configuration for an environment that is not loaded from a directory. # # @api private class Static attr_reader :environment_timeout def initialize(environment, environment_timeout) @environment = environment @environment_timeout = environment_timeout end def manifest @environment.manifest end def modulepath @environment.modulepath.join(File::PATH_SEPARATOR) end def config_version @environment.config_version end end end diff --git a/spec/unit/environments_spec.rb b/spec/unit/environments_spec.rb index 0d221bb58..19c680707 100644 --- a/spec/unit/environments_spec.rb +++ b/spec/unit/environments_spec.rb @@ -1,534 +1,652 @@ require 'spec_helper' require 'puppet/environments' require 'puppet/file_system' require 'matchers/include' +require 'matchers/include_in_order' module PuppetEnvironments describe Puppet::Environments do include Matchers::Include FS = Puppet::FileSystem before(:each) do Puppet.settings.initialize_global_settings Puppet[:environment_timeout] = "unlimited" end let(:directory_tree) do FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_regular_file_containing("ignored_file", ''), FS::MemoryFile.a_directory("an_environment", [ FS::MemoryFile.a_missing_file("environment.conf"), FS::MemoryFile.a_directory("modules"), FS::MemoryFile.a_directory("manifests"), ]), FS::MemoryFile.a_directory("another_environment", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) end describe "directories loader" do it "lists environments" do global_path_1_location = File.expand_path("global_path_1") global_path_2_location = File.expand_path("global_path_2") global_path_1 = FS::MemoryFile.a_directory(global_path_1_location) global_path_2 = FS::MemoryFile.a_directory(global_path_2_location) loader_from(:filesystem => [directory_tree, global_path_1, global_path_2], :directory => directory_tree, :modulepath => [global_path_1_location, global_path_2_location]) do |loader| expect(loader.list).to include_in_any_order( environment(:an_environment). with_manifest("#{FS.path_string(directory_tree)}/an_environment/manifests"). with_modulepath(["#{FS.path_string(directory_tree)}/an_environment/modules", global_path_1_location, global_path_2_location]), environment(:another_environment)) end end + it "has search_paths" do + loader_from(:filesystem => [directory_tree], + :directory => directory_tree) do |loader| + expect(loader.search_paths).to eq(["file://#{directory_tree}"]) + end + end + it "ignores directories that are not valid env names (alphanumeric and _)" do envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory(".foo"), FS::MemoryFile.a_directory("bar-thing"), FS::MemoryFile.a_directory("with spaces"), FS::MemoryFile.a_directory("some.thing"), FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), FS::MemoryFile.a_directory("env2", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) loader_from(:filesystem => [envdir], :directory => envdir) do |loader| expect(loader.list).to include_in_any_order(environment(:env1), environment(:env2)) end end it "gets a particular environment" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(loader.get("an_environment")).to environment(:an_environment) end end it "raises error when environment not found" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect do loader.get!("does_not_exist") end.to raise_error(Puppet::Environments::EnvironmentNotFound) end end it "returns nil if an environment can't be found" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(loader.get("env_not_in_this_list")).to be_nil end end context "with an environment.conf" do let(:envdir) do FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), ]), ]) end let(:manifestdir) { FS::MemoryFile.a_directory(File.expand_path("/some/manifest/path")) } let(:modulepath) do [ FS::MemoryFile.a_directory(File.expand_path("/some/module/path")), FS::MemoryFile.a_directory(File.expand_path("/some/other/path")), ] end let(:content) do <<-EOF manifest=#{manifestdir} modulepath=#{modulepath.join(File::PATH_SEPARATOR)} config_version=/some/script EOF end it "reads environment.conf settings" do loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)) end end it "does not append global_module_path to environment.conf modulepath setting" do global_path_location = File.expand_path("global_path") global_path = FS::MemoryFile.a_directory(global_path_location) loader_from(:filesystem => [envdir, manifestdir, modulepath, global_path].flatten, :directory => envdir, :modulepath => [global_path]) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)) end end it "reads config_version setting" do loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end end it "accepts an empty environment.conf without warning" do content = nil envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), ]), ]) manifestdir = FS::MemoryFile.a_directory(File.join(envdir, "env1", "manifests")) modulesdir = FS::MemoryFile.a_directory(File.join(envdir, "env1", "modules")) global_path_location = File.expand_path("global_path") global_path = FS::MemoryFile.a_directory(global_path_location) loader_from(:filesystem => [envdir, manifestdir, modulesdir, global_path].flatten, :directory => envdir, :modulepath => [global_path]) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest("#{FS.path_string(envdir)}/env1/manifests"). with_modulepath(["#{FS.path_string(envdir)}/env1/modules", global_path_location]). with_config_version(nil) end expect(@logs).to be_empty end it "logs a warning, but processes the main settings if there are extraneous sections" do content << "[foo]" loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end expect(@logs.map(&:to_s).join).to match(/Invalid.*at.*\/env1.*may not have sections.*ignored: 'foo'/) end it "logs a warning, but processes the main settings if there are any extraneous settings" do content << "dog=arf\n" content << "cat=mew\n" content << "[ignored]\n" content << "cow=moo\n" loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end expect(@logs.map(&:to_s).join).to match(/Invalid.*at.*\/env1.*unknown setting.*dog, cat/) end it "interpretes relative paths from the environment's directory" do content = <<-EOF manifest=relative/manifest modulepath=relative/modules config_version=relative/script EOF envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_missing_file("modules"), FS::MemoryFile.a_directory('relative', [ FS::MemoryFile.a_directory('modules'), ]), ]), ]) loader_from(:filesystem => [envdir], :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(File.join(envdir, 'env1', 'relative', 'manifest')). with_modulepath([File.join(envdir, 'env1', 'relative', 'modules')]). with_config_version(File.join(envdir, 'env1', 'relative', 'script')) end end it "interpolates other setting values correctly" do modulepath = [ File.expand_path('/some/absolute'), '$basemodulepath', 'modules' ].join(File::PATH_SEPARATOR) content = <<-EOF manifest=$confdir/whackymanifests modulepath=#{modulepath} config_version=$vardir/random/scripts EOF some_absolute_dir = FS::MemoryFile.a_directory(File.expand_path('/some/absolute')) base_module_dirs = Puppet[:basemodulepath].split(File::PATH_SEPARATOR).map do |path| FS::MemoryFile.a_directory(path) end envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_directory("modules"), ]), ]) loader_from(:filesystem => [envdir, some_absolute_dir, base_module_dirs].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(File.join(Puppet[:confdir], 'whackymanifests')). with_modulepath([some_absolute_dir.path, base_module_dirs.map { |d| d.path }, File.join(envdir, 'env1', 'modules')].flatten). with_config_version(File.join(Puppet[:vardir], 'random', 'scripts')) end end it "uses environment.conf settings regardless of existence of modules and manifests subdirectories" do envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_directory("modules"), FS::MemoryFile.a_directory("manifests"), ]), ]) loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end end it "should update environment settings if environment.conf has changed and timeout has expired" do base_dir = File.expand_path("envdir") original_envdir = FS::MemoryFile.a_directory(base_dir, [ FS::MemoryFile.a_directory("env3", [ FS::MemoryFile.a_regular_file_containing("environment.conf", <<-EOF) manifest=/manifest_orig modulepath=/modules_orig environment_timeout=0 EOF ]), ]) FS.overlay(original_envdir) do dir_loader = Puppet::Environments::Directories.new(original_envdir, []) loader = Puppet::Environments::Cached.new(dir_loader) Puppet.override(:environments => loader) do original_env = loader.get("env3") # force the environment.conf to be read changed_envdir = FS::MemoryFile.a_directory(base_dir, [ FS::MemoryFile.a_directory("env3", [ FS::MemoryFile.a_regular_file_containing("environment.conf", <<-EOF) manifest=/manifest_changed modulepath=/modules_changed environment_timeout=0 EOF ]), ]) FS.overlay(changed_envdir) do changed_env = loader.get("env3") expect(original_env).to environment(:env3). with_manifest(File.expand_path("/manifest_orig")). with_full_modulepath([File.expand_path("/modules_orig")]) expect(changed_env).to environment(:env3). with_manifest(File.expand_path("/manifest_changed")). with_full_modulepath([File.expand_path("/modules_changed")]) end end end end context "custom cache expiration service" do it "consults the custom service to expire the cache" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| service = ReplayExpirationService.new([true]) using_expiration_service(service) do cached = Puppet::Environments::Cached.new(loader) cached.get(:an_environment) cached.get(:an_environment) expect(service.created_envs).to include(:an_environment) expect(service.expired_envs).to include(:an_environment) expect(service.evicted_envs).to include(:an_environment) end end end end end end describe "static loaders" do let(:static1) { Puppet::Node::Environment.create(:static1, []) } let(:static2) { Puppet::Node::Environment.create(:static2, []) } let(:loader) { Puppet::Environments::Static.new(static1, static2) } it "lists environments" do expect(loader.list).to eq([static1, static2]) end + it "has search_paths" do + expect(loader.search_paths).to eq(["data:text/plain,internal"]) + end + it "gets an environment" do expect(loader.get(:static2)).to eq(static2) end it "returns nil if env not found" do expect(loader.get(:doesnotexist)).to be_nil end it "raises error if environment is not found" do expect do loader.get!(:doesnotexist) end.to raise_error(Puppet::Environments::EnvironmentNotFound) end it "gets a basic conf" do conf = loader.get_conf(:static1) expect(conf.modulepath).to eq('') expect(conf.manifest).to eq(:no_manifest) expect(conf.config_version).to be_nil end it "returns nil if you request a configuration from an env that doesn't exist" do expect(loader.get_conf(:doesnotexist)).to be_nil end context "that are private" do let(:private_env) { Puppet::Node::Environment.create(:private, []) } let(:loader) { Puppet::Environments::StaticPrivate.new(private_env) } it "lists nothing" do expect(loader.list).to eq([]) end end end + describe "combined loaders" do + let(:static1) { Puppet::Node::Environment.create(:static1, []) } + let(:static2) { Puppet::Node::Environment.create(:static2, []) } + let(:static_loader) { Puppet::Environments::Static.new(static1, static2) } + let(:directory_tree) do + FS::MemoryFile.a_directory(File.expand_path("envdir"), [ + FS::MemoryFile.a_directory("an_environment", [ + FS::MemoryFile.a_missing_file("environment.conf"), + FS::MemoryFile.a_directory("modules"), + FS::MemoryFile.a_directory("manifests"), + ]), + ]) + end + + it "lists environments" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + envs = Puppet::Environments::Combined.new(loader, static_loader).list + expect(envs[0]).to environment(:an_environment) + expect(envs[1]).to environment(:static1) + expect(envs[2]).to environment(:static2) + end + end + + it "has search_paths" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Combined.new(loader, static_loader).search_paths).to eq(["file://#{directory_tree}","data:text/plain,internal"]) + end + end + + it "gets an environment" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Combined.new(loader, static_loader).get(:an_environment)).to environment(:an_environment) + expect(Puppet::Environments::Combined.new(loader, static_loader).get(:static2)).to environment(:static2) + end + end + + it "returns nil if env not found" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Combined.new(loader, static_loader).get(:env_does_not_exist)).to be_nil + end + end + + it "raises an error if environment is not found" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect do + Puppet::Environments::Combined.new(loader, static_loader).get!(:env_does_not_exist) + end.to raise_error(Puppet::Environments::EnvironmentNotFound) + end + end + + it "gets an environment.conf" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Combined.new(loader, static_loader).get_conf(:an_environment)).to match_environment_conf(:an_environment). + with_env_path(directory_tree). + with_global_module_path([]) + end + end + end describe "cached loaders" do + it "lists environments" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Cached.new(loader).list).to include_in_any_order( + environment(:an_environment), + environment(:another_environment)) + end + end + + it "has search_paths" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Cached.new(loader).search_paths).to eq(["file://#{directory_tree}"]) + end + end + context "#get" do it "gets an environment" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(Puppet::Environments::Cached.new(loader).get(:an_environment)).to environment(:an_environment) end end it "does not reload the environment if it isn't expired" do env = Puppet::Node::Environment.create(:cached, []) mocked_loader = mock('loader') mocked_loader.expects(:get).with(:cached).returns(env).once mocked_loader.expects(:get_conf).with(:cached).returns(Puppet::Settings::EnvironmentConf.static_for(env, 20)).once cached = Puppet::Environments::Cached.new(mocked_loader) cached.get(:cached) cached.get(:cached) end it "returns nil if env not found" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(Puppet::Environments::Cached.new(loader).get(:doesnotexist)).to be_nil end end end context "#get!" do it "gets an environment" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(Puppet::Environments::Cached.new(loader).get!(:an_environment)).to environment(:an_environment) end end it "does not reload the environment if it isn't expired" do env = Puppet::Node::Environment.create(:cached, []) mocked_loader = mock('loader') mocked_loader.expects(:get).with(:cached).returns(env).once mocked_loader.expects(:get_conf).with(:cached).returns(Puppet::Settings::EnvironmentConf.static_for(env, 20)).once cached = Puppet::Environments::Cached.new(mocked_loader) cached.get!(:cached) cached.get!(:cached) end it "raises error if environment is not found" do loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect do Puppet::Environments::Cached.new(loader).get!(:doesnotexist) end.to raise_error(Puppet::Environments::EnvironmentNotFound) end end end + + it "gets an environment.conf" do + loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| + expect(Puppet::Environments::Cached.new(loader).get_conf(:an_environment)).to match_environment_conf(:an_environment). + with_env_path(directory_tree). + with_global_module_path([]) + end + end end RSpec::Matchers.define :environment do |name| match do |env| env.name == name && (!@manifest || @manifest == env.manifest) && (!@modulepath || @modulepath == env.modulepath) && (!@full_modulepath || @full_modulepath == env.full_modulepath) && (!@config_version || @config_version == env.config_version) end chain :with_manifest do |manifest| @manifest = manifest end chain :with_modulepath do |modulepath| @modulepath = modulepath end chain :with_full_modulepath do |full_modulepath| @full_modulepath = full_modulepath end chain :with_config_version do |config_version| @config_version = config_version end description do "environment #{expected}" + (@manifest ? " with manifest #{@manifest}" : "") + (@modulepath ? " with modulepath [#{@modulepath.join(', ')}]" : "") + (@full_modulepath ? " with full_modulepath [#{@full_modulepath.join(', ')}]" : "") + (@config_version ? " with config_version #{@config_version}" : "") end failure_message_for_should do |env| "expected <#{env.name}: modulepath = [#{env.modulepath.join(', ')}], manifest = #{env.manifest}, config_version = #{env.config_version}> to be #{description}" end end + RSpec::Matchers.define :match_environment_conf do |env_name| + match do |env_conf| + env_conf.path_to_env =~ /#{env_name}$/ && + (!@env_path || File.join(@env_path,env_name.to_s) == env_conf.path_to_env) && + (!@global_modulepath || @global_module_path == env_conf.global_module_path) + end + + chain :with_env_path do |env_path| + @env_path = env_path.to_s + end + + chain :with_global_module_path do |global_module_path| + @global_module_path = global_module_path + end + + description do + "EnvironmentConf #{expected}" + + " with path_to_env: #{@env_path ? @env_path : "*"}/#{env_name}" + + (@global_module_path ? " with global_module_path [#{@global_module_path.join(', ')}]" : "") + end + + failure_message_for_should do |env_conf| + "expected #{env_conf.inspect} to be #{description}" + end + end + def loader_from(options, &block) FS.overlay(*options[:filesystem]) do environments = Puppet::Environments::Directories.new( options[:directory], options[:modulepath] || [] ) Puppet.override(:environments => environments) do yield environments end end end def using_expiration_service(service) begin orig_svc = Puppet::Environments::Cached.cache_expiration_service Puppet::Environments::Cached.cache_expiration_service = service yield ensure Puppet::Environments::Cached.cache_expiration_service = orig_svc end end class ReplayExpirationService attr_reader :created_envs, :expired_envs, :evicted_envs def initialize(expiration_sequence) @created_envs = [] @expired_envs = [] @evicted_envs = [] @expiration_sequence = expiration_sequence end def created(env) @created_envs << env.name end def expired?(env_name) @expired_envs << env_name @expiration_sequence.pop end def evicted(env_name) @evicted_envs << env_name end end end end