diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 094609f16..3db89d5ee 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,327 +1,324 @@ 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.each do |dep| - dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0' + dep['version_requirement'] ||= '>= 0.0.0' end end send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.pp' 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_manifest = manifest("init.pp") if !init_manifest.nil? && !searched_manifests.include?(init_manifest) searched_manifests.unshift(init_manifest) end searched_manifests end def all_manifests return [] unless Puppet::FileSystem.exist?(manifests) Dir.glob(File.join(manifests, '**', '*.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 # 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 def ==(other) self.name == other.name && self.version == other.version && self.path == other.path && self.environment == other.environment end private def wanted_manifests_from(pattern) begin extended = File.extname(pattern).empty? ? "#{pattern}.pp" : 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 end diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb index 790a66e04..c154eb182 100644 --- a/lib/puppet/module_tool.rb +++ b/lib/puppet/module_tool.rb @@ -1,194 +1,194 @@ # encoding: UTF-8 # Load standard libraries require 'pathname' require 'fileutils' require 'puppet/util/colors' module Puppet module ModuleTool require 'puppet/module_tool/tar' extend Puppet::Util::Colors # Directory and names that should not be checksummed. ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'checksums.json', 'REVISION'] FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/ REPOSITORY_URL = Puppet.settings[:module_repository] # Is this a directory that shouldn't be checksummed? # # TODO: Should this be part of Checksums? # TODO: Rename this method to reflect its purpose? # TODO: Shouldn't this be used when building packages too? def self.artifact?(path) case File.basename(path) when *ARTIFACTS true else false end end # Return the +username+ and +modname+ for a given +full_module_name+, or raise an # ArgumentError if the argument isn't parseable. def self.username_and_modname_from(full_module_name) if matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN) return matcher.captures else raise ArgumentError, "Not a valid full name: #{full_module_name}" end end # Find the module root when given a path by checking each directory up from # its current location until it finds one that contains a file called # 'Modulefile'. # # @param path [Pathname, String] path to start from # @return [Pathname, nil] the root path of the module directory or nil if # we cannot find one def self.find_module_root(path) path = Pathname.new(path) if path.class == String path.expand_path.ascend do |p| return p if is_module_root?(p) end nil end # Analyse path to see if it is a module root directory by detecting a # file named 'metadata.json' or 'Modulefile' in the directory. # # @param path [Pathname, String] path to analyse # @return [Boolean] true if the path is a module root, false otherwise def self.is_module_root?(path) path = Pathname.new(path) if path.class == String FileTest.file?(path + 'metadata.json') || FileTest.file?(path + 'Modulefile') end # Builds a formatted tree from a list of node hashes containing +:text+ # and +:dependencies+ keys. def self.format_tree(nodes, level = 0) str = '' nodes.each_with_index do |node, i| last_node = nodes.length - 1 == i deps = node[:dependencies] || [] str << (indent = " " * level) str << (last_node ? "└" : "├") str << "─" str << (deps.empty? ? "─" : "┬") str << " #{node[:text]}\n" branch = format_tree(deps, level + 1) branch.gsub!(/^#{indent} /, indent + '│') unless last_node str << branch end return str end def self.build_tree(mods, dir) mods.each do |mod| version_string = mod[:version].to_s.sub(/^(?!v)/, 'v') if mod[:action] == :upgrade previous_version = mod[:previous_version].to_s.sub(/^(?!v)/, 'v') version_string = "#{previous_version} -> #{version_string}" end mod[:text] = "#{mod[:name]} (#{colorize(:cyan, version_string)})" mod[:text] += " [#{mod[:path]}]" unless mod[:path].to_s == dir.to_s deps = (mod[:dependencies] || []) deps.sort! { |a, b| a[:name] <=> b[:name] } build_tree(deps, dir) end end # @param options [Hash] This hash will contain any # command-line arguments that are not Settings, as those will have already # been extracted by the underlying application code. # # @note Unfortunately the whole point of this method is the side effect of # modifying the options parameter. This same hash is referenced both # when_invoked and when_rendering. For this reason, we are not returning # a duplicate. # @todo Validate the above note... # # An :environment_instance and a :target_dir are added/updated in the # options parameter. # # @api private def self.set_option_defaults(options) current_environment = environment_from_options(options) modulepath = [options[:target_dir]] + current_environment.full_modulepath face_environment = current_environment.override_with(:modulepath => modulepath.compact) options[:environment_instance] = face_environment # Note: environment will have expanded the path options[:target_dir] = face_environment.full_modulepath.first end # Given a hash of options, we should discover or create a # {Puppet::Node::Environment} instance that reflects the provided options. # # Generally speaking, the `:modulepath` parameter should supersede all # others, the `:environment` parameter should follow after that, and we # should default to Puppet's current environment. # # @param options [{Symbol => Object}] the options to derive environment from # @return [Puppet::Node::Environment] the environment described by the options def self.environment_from_options(options) if options[:modulepath] path = options[:modulepath].split(File::PATH_SEPARATOR) Puppet::Node::Environment.create(:anonymous, path, '') elsif options[:environment].is_a?(Puppet::Node::Environment) options[:environment] elsif options[:environment] # This use of looking up an environment is correct since it honours # a reguest to get a particular environment via environment name. Puppet.lookup(:environments).get!(options[:environment]) else Puppet.lookup(:current_environment) end end # Handles parsing of module dependency expressions into proper # {Semantic::VersionRange}s, including reasonable error handling. # # @param where [String] a description of the thing we're parsing the # dependency expression for # @param dep [Hash] the dependency description to parse # @return [Array(String, Semantic::VersionRange, String)] an tuple of the # dependent module's name, the version range dependency, and the # unparsed range expression. def self.parse_module_dependency(where, dep) dep_name = dep['name'].tr('/', '-') - range = dep['version_requirement'] || dep['versionRequirement'] || '>= 0.0.0' + range = dep['version_requirement'] || '>= 0.0.0' begin parsed_range = Semantic::VersionRange.parse(range) rescue ArgumentError => e Puppet.debug "Error in #{where} parsing dependency #{dep_name} (#{e.message}); using empty range." parsed_range = Semantic::VersionRange::EMPTY_RANGE end [ dep_name, parsed_range, range ] end end end # Load remaining libraries require 'puppet/module_tool/errors' require 'puppet/module_tool/applications' require 'puppet/module_tool/checksums' require 'puppet/module_tool/contents_description' require 'puppet/module_tool/dependency' require 'puppet/module_tool/metadata' require 'puppet/module_tool/modulefile' require 'puppet/forge/cache' require 'puppet/forge' diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 0574f5bb2..dc3e98639 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,718 +1,684 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/modules' require 'puppet/module_tool/checksums' describe Puppet::Module do include PuppetSpec::Files let(:env) { mock("environment") } let(:path) { "/path" } let(:name) { "mymod" } let(:mod) { Puppet::Module.new(name, path, env) } before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory Puppet::FileSystem.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns "yep" Puppet.override(:environments => Puppet::Environments::Static.new(env)) do expect(Puppet::Module.find(name, "myenv")).to eq("yep") end end it "should return nil if asked for a named module that doesn't exist" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns nil Puppet.override(:environments => Puppet::Environments::Static.new(env)) do expect(Puppet::Module.find(name, "myenv")).to be_nil end end describe "attributes" do it "should support a 'version' attribute" do mod.version = 1.09 expect(mod.version).to eq(1.09) end it "should support a 'source' attribute" do mod.source = "http://foo/bar" expect(mod.source).to eq("http://foo/bar") end it "should support a 'project_page' attribute" do mod.project_page = "http://foo/bar" expect(mod.project_page).to eq("http://foo/bar") end it "should support an 'author' attribute" do mod.author = "Luke Kanies " expect(mod.author).to eq("Luke Kanies ") end it "should support a 'license' attribute" do mod.license = "GPL2" expect(mod.license).to eq("GPL2") end it "should support a 'summary' attribute" do mod.summary = "GPL2" expect(mod.summary).to eq("GPL2") end it "should support a 'description' attribute" do mod.description = "GPL2" expect(mod.description).to eq("GPL2") end it "should support specifying a compatible puppet version" do mod.puppetversion = "0.25" expect(mod.puppetversion).to eq("0.25") end end it "should validate that the puppet version is compatible" do mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" expect { mod.validate_puppet_version }.to raise_error(Puppet::Module::IncompatibleModule) end describe "when finding unmet dependencies" do before do Puppet::FileSystem.unstub(:exist?) @modpath = tmpdir('modpath') Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do metadata_file = "#{@modpath}/needy/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] } ) expect(mod.unmet_dependencies).to eq([{ :reason => :missing, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, :mod_details => { :installed_version => nil } }]) end it "should list modules that are missing and have invalid names" do metadata_file = "#{@modpath}/needy/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar=bar" }] } ) expect(mod.unmet_dependencies).to eq([{ :reason => :missing, :name => "baz/foobar=bar", :version_constraint => ">= 2.2.0", :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, :mod_details => { :installed_version => nil } }]) end it "should list modules with unmet version requirement" do env = Puppet::Node::Environment.create(:testing, [@modpath]) ['test_gte_req', 'test_specific_req', 'foobar'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.stubs(:exist?).with(metadata_file).returns true end mod = PuppetSpec::Modules.create( 'test_gte_req', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] }, :environment => env ) mod2 = PuppetSpec::Modules.create( 'test_specific_req', @modpath, :metadata => { :dependencies => [{ "version_requirement" => "1.0.0", "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' }, :environment => env ) expect(mod.unmet_dependencies).to eq([{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/test_gte_req" }, :mod_details => { :installed_version => "2.0.0" } }]) expect(mod2.unmet_dependencies).to eq([{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => "v1.0.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/test_specific_req" }, :mod_details => { :installed_version => "2.0.0" } }]) end it "should consider a dependency without a version requirement to be satisfied" do env = Puppet::Node::Environment.create(:testing, [@modpath]) mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' }, :environment => env ) expect(mod.unmet_dependencies).to be_empty end it "should consider a dependency without a semantic version to be unmet" do env = Puppet::Node::Environment.create(:testing, [@modpath]) metadata_file = "#{@modpath}/foobar/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).times(3).returns true mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '5.1', :author => 'baz' }, :environment => env ) expect(mod.unmet_dependencies).to eq([{ :reason => :non_semantic_version, :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, :mod_details => { :installed_version => "5.1" }, :name => "baz/foobar", :version_constraint => ">= 0.0.0" }]) end it "should have valid dependencies when no dependencies have been specified" do mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [] } ) expect(mod.unmet_dependencies).to eq([]) end it "should only list unmet dependencies" do env = Puppet::Node::Environment.create(:testing, [@modpath]) [name, 'satisfied'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).twice.returns true end mod = PuppetSpec::Modules.create( name, @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => ">= 2.2.0", "name" => "baz/notsatisfied" } ] }, :environment => env ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' }, :environment => env ) expect(mod.unmet_dependencies).to eq([{ :reason => :missing, :mod_details => { :installed_version => nil }, :parent => { :version => "v9.9.9", :name => "puppetlabs/#{name}" }, :name => "baz/notsatisfied", :version_constraint => ">= 2.2.0" }]) end it "should be empty when all dependencies are met" do env = Puppet::Node::Environment.create(:testing, [@modpath]) mod = PuppetSpec::Modules.create( 'mymod2', @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => "< 2.2.0", "name" => "baz/alsosatisfied" } ] }, :environment => env ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' }, :environment => env ) PuppetSpec::Modules.create( 'alsosatisfied', @modpath, :metadata => { :version => '2.1.0', :author => 'baz' }, :environment => env ) expect(mod.unmet_dependencies).to be_empty end end describe "when managing supported platforms" do it "should support specifying a supported platform" do mod.supports "solaris" end it "should support specifying a supported platform and version" do mod.supports "solaris", 1.0 end end it "should return nil if asked for a module whose name is 'nil'" do expect(Puppet::Module.find(nil, "myenv")).to be_nil end it "should provide support for logging" do expect(Puppet::Module.ancestors).to be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do expect(mod.to_s).to eq("Module #{name}(#{path})") end it "should fail if its name is not alphanumeric" do expect { Puppet::Module.new(".something", "/path", env) }.to raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do expect { Puppet::Module.new }.to raise_error(ArgumentError) end it "should accept an environment at initialization" do expect(Puppet::Module.new("foo", "/path", env).environment).to eq(env) end describe '#modulepath' do it "should return the directory the module is installed in, if a path exists" do mod = Puppet::Module.new("foo", "/a/foo", env) expect(mod.modulepath).to eq('/a') end end [:plugins, :pluginfacts, :templates, :files, :manifests].each do |filetype| case filetype when :plugins dirname = "lib" when :pluginfacts dirname = "facts.d" else dirname = filetype.to_s end it "should be able to return individual #{filetype}" do module_file = File.join(path, dirname, "my/file") Puppet::FileSystem.expects(:exist?).with(module_file).returns true expect(mod.send(filetype.to_s.sub(/s$/, ''), "my/file")).to eq(module_file) end it "should consider #{filetype} to be present if their base directory exists" do module_file = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(module_file).returns true expect(mod.send(filetype.to_s + "?")).to be_truthy end it "should consider #{filetype} to be absent if their base directory does not exist" do module_file = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(module_file).returns false expect(mod.send(filetype.to_s + "?")).to be_falsey end it "should return nil if asked to return individual #{filetype} that don't exist" do module_file = File.join(path, dirname, "my/file") Puppet::FileSystem.expects(:exist?).with(module_file).returns false expect(mod.send(filetype.to_s.sub(/s$/, ''), "my/file")).to be_nil end it "should return the base directory if asked for a nil path" do base = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(base).returns true expect(mod.send(filetype.to_s.sub(/s$/, ''), nil)).to eq(base) end end it "should return the path to the plugin directory" do expect(mod.plugin_directory).to eq(File.join(path, "lib")) end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod", "/a", mock("environment")) @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false expect(@mod.match_manifests(@pq_glob_with_extension)).to eq(%w{foo bar}) end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true expect(@mod.match_manifests(@pq_glob_with_extension)).to eq(%w{foo}) end it "should default to the 'init' file if no glob pattern is specified" do Puppet::FileSystem.expects(:exist?).with("/a/manifests/init.pp").returns(true) expect(@mod.match_manifests(nil)).to eq(%w{/a/manifests/init.pp}) end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) expect(@mod.match_manifests(@pq_glob_with_extension)).to eq(%w{a b}) end it "should match the glob pattern plus '.pp' if no extension is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.pp").returns(%w{yay}) expect(@mod.match_manifests("yay/foo")).to eq(%w{yay}) end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) expect(@mod.match_manifests(@pq_glob_with_extension)).to eq([]) end it "should raise an error if the pattern tries to leave the manifest directory" do expect do @mod.match_manifests("something/../../*") end.to raise_error(Puppet::Module::InvalidFilePattern, 'The pattern "something/../../*" to find manifests in the module "mymod" is invalid and potentially unsafe.') end end describe Puppet::Module do include PuppetSpec::Files before do @modpath = tmpdir('modpath') @module = PuppetSpec::Modules.create('mymod', @modpath) end it "should use 'License' in its current path as its metadata file" do expect(@module.license_file).to eq("#{@modpath}/mymod/License") end it "should cache the license file" do @module.expects(:path).once.returns nil @module.license_file @module.license_file end it "should use 'metadata.json' in its current path as its metadata file" do expect(@module.metadata_file).to eq("#{@modpath}/mymod/metadata.json") end it "should have metadata if it has a metadata file and its data is not empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" expect(@module).to be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" expect(@module).to be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | | ==> DO NOT EDIT THIS FILE! <== | | | | You should edit the `Modulefile` and run `puppet-module build` | | to generate the `metadata.json` file for your releases. | | | +-----------------------------------------------------------------------+ */ {}" expect(@module).not_to be_has_metadata end it "should know if it is missing a metadata file" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns false expect(@module).not_to be_has_metadata end it "should be able to parse its metadata file" do expect(@module).to respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay", "/path", mock("env")) end it "should tolerate failure to parse" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns(my_fixture('trailing-comma.json')) expect(@module.has_metadata?).to be_falsey end def a_module_with_metadata(data) text = data.to_pson mod = Puppet::Module.new("foo", "/path", mock("env")) mod.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns text mod end describe "when loading the metadata file" do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25", :dependencies => [] } @module = a_module_with_metadata(@data) end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata expect(@module.send(attr)).to eq(@data[attr.to_sym]) end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_pson File.stubs(:read).with("/my/file").returns @text expect { @module.load_metadata }.to raise_error( Puppet::Module::MissingMetadata, "No #{attr} module metadata provided for foo" ) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata expect(@module.puppetversion).to eq(@data[:puppetversion]) end - - context "when versionRequirement is used for dependency version info" do - before do - @data = { - :license => "GPL2", - :author => "luke", - :version => "1.0", - :source => "http://foo/", - :puppetversion => "0.25", - :dependencies => [ - { - "versionRequirement" => "0.0.1", - "name" => "pmtacceptance/stdlib" - }, - { - "versionRequirement" => "0.1.0", - "name" => "pmtacceptance/apache" - } - ] - } - @module = a_module_with_metadata(@data) - end - - it "should set the dependency version_requirement key" do - @module.load_metadata - expect(@module.dependencies[0]['version_requirement']).to eq("0.0.1") - end - - it "should set the version_requirement key for all dependencies" do - @module.load_metadata - expect(@module.dependencies[0]['version_requirement']).to eq("0.0.1") - expect(@module.dependencies[1]['version_requirement']).to eq("0.1.0") - end - end end it "should be able to tell if there are local changes" do modpath = tmpdir('modpath') foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8' checksummed_module = PuppetSpec::Modules.create( 'changed', modpath, :metadata => { :checksums => { "foo" => foo_checksum, } } ) foo_path = Pathname.new(File.join(checksummed_module.path, 'foo')) IO.binwrite(foo_path, 'notfoo') expect(Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path)).not_to eq(foo_checksum) IO.binwrite(foo_path, 'foo') expect(Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path)).to eq(foo_checksum) end it "should know what other modules require it" do env = Puppet::Node::Environment.create(:testing, [@modpath]) dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :metadata => {:author => 'puppetlabs'}, :environment => env ) PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "puppetlabs/dependable" }] }, :environment => env ) PuppetSpec::Modules.create( 'wantit', @modpath, :metadata => { :author => 'spoiled', :dependencies => [{ "version_requirement" => "< 5.0.0", "name" => "puppetlabs/dependable" }] }, :environment => env ) expect(dependable.required_by).to match_array([ { "name" => "beggar/needy", "version" => "9.9.9", "version_requirement" => ">= 2.2.0" }, { "name" => "spoiled/wantit", "version" => "9.9.9", "version_requirement" => "< 5.0.0" } ]) end end diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb index fc303ba4a..58e990df8 100755 --- a/spec/unit/module_tool_spec.rb +++ b/spec/unit/module_tool_spec.rb @@ -1,327 +1,320 @@ #! /usr/bin/env ruby # encoding: UTF-8 require 'spec_helper' require 'puppet/module_tool' describe Puppet::ModuleTool do describe '.is_module_root?' do it 'should return true if directory has a Modulefile file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(false) FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(true) expect(subject.is_module_root?(Pathname.new('/a/b/c'))).to be_truthy end it 'should return true if directory has a metadata.json file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(true) expect(subject.is_module_root?(Pathname.new('/a/b/c'))).to be_truthy end it 'should return false if directory does not have a metadata.json or a Modulefile file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(false) FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(false) expect(subject.is_module_root?(Pathname.new('/a/b/c'))).to be_falsey end end describe '.find_module_root' do let(:sample_path) { Pathname.new('/a/b/c').expand_path } it 'should return the first path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?).with(sample_path). returns(true) expect(subject.find_module_root(sample_path)).to eq(sample_path) end it 'should return a parent path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b/c'))).returns(false) Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b'))).returns(true) expect(subject.find_module_root(sample_path)).to eq(Pathname.new('/a/b').expand_path) end it 'should return nil when no module root can be found' do Puppet::ModuleTool.expects(:is_module_root?).at_least_once.returns(false) expect(subject.find_module_root(sample_path)).to be_nil end end describe '.format_tree' do it 'should return an empty tree when given an empty list' do expect(subject.format_tree([])).to eq('') end it 'should return a shallow when given a list without dependencies' do list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ] expect(subject.format_tree(list)).to eq <<-TREE ├── first ├── second └── third TREE end it 'should return a deeply nested tree when given a list with deep dependencies' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, ] expect(subject.format_tree(list)).to eq <<-TREE └─┬ first └─┬ second └── third TREE end it 'should show connectors when deep dependencies are not on the last node of the top level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, { :text => 'fourth' } ] expect(subject.format_tree(list)).to eq <<-TREE ├─┬ first │ └─┬ second │ └── third └── fourth TREE end it 'should show connectors when deep dependencies are not on the last node of any level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] } ] expect(subject.format_tree(list)).to eq <<-TREE └─┬ first ├─┬ second │ └── third └── fourth TREE end it 'should show connectors in every case when deep dependencies are not on the last node' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] }, { :text => 'fifth' } ] expect(subject.format_tree(list)).to eq <<-TREE ├─┬ first │ ├─┬ second │ │ └── third │ └── fourth └── fifth TREE end end describe '.set_option_defaults' do let(:options) { {} } let(:modulepath) { ['/env/module/path', '/global/module/path'] } let(:environment_name) { :current_environment } let(:environment) { Puppet::Node::Environment.create(environment_name, modulepath) } subject do described_class.set_option_defaults(options) options end around do |example| envs = Puppet::Environments::Static.new(environment) Puppet.override(:environments => envs) do example.run end end describe ':environment' do context 'as String' do let(:options) { { :environment => "#{environment_name}" } } it 'assigns the environment with the given name to :environment_instance' do expect(subject).to include :environment_instance => environment end end context 'as Symbol' do let(:options) { { :environment => :"#{environment_name}" } } it 'assigns the environment with the given name to :environment_instance' do expect(subject).to include :environment_instance => environment end end context 'as Puppet::Node::Environment' do let(:env) { Puppet::Node::Environment.create('anonymous', []) } let(:options) { { :environment => env } } it 'assigns the given environment to :environment_instance' do expect(subject).to include :environment_instance => env end end end describe ':modulepath' do let(:options) do { :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR) } end let(:paths) { options[:modulepath].split(File::PATH_SEPARATOR).map { |dir| File.expand_path(dir) } } it 'is expanded to an absolute path' do expect(subject[:environment_instance].full_modulepath).to eql paths end it 'is used to compute :target_dir' do expect(subject).to include :target_dir => paths.first end context 'conflicts with :environment' do let(:options) do { :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR), :environment => environment_name } end it 'replaces the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath).to eql paths end it 'is used to compute :target_dir' do expect(subject).to include :target_dir => paths.first end end end describe ':target_dir' do let(:options) do { :target_dir => 'foo' } end let(:target) { File.expand_path(options[:target_dir]) } it 'is expanded to an absolute path' do expect(subject).to include :target_dir => target end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end context 'conflicts with :modulepath' do let(:options) do { :target_dir => 'foo', :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR) } end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end it 'shares the provided :modulepath via the :environment_instance' do paths = %w[foo] + options[:modulepath].split(File::PATH_SEPARATOR) paths.map! { |dir| File.expand_path(dir) } expect(subject[:environment_instance].full_modulepath).to eql paths end end context 'conflicts with :environment' do let(:options) do { :target_dir => 'foo', :environment => environment_name } end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end it 'shares the provided :modulepath via the :environment_instance' do paths = %w[foo] + environment.full_modulepath paths.map! { |dir| File.expand_path(dir) } expect(subject[:environment_instance].full_modulepath).to eql paths end end context 'when not passed' do it 'is populated with the first component of the modulepath' do expect(subject).to include :target_dir => subject[:environment_instance].full_modulepath.first end end end end describe '.parse_module_dependency' do it 'parses a dependency without a version range expression' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange.parse('>= 0.0.0')) expect(expr).to eql('>= 0.0.0') end it 'parses a dependency with a version range expression' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'version_requirement' => '1.2.x') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange.parse('1.2.x')) expect(expr).to eql('1.2.x') end - it 'parses a dependency with a version range expression in the (deprecated) versionRange key' do - name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'versionRequirement' => '1.2.x') - expect(name).to eql('foo-bar') - expect(range).to eql(Semantic::VersionRange.parse('1.2.x')) - expect(expr).to eql('1.2.x') - end - it 'does not raise an error on invalid version range expressions' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'version_requirement' => 'nope') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange::EMPTY_RANGE) expect(expr).to eql('nope') end end end