diff --git a/acceptance/tests/pluginsync/apply_should_sync_plugins.rb b/acceptance/tests/pluginsync/apply_should_sync_plugins.rb index bbbc69aac..943238fa9 100644 --- a/acceptance/tests/pluginsync/apply_should_sync_plugins.rb +++ b/acceptance/tests/pluginsync/apply_should_sync_plugins.rb @@ -1,20 +1,22 @@ test_name "puppet apply should pluginsync" step "Create some modules in the modulepath" basedir = '/tmp/acceptance_pluginsync_modules' on agents, "rm -rf #{basedir}" on agents, "mkdir -p #{basedir}/1/a/lib/ #{basedir}/2/a/lib" +# create two modules called "a", in different paths... this is intended to validate precedence in the module path; +# if two modules are found with the same name, the one that is found earlier in the path should be used. create_remote_file(agents, "#{basedir}/1/a/lib/foo.rb", "#1a") create_remote_file(agents, "#{basedir}/2/a/lib/foo.rb", "#2a") on agents, puppet_apply("--modulepath=#{basedir}/1:#{basedir}/2 --pluginsync -e 'notify { \"hello\": }'") do agents.each do |agent| on agent, "cat #{agent['puppetvardir']}/lib/foo.rb" assert_match(/#1a/, stdout, "The synced plugin was not found or the wrong version was synced") on agent, "rm -f #{agent['puppetvardir']}/lib/foo.rb" end end on agents, "rm -rf #{basedir}" diff --git a/lib/puppet/file_serving/mount/plugins.rb b/lib/puppet/file_serving/mount/plugins.rb index a9048ab98..1e5dcefdd 100644 --- a/lib/puppet/file_serving/mount/plugins.rb +++ b/lib/puppet/file_serving/mount/plugins.rb @@ -1,34 +1,35 @@ require 'puppet/file_serving/mount' # Find files in the modules' plugins directories. # This is a very strange mount because it merges # many directories into one. class Puppet::FileServing::Mount::Plugins < Puppet::FileServing::Mount # Return an instance of the appropriate class. def find(relative_path, request) return nil unless mod = request.environment.modules.find { |mod| mod.plugin(relative_path) } path = mod.plugin(relative_path) path end def search(relative_path, request) # We currently only support one kind of search on plugins - return # them all. + Puppet.debug("Warning: calling Plugins.search with empty module path.") if request.environment.modules.empty? paths = request.environment.modules.find_all { |mod| mod.plugins? }.collect { |mod| mod.plugin_directory } if paths.empty? # If the modulepath is valid then we still need to return a valid root # directory for the search, but make sure nothing inside it is # returned. request.options[:recurse] = false request.environment.modulepath.empty? ? nil : request.environment.modulepath else paths end end def valid? true end end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index c3eff6b76..9ace9f195 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,233 +1,243 @@ require 'puppet/util/logging' require 'semver' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end include Puppet::Util::Logging TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" PLUGINS = "plugins" FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname Puppet::Node::Environment.new(environment).module(modname) end attr_reader :name, :environment attr_writer :environment attr_accessor :dependencies attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file return false unless FileTest.exist?(metadata_file) metadata = PSON.parse File.read(metadata_file) return metadata.is_a?(Hash) && !metadata.keys.empty? end def initialize(name, options = {}) @name = name @path = options[:path] assert_validity if options[:environment].is_a?(Puppet::Node::Environment) @environment = options[:environment] else @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? validate_puppet_version end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do - return false unless path - return false unless FileTest.exist?(subpath(type)) + unless path + Puppet.debug("No #{type} found; path not specified") + return false + end + + type_subpath = subpath(type) + unless FileTest.exist?(type_subpath) + Puppet.debug("No #{type} found in subpath '#{type_subpath}' " + + "(file / directory does not exist)") + return false + end + return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.to_s.sub(/s$/, '')) do |file| return nil unless path # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(type), file) else full_path = subpath(type) end return nil unless FileTest.exist?(full_path) return full_path end end def exist? ! path.nil? end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata data = PSON.parse File.read(metadata_file) [:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.{pp,rb}' for empty modules. def match_manifests(rest) pat = File.join(path, MANIFESTS, rest || 'init') [manifest("init.pp"),manifest("init.rb")].compact + Dir. glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')). reject { |f| FileTest.directory?(f) } end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end # Find this module in the modulepath. def path @path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } end # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("plugins") end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def unmet_dependencies return [] unless dependencies unmet_dependencies = [] dependencies.each do |dependency| forge_name = dependency['name'] author, dep_name = forge_name.split('/') version_string = dependency['version_requirement'] equality, dep_version = version_string ? version_string.split("\s") : [nil, nil] unless dep_mod = environment.module(dep_name) msg = "Missing dependency `#{dep_name}`:\n" msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" unmet_dependencies << { :name => forge_name, :error => msg } next end if dep_version && !dep_mod.version msg = "Unversioned dependency `#{dep_mod.name}`:\n" msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" unmet_dependencies << { :name => forge_name, :error => msg } next end if dep_version begin required_version_semver = SemVer.new(dep_version) actual_version_semver = SemVer.new(dep_mod.version) rescue ArgumentError msg = "Non semantic version dependency `#{dep_mod.name}` (#{dep_mod.version}):\n" msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" unmet_dependencies << { :name => forge_name, :error => msg } next end if !actual_version_semver.send(equality, required_version_semver) msg = "Version dependency mismatch `#{dep_mod.name}` (#{dep_mod.version}):\n" msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n" unmet_dependencies << { :name => forge_name, :error => msg } next end end end unmet_dependencies end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end private def subpath(type) return File.join(path, type) unless type.to_s == "plugins" backward_compatible_plugins_dir end def backward_compatible_plugins_dir if dir = File.join(path, "plugins") and FileTest.exist?(dir) Puppet.deprecation_warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" return dir else return File.join(path, "lib") end end def assert_validity raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end def ==(other) self.name == other.name && self.version == other.version && self.path == other.path && self.environment == other.environment end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 06be16d89..5f88848c0 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,186 +1,194 @@ require 'puppet/util' require 'puppet/util/cacher' require 'monitor' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Model the environment that a node can operate in. This class just # provides a simple wrapper for the functionality around environments. class Puppet::Node::Environment module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher @seen = {} # Return an existing environment instance, or create a new one. def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end def self.current Thread.current[:environment] || root end def self.current=(env) Thread.current[:environment] = new(env) end def self.root @root end def self.clear @seen.clear end attr_reader :name # Return an environment-specific setting. def [](param) Puppet.settings.value(param, self.name) end def initialize(name) @name = name extend MonitorMixin end def known_resource_types # This makes use of short circuit evaluation to get the right thread-safe # per environment semantics with an efficient most common cases; we almost # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self Thread.current[:known_resource_types] ||= synchronize { if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import, '') end @known_resource_types } end def module(name) mod = Puppet::Module.new(name, :environment => self) return nil unless mod.exist? mod end # Cache the modulepath, so that we aren't searching through # all known directories all the time. cached_attr(:modulepath, Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # Return all modules from this environment. # Cache the list, because it can be expensive to create. cached_attr(:modules, Puppet[:filetimeout]) do - module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq + module_names = + modulepath.collect do |path| + module_names = Dir.entries(path) + 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.") if module_names.include?("lib") + module_names + end .flatten.uniq + module_names.collect do |path| begin Puppet::Module.new(path, :environment => self) rescue Puppet::Module::Error => e nil end end.compact end # Modules broken out by directory in the modulepath def modules_by_path modules_by_path = {} modulepath.each do |path| Dir.chdir(path) do module_names = Dir.glob('*').select { |d| FileTest.directory? d } modules_by_path[path] = module_names.map do |name| Puppet::Module.new(name, :environment => self, :path => File.join(path, name)) end end end modules_by_path end def to_s name.to_s end def to_sym to_s.to_sym end # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. def to_zaml(z) self.to_s.to_zaml(z) end def validate_dirs(dirs) dirs.collect do |dir| unless Puppet::Util.absolute_path?(dir) File.expand_path(File.join(Dir.getwd, dir)) else dir end end.find_all do |p| Puppet::Util.absolute_path?(p) && FileTest.directory?(p) end end private def perform_initial_import return empty_parse_result if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(self) if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, name.to_s) parser.file = file end parser.parse rescue => detail known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def empty_parse_result # Return an empty toplevel hostclass to use as the result of # perform_initial_import when no file was actually loaded. return Puppet::Parser::AST::Hostclass.new('') end @root = new(:'*root*') end diff --git a/spec/unit/file_serving/mount/plugins_spec.rb b/spec/unit/file_serving/mount/plugins_spec.rb index 09da124c3..a5bc0501d 100755 --- a/spec/unit/file_serving/mount/plugins_spec.rb +++ b/spec/unit/file_serving/mount/plugins_spec.rb @@ -1,73 +1,73 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_serving/mount/plugins' describe Puppet::FileServing::Mount::Plugins do before do @mount = Puppet::FileServing::Mount::Plugins.new("plugins") @environment = stub 'environment', :module => nil @options = { :recurse => true } @request = stub 'request', :environment => @environment, :options => @options end describe "when finding files" do it "should use the provided environment to find the modules" do @environment.expects(:modules).returns [] @mount.find("foo", @request) end it "should return nil if no module can be found with a matching plugin" do mod = mock 'module' mod.stubs(:plugin).with("foo/bar").returns nil @environment.stubs(:modules).returns [mod] @mount.find("foo/bar", @request).should be_nil end it "should return the file path from the module" do mod = mock 'module' mod.stubs(:plugin).with("foo/bar").returns "eh" @environment.stubs(:modules).returns [mod] @mount.find("foo/bar", @request).should == "eh" end end describe "when searching for files" do it "should use the node's environment to find the modules" do - @environment.expects(:modules).returns [] + @environment.expects(:modules).at_least_once.returns [] @environment.stubs(:modulepath).returns ["/tmp/modules"] @mount.search("foo", @request) end it "should return modulepath if no modules can be found that have plugins" do mod = mock 'module' mod.stubs(:plugins?).returns false @environment.stubs(:modules).returns [] @environment.stubs(:modulepath).returns ["/"] @options.expects(:[]=).with(:recurse, false) @mount.search("foo/bar", @request).should == ["/"] end it "should return nil if no modules can be found that have plugins and modulepath is invalid" do mod = mock 'module' mod.stubs(:plugins?).returns false @environment.stubs(:modules).returns [] @environment.stubs(:modulepath).returns [] @mount.search("foo/bar", @request).should be_nil end it "should return the plugin paths for each module that has plugins" do one = stub 'module', :plugins? => true, :plugin_directory => "/one" two = stub 'module', :plugins? => true, :plugin_directory => "/two" @environment.stubs(:modules).returns [one, two] @mount.search("foo/bar", @request).should == %w{/one /two} end end end