diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index deecfd45b..3725935a2 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,339 +1,339 @@ require 'puppet/util/logging' require 'semver' require 'json' # 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 class InvalidFilePattern < Error; end include Puppet::Util::Logging FILETYPES = { "manifests" => "manifests", "files" => "files", "templates" => "templates", "plugins" => "lib", "pluginfacts" => "facts.d", } # 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 # Unless a specific environment is given, use the current environment env = environment ? Puppet.lookup(:environments).get!(environment) : Puppet.lookup(:current_environment) env.module(modname) end attr_reader :name, :environment, :path, :metadata attr_writer :environment attr_accessor :dependencies, :forge_name attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def initialize(name, path, environment) @name = name @path = path @environment = environment assert_validity load_metadata if has_metadata? validate_puppet_version @absolute_path_to_manifests = Puppet::FileSystem::PathPattern.absolute(manifests) end def has_metadata? return false unless metadata_file return false unless Puppet::FileSystem.exist?(metadata_file) begin metadata = JSON.parse(File.read(metadata_file)) rescue JSON::JSONError => e Puppet.debug("#{name} has an invalid and unparsable metadata.json file. The parse error: #{e.message}") return false end return metadata.is_a?(Hash) && !metadata.keys.empty? end FILETYPES.each do |type, location| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do type_subpath = subpath(location) unless Puppet::FileSystem.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.sub(/s$/, '')) do |file| # 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(location), file) else full_path = subpath(location) end return nil unless Puppet::FileSystem.exist?(full_path) return full_path end # Return the base directory for the given type define_method(type) do subpath(location) end 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 @metadata = data = JSON.parse(File.read(metadata_file)) @forge_name = data['name'].gsub('-', '/') if data['name'] [: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 # NOTICE: The fallback to `versionRequirement` is something we'd like to # not have to support, but we have a reasonable number of releases that # don't use `version_requirement`. When we can deprecate this, we should. if attr == :dependencies value.tap do |dependencies| dependencies.each do |dep| dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0' end 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) if rest wanted_manifests = wanted_manifests_from(rest) searched_manifests = wanted_manifests.glob.reject { |f| FileTest.directory?(f) } else searched_manifests = [] end # (#4220) Always ensure init.pp in case class is defined there. init_manifests = [manifest("init.pp"), manifest("init.rb")].compact - init_manifests + searched_manifests + (init_manifests + searched_manifests).uniq end def all_manifests return [] unless Puppet::FileSystem.exist?(manifests) Dir.glob(File.join(manifests, '**', '*.{rb,pp}')) 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 def modulepath File.dirname(path) if path end # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("lib") end def plugin_fact_directory subpath("facts.d") end def has_external_facts? File.directory?(plugin_fact_directory) end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def dependencies_as_modules dependent_modules = [] dependencies and dependencies.each do |dep| author, dep_name = dep["name"].split('/') found_module = environment.module(dep_name) dependent_modules << found_module if found_module end dependent_modules end def required_by environment.module_requirements[self.forge_name] || {} end def has_local_changes? Puppet.deprecation_warning("This method is being removed.") require 'puppet/module_tool/applications' changes = Puppet::ModuleTool::Applications::Checksummer.run(path) !changes.empty? end def local_changes Puppet.deprecation_warning("This method is being removed.") require 'puppet/module_tool/applications' Puppet::ModuleTool::Applications::Checksummer.run(path) end # Identify and mark unmet dependencies. A dependency will be marked unmet # for the following reasons: # # * not installed and is thus considered missing # * installed and does not meet the version requirements for this module # * installed and doesn't use semantic versioning # # Returns a list of hashes representing the details of an unmet dependency. # # Example: # # [ # { # :reason => :missing, # :name => 'puppetlabs-mysql', # :version_constraint => 'v0.0.1', # :mod_details => { # :installed_version => '0.0.1' # } # :parent => { # :name => 'puppetlabs-bacula', # :version => 'v1.0.0' # } # } # ] # def unmet_dependencies unmet_dependencies = [] return unmet_dependencies unless dependencies dependencies.each do |dependency| forge_name = dependency['name'] version_string = dependency['version_requirement'] || '>= 0.0.0' dep_mod = begin environment.module_by_forge_name(forge_name) rescue nil end error_details = { :name => forge_name, :version_constraint => version_string.gsub(/^(?=\d)/, "v"), :parent => { :name => self.forge_name, :version => self.version.gsub(/^(?=\d)/, "v") }, :mod_details => { :installed_version => dep_mod.nil? ? nil : dep_mod.version } } unless dep_mod error_details[:reason] = :missing unmet_dependencies << error_details next end if version_string begin required_version_semver_range = SemVer[version_string] actual_version_semver = SemVer.new(dep_mod.version) rescue ArgumentError error_details[:reason] = :non_semantic_version unmet_dependencies << error_details next end unless required_version_semver_range.include? actual_version_semver error_details[:reason] = :version_mismatch unmet_dependencies << error_details 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 wanted_manifests_from(pattern) begin extended = File.extname(pattern).empty? ? "#{pattern}.{pp,rb}" : pattern relative_pattern = Puppet::FileSystem::PathPattern.relative(extended) rescue Puppet::FileSystem::PathPattern::InvalidPattern => error raise Puppet::Module::InvalidFilePattern.new( "The pattern \"#{pattern}\" to find manifests in the module \"#{name}\" " + "is invalid and potentially unsafe.", error) end relative_pattern.prefix_with(@absolute_path_to_manifests) end def subpath(type) File.join(path, type) 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/parser/files.rb b/lib/puppet/parser/files.rb index 81c523ffd..035b21a8c 100644 --- a/lib/puppet/parser/files.rb +++ b/lib/puppet/parser/files.rb @@ -1,137 +1,133 @@ -require 'puppet/module' - module Puppet::Parser::Files module_function # Return a list of manifests as absolute filenames matching the given # pattern. # - # @param pattern [String] A reference for a file in a module. It is the format "/" + # @param pattern [String] A reference for a file in a module. It is the + # format "/" # @param environment [Puppet::Node::Environment] the environment of modules # # @return [Array(String, Array)] the module name and the list of files found # @api private def find_manifests_in_modules(pattern, environment) module_name, file_pattern = split_file_path(pattern) - begin - if mod = environment.module(module_name) - return [mod.name, mod.match_manifests(file_pattern)] - end - rescue Puppet::Module::InvalidName - # one of the modules being loaded might have an invalid name and so - # looking for one might blow up since we load them lazily. + + if mod = environment.module(module_name) + [mod.name, mod.match_manifests(file_pattern)] + else + [nil, []] end - [nil, []] end # Find the path to the given file selector. Files can be selected in # one of two ways: # * absolute path: the path is simply returned # * modulename/filename selector: a file is found in the file directory # of the named module. # # In the second case a nil is returned if there isn't a file found. In the # first case (absolute path), there is no existence check done and so the # path will be returned even if there isn't a file available. # # @param template [String] the file selector # @param environment [Puppet::Node::Environment] the environment in which to search # @return [String, nil] the absolute path to the file or nil if there is no file found # # @api private def find_file(file, environment) if Puppet::Util.absolute_path?(file) file else path, module_file = split_file_path(file) mod = environment.module(path) if module_file && mod mod.file(module_file) else nil end end end # Find the path to the given template selector. Templates can be selected in # a number of ways: # * absolute path: the path is simply returned # * path relative to the templatepath setting: a file is found and the path # is returned # * modulename/filename selector: a file is found in the template directory # of the named module. # # In the last two cases a nil is returned if there isn't a file found. In the # first case (absolute path), there is no existence check done and so the # path will be returned even if there isn't a file available. # # @param template [String] the template selector # @param environment [Puppet::Node::Environment] the environment in which to search # @return [String, nil] the absolute path to the template file or nil if there is no file found # # @api private def find_template(template, environment) if Puppet::Util.absolute_path?(template) template else in_templatepath = find_template_in_templatepath(template, environment) if in_templatepath in_templatepath else find_template_in_module(template, environment) end end end # Templatepaths are deprecated functionality, this will be going away in # Puppet 4. # # @api private def find_template_in_templatepath(template, environment) template_paths = templatepath(environment) if template_paths template_paths.collect do |path| File::join(path, template) end.find do |f| Puppet::FileSystem.exist?(f) end else nil end end # @api private def find_template_in_module(template, environment) path, file = split_file_path(template) mod = environment.module(path) if file && mod mod.template(file) else nil end end # Return an array of paths by splitting the +templatedir+ config # parameter. # @api private def templatepath(environment) dirs = Puppet.settings.value(:templatedir, environment.to_s).split(File::PATH_SEPARATOR) dirs.select do |p| File::directory?(p) end end # Split the path into the module and the rest of the path, or return # nil if the path is empty or absolute (starts with a /). # @api private def split_file_path(path) if path == "" || Puppet::Util.absolute_path?(path) nil else path.split(File::SEPARATOR, 2) end end end diff --git a/spec/unit/parser/files_spec.rb b/spec/unit/parser/files_spec.rb index 020ce740b..5df46d978 100755 --- a/spec/unit/parser/files_spec.rb +++ b/spec/unit/parser/files_spec.rb @@ -1,168 +1,157 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/files' describe Puppet::Parser::Files do include PuppetSpec::Files let(:environment) { Puppet::Node::Environment.create(:testing, []) } before do @basepath = make_absolute("/somepath") end describe "when searching for files" do it "should return fully-qualified files directly" do Puppet::Parser::Files.expects(:modulepath).never Puppet::Parser::Files.find_file(@basepath + "/my/file", environment).should == @basepath + "/my/file" end it "should return the first found file" do mod = mock 'module' mod.expects(:file).returns("/one/mymod/files/myfile") environment.expects(:module).with("mymod").returns mod Puppet::Parser::Files.find_file("mymod/myfile", environment).should == "/one/mymod/files/myfile" end it "should return nil if template is not found" do Puppet::Parser::Files.find_file("foomod/myfile", environment).should be_nil end end describe "when searching for templates" do it "should return fully-qualified templates directly" do Puppet::Parser::Files.expects(:modulepath).never Puppet::Parser::Files.find_template(@basepath + "/my/template", environment).should == @basepath + "/my/template" end it "should return the template from the first found module" do mod = mock 'module' mod.expects(:template).returns("/one/mymod/templates/mytemplate") environment.expects(:module).with("mymod").returns mod Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate" end it "should return the file in the templatedir if it exists" do Puppet[:templatedir] = "/my/templates" Puppet[:modulepath] = "/one:/two" File.stubs(:directory?).returns(true) Puppet::FileSystem.stubs(:exist?).returns(true) Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == File.join(Puppet[:templatedir], "mymod/mytemplate") end it "should not raise an error if no valid templatedir exists and the template exists in a module" do mod = mock 'module' mod.expects(:template).returns("/one/mymod/templates/mytemplate") environment.expects(:module).with("mymod").returns mod Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(nil) Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate" end it "should return unqualified templates if they exist in the template dir" do Puppet::FileSystem.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate" end it "should only return templates if they actually exist" do Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns true Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate" end it "should return nil when asked for a template that doesn't exist" do Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns false Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate", environment).should be_nil end it "should accept relative templatedirs" do Puppet::FileSystem.stubs(:exist?).returns true Puppet[:templatedir] = "my/templates" File.expects(:directory?).with(File.expand_path("my/templates")).returns(true) Puppet::Parser::Files.find_template("mytemplate", environment).should == File.expand_path("my/templates/mytemplate") end it "should use the environment templatedir if no module is found and an environment is specified" do Puppet::FileSystem.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates"]) Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/myenv/templates/mymod/mytemplate" end it "should use first dir from environment templatedir if no module is found and an environment is specified" do Puppet::FileSystem.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates", "/two/templates"]) Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/myenv/templates/mymod/mytemplate" end it "should use a valid dir when templatedir is a path for unqualified templates and the first dir contains template" do Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(true) Puppet::Parser::Files.find_template("mytemplate", environment).should == "/one/templates/mytemplate" end it "should use a valid dir when templatedir is a path for unqualified templates and only second dir contains template" do Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(false) Puppet::FileSystem.expects(:exist?).with("/two/templates/mytemplate").returns(true) Puppet::Parser::Files.find_template("mytemplate", environment).should == "/two/templates/mytemplate" end it "should use the node environment if specified" do mod = mock 'module' environment.expects(:module).with("mymod").returns mod mod.expects(:template).returns("/my/modules/mymod/templates/envtemplate") Puppet::Parser::Files.find_template("mymod/envtemplate", environment).should == "/my/modules/mymod/templates/envtemplate" end it "should return nil if no template can be found" do Puppet::Parser::Files.find_template("foomod/envtemplate", environment).should be_nil end end - describe "when searching for manifests" do - it "should ignore invalid modules" do - mod = mock 'module' - environment.expects(:module).with("mymod").raises(Puppet::Module::InvalidName, "name is invalid") - Puppet.expects(:value).with(:modulepath).never - Dir.stubs(:glob).returns %w{foo} - - Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment) - end - end - describe "when searching for manifests in a module" do def a_module_in_environment(env, name) mod = Puppet::Module.new(name, "/one/#{name}", env) env.stubs(:module).with(name).returns mod mod.stubs(:match_manifests).with("init.pp").returns(["/one/#{name}/manifests/init.pp"]) end it "returns no files when no module is found" do module_name, files = Puppet::Parser::Files.find_manifests_in_modules("not_here_module/foo", environment) expect(files).to be_empty expect(module_name).to be_nil end it "should return the name of the module and the manifests from the first found module" do a_module_in_environment(environment, "mymod") Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment).should == ["mymod", ["/one/mymod/manifests/init.pp"]] end it "does not find the module when it is a different environment" do different_env = Puppet::Node::Environment.create(:different, []) a_module_in_environment(environment, "mymod") Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", different_env).should_not include("mymod") end end end