diff --git a/acceptance/tests/modules/fake_modulepath/apache/metadata.json b/acceptance/tests/modules/fake_modulepath/apache/metadata.json new file mode 100644 index 000000000..87fc9a78a --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/apache/metadata.json @@ -0,0 +1,54 @@ +{ + "name": "puppetlabs-apache", + "dependencies": [ + + ], + "author": "", + "license": "", + "version": "0.0.3", + "checksums": { + "tests/ssl.pp": "191912535199531fd631f911c6329e56", + "manifests/params.pp": "8728cf041cdd94bb0899170eb2b417d9", + "tests/vhost.pp": "1b91e03c8ef89a7ecb6793831ac18399", + "manifests/php.pp": "8a5ca4035b1c22892923f3fde55e3d5e", + "lib/puppet/provider/a2mod/a2mod.rb": "18c5bb180b75a2375e95e07f88a94257", + "tests/php.pp": "ce7bb9eef69d32b42a32ce32d9653625", + "files/httpd": "295f5e924afe6f752d29327e73fe6d0a", + "manifests/dev.pp": "bc54a5af648cb04b7b3bb0e3f7be6543", + "manifests/ssl.pp": "11ed1861298c72cca3a706480bb0b67c", + "files/test.vhost": "0602022c19a7b6b289f218c7b93c1aea", + "tests/init.pp": "4eac4a7ef68499854c54a78879e25535", + "manifests/vhost.pp": "7806a6c098e217da046d0555314756c4", + "lib/puppet/type/a2mod.rb": "0e1b4843431413a10320ac1f6a055d15", + "templates/vhost-default.conf.erb": "ed64a53af0d7bad762176a98c9ea3e62", + "tests/dev.pp": "4cf15c1fecea3ca86009f182b402c7ab", + "tests/apache.pp": "4eac4a7ef68499854c54a78879e25535", + "Modulefile": "9b7a414bf15b06afe2f011068fcaff52", + "manifests/init.pp": "9ef7e081c832bca8f861c3a9feb9949d" + }, + "types": [ + { + "name": "a2mod", + "doc": "Manage Apache 2 modules on Debian and Ubuntu", + "parameters": [ + { + "name": "name", + "doc": "The name of the module to be managed" + } + ], + "providers": [ + { + "name": "a2mod", + "doc": "Manage Apache 2 modules on Debian and Ubuntu Required binaries: ``a2enmod``, ``a2dismod``. Default for ``operatingsystem`` == ``debianubuntu``. " + } + ], + "properties": [ + { + "name": "ensure", + "doc": "The basic property that the resource should be in. Valid values are ``present``, ``absent``." + } + ] + } + ], + "source": "" +} \ No newline at end of file diff --git a/acceptance/tests/modules/fake_modulepath/bacula/metadata.json b/acceptance/tests/modules/fake_modulepath/bacula/metadata.json new file mode 100644 index 000000000..dbc780841 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/bacula/metadata.json @@ -0,0 +1,53 @@ +{ + "description": "This module manages Bacula, a complete backup solution", + "source": "http://github.com/puppetlabs/puppetlabs-bacula", + "checksums": { + "templates/client_config.erb": "5d4005eda5ace78fd90a8cb6dcb388bd", + "spec/spec_helper.rb": "ca19ec4f451ebc7fdb035b52eae6e909", + "templates/bacula-fd.conf.erb": "344fff616e138fbf8cd150f4bab8125d", + "CHANGELOG": "a20043858790de6086b9b30c36806cd2", + "manifests/config/validate.pp": "7d2f9f7cffb2bd1d221eb699ea55f246", + "LICENSE": "26ce13c80c7a8493533c65c32dc29f09", + "manifests/director.pp": "36f34f749314634173b6a47b92a58421", + "manifests/config/client.pp": "5d25cbfd94be2829d9d530673cd2307f", + "lib/puppet/parser/functions/generate_clients.rb": "e09d77f88d18ec3e59bbe7bae4a48036", + "manifests/common.pp": "67391e5560ee7fc97d873ab5c3e2f84c", + "manifests/client.pp": "42e9255711d0a6abed6ab8deafb1248c", + "manifests/console.pp": "4cf8ac2f96f9268e2a3d70ed777797b7", + "README.md": "13d0f1119d510e41c539cbc3825a4f82", + "templates/bacula-sd.conf.erb": "df7987695bf1dd18bb789a2673b2cd95", + "manifests/config.pp": "624a225f85f907e7db5cf717561953f3", + "tests/init.pp": "b95d199119d5e592df7e1580bf23fe06", + "templates/bconsole.conf.erb": "ba5e1ef7d320a48bde6e403dc956952a", + "manifests/bat.pp": "3743bddf0197b4110e54a8e297e927e8", + "manifests/storage.pp": "84f4ec1413df4fdec7288c18d6ba2897", + "templates/bacula-dir.conf.erb": "5ab5aa263cf318ebf65c6b52a0726647", + "metadata.json": "d34d0b70aba36510fbc2df4e667479ef", + "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c", + "Modulefile": "63f93af605871a69211cd3f79ac62582", + "manifests/init.pp": "fafac38161dd69f4b7e452cf910812f4" + }, + "summary": "This module manages a bacula infrastructure", + "author": "Puppet Labs", + "dependencies": [ + { + "version_requirement": ">= 2.2.0", + "name": "puppetlabs/stdlib" + }, + { + "version_requirement": ">= 0.0.1", + "name": "puppetlabs/mysql" + }, + { + "version_requirement": ">= 0.0.1", + "name": "puppetlabs/sqlite" + } + ], + "project_page": "http://github.com/puppetlabs/puppetlabs-bacula", + "types": [ + + ], + "license": "Apache", + "version": "0.0.2", + "name": "puppetlabs-bacula" +} \ No newline at end of file diff --git a/acceptance/tests/modules/fake_modulepath/mysql/metadata.json b/acceptance/tests/modules/fake_modulepath/mysql/metadata.json new file mode 100644 index 000000000..0aa833df3 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/mysql/metadata.json @@ -0,0 +1,140 @@ +{ + "description": "Mysql module", + "source": "git://github.com/puppetlabs/puppetlabs-mysql.git", + "checksums": { + "lib/puppet/provider/database_grant/default.rb": "38a9c5fe0fe1b8474cc2bfd475a225f1", + "manifests/python.pp": "743e5ce2255afa9113a82c5e7fee3740", + "manifests/ruby.pp": "7b57a3321f90c455bccea9de1d57149a", + "manifests/params.pp": "9aeda052d3518d3fcd6e9ee353c899c5", + "lib/puppet/type/database_user.rb": "134269c960f9f751c33e0f023692e256", + "tests/mysql_user.pp": "7b066843d7cdcc54e95ae13ab82ec4f3", + "CHANGELOG": "f2e3e57948da2dcab3bdbe782efd6b11", + "lib/puppet/type/database.rb": "f6ca3a0d053c06752fec999a33c1f5a0", + "templates/my.cnf.erb": "302d55a6dfa368e3957abdd018e0c915", + "manifests/server.pp": "870e294ec504bde5174c203747312f8a", + "LICENSE": "0e5ccf641e613489e66aa98271dbe798", + "templates/my.cnf.pass.erb": "a4952e72bb8aea85a07274c2c1c0334f", + "manifests/server/mysqltuner.pp": "68951b161e11dfce8d93b202d7937704", + "manifests/server/monitor.pp": "76bb559e957086f6bd97ed286f15fd0c", + "lib/puppet/provider/database/mysql.rb": "92bd9124898e9a6258b585085034af4e", + "README": "33f2ef98ed5732170ea12de2598342a5", + "manifests/config.pp": "264b959f3529558050205eae26a61883", + "tests/python.pp": "b093828acfed9c14e25ebdd60d90c282", + "lib/puppet/provider/database/default.rb": "2f4d021abda21e363604403b0e0be231", + "lib/puppet/type/database_grant.rb": "d1b41c45e9c18262310b55170b364c75", + "files/mysqltuner.pl": "de535154b7fb28e437ba412434ea535e", + "tests/init.pp": "6b34827ac4731829c8a117f0b3fb8167", + "manifests/db.pp": "167ab5ec006ad0a9ea6d8a52f554eef5", + "TODO": "88ca4024a37992b46c34cb46e4ac39e6", + "tests/ruby.pp": "6c5071fcaf731995c9b8e31e00eaffa0", + "tests/mysql_database.pp": "2c611d35a1fabe5c418a917391dccade", + "lib/puppet/provider/database_grant/mysql.rb": "43cccef7eaf04b5cf343d2aff9147b99", + "tests/mysql_grant.pp": "106e1671b1f68701778401e4a3fc8d05", + "tests/server.pp": "afa67b373af325b705b49239c7e2efcf", + "lib/puppet/provider/database_user/mysql.rb": "5433dbcc8b596d6a141d0ee31e590f3e", + "lib/puppet/parser/functions/mysql_password.rb": "08aaa14cfbe99ceac1b59053685ee4c0", + "lib/puppet/provider/database_user/default.rb": "31cc564c11b58a23ab694ed17143f70f", + "Modulefile": "49f8c465c58c8841c2c1a98a8ad485dc", + "manifests/init.pp": "ed5175393dfa7da87e75a5f1ebfa21ef" + }, + "summary": "Mysql module", + "author": "PuppetLabs", + "dependencies": [ + { + "version_requirement": ">= 0.0.1", + "name": "bodepd/create_resources" + } + ], + "project_page": "http://github.com/puppetlabs/puppetlabs-mysql", + "types": [ + { + "parameters": [ + { + "doc": "The name of the database.", + "name": "name" + } + ], + "doc": "Manage creation/deletion of a database.", + "providers": [ + { + "doc": "This is a default provider that does nothing. This allows us to install mysql on the same puppet run where we want to use it. ", + "name": "default" + }, + { + "doc": "Create mysql database. Required binaries: `mysql`, `mysqladmin`, `mysqlshow`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database", + "properties": [ + { + "doc": "The basic property that the resource should be in. Valid values are `present`, `absent`.", + "name": "ensure" + }, + { + "doc": "The characterset to use for a database Values can match `/^\\S+$/`.", + "name": "charset" + } + ] + }, + { + "parameters": [ + { + "doc": "The primary key: either user@host for global privilges or user@host/database for database specific privileges", + "name": "name" + } + ], + "doc": "Manage a database user's rights.", + "providers": [ + { + "doc": "Uses mysql as database. ", + "name": "default" + }, + { + "doc": "Uses mysql as database. Required binaries: `mysql`, `mysqladmin`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database_grant", + "properties": [ + { + "doc": "The privileges the user should have. The possible values are implementation dependent.", + "name": "privileges" + } + ] + }, + { + "parameters": [ + { + "doc": "The name of the user. This uses the 'username@hostname' or username@hosname.", + "name": "name" + } + ], + "doc": "Manage a database user. This includes management of users password as well as priveleges", + "providers": [ + { + "doc": "manage users for a mysql database. ", + "name": "default" + }, + { + "doc": "manage users for a mysql database. Required binaries: `mysql`, `mysqladmin`. Default for `kernel` == `Linux`. ", + "name": "mysql" + } + ], + "name": "database_user", + "properties": [ + { + "doc": "The basic property that the resource should be in. Valid values are `present`, `absent`.", + "name": "ensure" + }, + { + "doc": "The password hash of the user. Use mysql_password() for creating such a hash. Values can match `/\\w+/`.", + "name": "password_hash" + } + ] + } + ], + "license": "Apache", + "version": "0.0.0", + "name": "puppetlabs-mysql" +} diff --git a/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json b/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json new file mode 100644 index 000000000..280db67d8 --- /dev/null +++ b/acceptance/tests/modules/fake_modulepath/sqlite/metadata.json @@ -0,0 +1,26 @@ +{ + "description": "This module provides a sqlite class to manage\nthe installation of sqlite on a node. It also provides\na sqlite::db defined type to manage databases on a system", + "source": "https://github.com/puppetlabs/puppetlabs-sqlite/", + "checksums": { + "spec/spec_helper.rb": "ca19ec4f451ebc7fdb035b52eae6e909", + "README.md": "ed04f8ed93d3a6ce19b9153b9444039c", + "tests/init.pp": "e8b321554c2d582e35beb01c57951062", + "manifests/db.pp": "ce94dbfcc3b10738eeec23304898ee78", + "metadata.json": "d34d0b70aba36510fbc2df4e667479ef", + "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c", + "Modulefile": "dda385f94c11e563df1fbe11eeba272d", + "manifests/init.pp": "859cb8ed63863adbaa202c45561280c5" + }, + "summary": "Manage a sqlite installation and databases", + "author": "puppetlabs", + "dependencies": [ + + ], + "project_page": "http://projects.puppetlabs.com/projects/modules/issues", + "types": [ + + ], + "license": "Apache", + "version": "0.0.1.1", + "name": "puppetlabs-sqlite" +} diff --git a/acceptance/tests/modules/list.rb b/acceptance/tests/modules/list.rb new file mode 100644 index 000000000..3cc20730d --- /dev/null +++ b/acceptance/tests/modules/list.rb @@ -0,0 +1,26 @@ +test_name "puppet module list test output and dependency error checking" + +step "Run puppet module list" +expected_stdout = <<-HEREDOC +/opt/puppet-git-repos/puppet/acceptance/tests/modules/fake_modulepath + mysql (0.0.0) + apache (0.0.3) + bacula (0.0.2) + sqlite (0.0.1.1) + HEREDOC + +expected_stderr = <<-HEREDOC +Missing dependency `create_resources`: + `mysql` (0.0.0) requires `bodepd/create_resources` (>= 0.0.1) +Missing dependency `stdlib`: + `bacula` (0.0.2) requires `puppetlabs/stdlib` (>= 2.2.0) +Version dependency mismatch `mysql` (0.0.0): + `bacula` (0.0.2) requires `puppetlabs/mysql` (>= 0.0.1) +Non semantic version dependency `sqlite` (0.0.1.1): + `bacula` (0.0.2) requires `puppetlabs/sqlite` (>= 0.0.1) + HEREDOC + +on master, "puppet module list --modulepath /opt/puppet-git-repos/puppet/acceptance/tests/modules/fake_modulepath" do + assert_match(expected_stdout, stdout, "puppet module list did not output expected stdout") + assert_match(expected_stderr, stderr, "puppet module list did not output expected stderr") +end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb index 772990069..fae406079 100644 --- a/lib/puppet/face/module/list.rb +++ b/lib/puppet/face/module/list.rb @@ -1,64 +1,79 @@ Puppet::Face.define(:module, '1.0.0') do action(:list) do summary "List installed modules" description <<-HEREDOC List puppet modules from a specific environment, specified modulepath or default to listing modules in the default modulepath: #{Puppet.settings[:modulepath]} HEREDOC returns "hash of paths to module objects" option "--env ENVIRONMENT" do summary "Which environments' modules to list" end option "--modulepath MODULEPATH" do summary "Which directories to look for modules in" end examples <<-EOT List installed modules: $ puppet module list /etc/puppet/modules bacula (0.0.2) /usr/share/puppet/modules apache (0.0.3) bacula (0.0.1) List installed modules from a specified environment: $ puppet module list --env 'test' /tmp/puppet/modules rrd (0.0.2) List installed modules from a specified modulepath: $ puppet module list --modulepath /tmp/facts1:/tmp/facts2 /tmp/facts1 stdlib /tmp/facts2 nginx (1.0.0) EOT when_invoked do |options| Puppet[:modulepath] = options[:modulepath] if options[:modulepath] environment = Puppet::Node::Environment.new(options[:env]) environment.modules_by_path end - when_rendering :console do |modules_by_path| + when_rendering :console do |modules_by_path, options| output = '' + + Puppet[:modulepath] = options[:modulepath] if options[:modulepath] + environment = Puppet::Node::Environment.new(options[:env]) + + dependency_errors = false + + environment.modules.each do |mod| + mod.unsatisfied_dependencies.each do |dep_issue| + dependency_errors = true + $stderr.puts dep_issue.to_s + end + end + + output << "\n" if dependency_errors + modules_by_path.each do |path, modules| output << "#{path}\n" modules.each do |mod| version_string = mod.version ? "(#{mod.version})" : '' output << " #{mod.name} #{version_string}\n" end end output end end end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 7f86df83d..5d34ae943 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,203 +1,238 @@ 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 - validate_dependencies 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)) 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 # Find the first 'files' directory. This is used by the XMLRPC fileserver. def file_directory subpath("files") 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].each do |attr| + [: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 validate_dependencies - return unless defined?(@requires) + def unsatisfied_dependencies + return [] unless dependencies - @requires.each do |name, version| - unless mod = environment.module(name) - raise MissingModule, "Missing module #{name} required by #{self.name}" + unsatisfied_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" + unsatisfied_dependencies << 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" + unsatisfied_dependencies << msg + next end - if version and mod.version != version - raise IncompatibleModule, "Required module #{name} is version #{mod.version} but #{self.name} requires #{version}" + 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" + unsatisfied_dependencies << 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" + unsatisfied_dependencies << msg + next + end end end + unsatisfied_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.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; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ + 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/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index e1172497d..0b6ae566f 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,512 +1,648 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' describe Puppet::Module do include PuppetSpec::Files before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory FileTest.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = mock 'module' env.expects(:module).with("mymod").returns "yep" Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should == "yep" end it "should return nil if asked for a named module that doesn't exist" do env = mock 'module' env.expects(:module).with("mymod").returns nil Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should be_nil end it "should support a 'version' attribute" do mod = Puppet::Module.new("mymod") mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod = Puppet::Module.new("mymod") mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod = Puppet::Module.new("mymod") mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod = Puppet::Module.new("mymod") mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod = Puppet::Module.new("mymod") mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod = Puppet::Module.new("mymod") mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod = Puppet::Module.new("mymod") mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end it "should validate that the puppet version is compatible" do mod = Puppet::Module.new("mymod") 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 = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end + describe "when finding unsatisfied dependencies" do + before do + @mod = Puppet::Module.new("mymod") + @mod.stubs(:dependencies).returns [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/foobar" + } + ] + end + + it "should list modules that are missing" do + @mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Missing dependency `foobar`: + `mymod` () requires `baz/foobar` (>= 2.2.0) + HEREDOC + ] + end + + it "should list modules with unsatisfied version" do + foobar = Puppet::Module.new("foobar") + foobar.version = '2.0.0' + @mod.environment.expects(:module).with("foobar").returns foobar + + @mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Version dependency mismatch `foobar` (2.0.0): + `mymod` () requires `baz/foobar` (>= 2.2.0) + HEREDOC + ] + end + + it "should consider a dependency without a version requirement to be satisfied" do + mod = Puppet::Module.new("mymod") + mod.stubs(:dependencies).returns [{ "name" => "baz/foobar" }] + + foobar = Puppet::Module.new("foobar") + mod.environment.expects(:module).with("foobar").returns foobar + + mod.unsatisfied_dependencies.should be_empty + end + + it "should consider a dependency without a version to be unsatisfied" do + foobar = Puppet::Module.new("foobar") + @mod.environment.expects(:module).with("foobar").returns foobar + + @mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Unversioned dependency `foobar`: + `mymod` () requires `baz/foobar` (>= 2.2.0) + HEREDOC + ] + end + + it "should consider a dependency without a semantic version to be unsatisfied" do + foobar = Puppet::Module.new("foobar") + foobar.version = '5.1' + @mod.environment.expects(:module).with("foobar").returns foobar + + @mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Non semantic version dependency `foobar` (5.1): + `mymod` () requires `baz/foobar` (>= 2.2.0) + HEREDOC + ] + end + + it "should consider a dependency requirement without a semantic version to be unsatisfied" do + foobar = Puppet::Module.new("foobar") + foobar.version = '5.1.0' + + mod = Puppet::Module.new("mymod") + mod.stubs(:dependencies).returns [{ "name" => "baz/foobar", "version_requirement" => '> 2.0' }] + mod.environment.expects(:module).with("foobar").returns foobar + + mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Non semantic version dependency `foobar` (5.1.0): + `mymod` () requires `baz/foobar` (> 2.0) + HEREDOC + ] + end + + it "should have valid dependencies when no dependencies have been specified" do + mod = Puppet::Module.new("mymod") + + mod.unsatisfied_dependencies.should == [] + end + + it "should only list unsatisfied dependencies" do + mod = Puppet::Module.new("mymod") + mod.stubs(:dependencies).returns [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/notsatisfied" + } + ] + + satisfied = Puppet::Module.new("satisfied") + satisfied.version = "3.3.0" + + mod.environment.expects(:module).with("satisfied").returns satisfied + mod.environment.expects(:module).with("notsatisfied").returns nil + + mod.unsatisfied_dependencies.should == [<<-HEREDOC.gsub(/^\s{10}/, '') + Missing dependency `notsatisfied`: + `mymod` () requires `baz/notsatisfied` (>= 2.2.0) + HEREDOC + ] + end + + it "should be empty when all dependencies are met" do + mod = Puppet::Module.new("mymod") + mod.stubs(:dependencies).returns [ + { + "version_requirement" => ">= 2.2.0", + "name" => "baz/satisfied" + }, + { + "version_requirement" => "< 2.2.0", + "name" => "baz/alsosatisfied" + } + ] + satisfied = Puppet::Module.new("satisfied") + satisfied.version = "3.3.0" + alsosatisfied = Puppet::Module.new("alsosatisfied") + alsosatisfied.version = "2.1.0" + + mod.environment.expects(:module).with("satisfied").returns satisfied + mod.environment.expects(:module).with("alsosatisfied").returns alsosatisfied + + mod.unsatisfied_dependencies.should be_empty + end + end + describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") mod.supports "solaris" end it "should support specifying a supported platform and version" do mod = Puppet::Module.new("mymod") mod.supports "solaris", 1.0 end it "should fail when not running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" mod.supports "hpux" lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform) end it "should fail when supported platforms are present but of the wrong version" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when no supported platforms have been specified" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") lambda { mod.validate_supported_platform }.should_not raise_error end it "should be considered supported when running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when running on any of multiple supported platforms" do pending "Not sure how to send client platform to the module" end it "should validate its platform support on initialization" do pending "Not sure how to send client platform to the module" end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do Puppet::Module.new("foo").to_s.should == "Module foo" end it "should add the path to its string form if the module is found" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a" mod.to_s.should == "Module foo(/a)" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should convert an environment name into an Environment instance" do Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do env = Puppet::Node::Environment.new Puppet::Module.new("foo").environment.should equal(env) end it "should use any provided Environment instance" do env = Puppet::Node::Environment.new Puppet::Module.new("foo", :environment => env).environment.should equal(env) end describe ".path" do before do dir = tmpdir("deep_path") @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end it "should return the path to the first found instance in its environment's module paths as its path" do modpath = File.join(@first, "foo") FileUtils.mkdir_p(modpath) # Make a second one, which we shouldn't find FileUtils.mkdir_p(File.join(@second, "foo")) mod = Puppet::Module.new("foo") mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path" do modpath = File.join(@second, "foo") FileUtils.mkdir_p(modpath) mod = Puppet::Module.new("foo") mod.should be_exist mod.path.should == modpath end it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do environment = Puppet::Node::Environment.new first_modpath = File.join(@first, "foo") FileUtils.mkdir_p(first_modpath) second_modpath = File.join(@second, "foo") FileUtils.mkdir_p(second_modpath) mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath) mod.path.should == File.join(@second, "foo") mod.environment.should == environment end end it "should be considered existent if it exists in at least one module path" do mod = Puppet::Module.new("foo") mod.expects(:path).returns "/a/foo" mod.should be_exist end it "should be considered nonexistent if it does not exist in any of the module paths" do mod = Puppet::Module.new("foo") mod.expects(:path).returns nil mod.should_not be_exist end [:plugins, :templates, :files, :manifests].each do |filetype| dirname = filetype == :plugins ? "lib" : filetype.to_s it "should be able to return individual #{filetype}" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path end it "should consider #{filetype} to be present if their base directory exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s + "?").should be_false end it "should consider #{filetype} to be absent if the module base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return nil when asked for individual #{filetype} if the module does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" base = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end %w{plugins files}.each do |filetype| short = filetype.sub(/s$/, '') dirname = filetype == "plugins" ? "lib" : filetype.to_s it "should be able to return the #{short} directory" do Puppet::Module.new("foo").should respond_to(short + "_directory") end it "should return the path to the #{short} directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.send(short + "_directory").should == "/a/foo/#{dirname}" end end it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" FileTest.expects(:exist?).with("/a/foo/plugins").returns true mod.plugin_directory.should == "/a/foo/plugins" @logs.first.message.should == "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" @logs.first.level.should == :warning end it "should default to 'lib' for the plugins directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @mod.stubs(:path).returns "/a" @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 @mod.match_manifests(@pq_glob_with_extension).should == %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 @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp}) @mod.match_manifests(nil).should == %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}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end end describe Puppet::Module do before do Puppet::Module.any_instance.stubs(:path).returns "/my/mod/path" @module = Puppet::Module.new("foo") end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "/my/mod/path/License" end it "should return nil as its license file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").license_file.should be_nil end it "should cache the license file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.license_file.should == mod.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "/my/mod/path/metadata.json" end it "should return nil as its metadata file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").metadata_file.should be_nil end it "should cache the metadata file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.metadata_file.should == mod.metadata_file end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do FileTest.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. | | | +-----------------------------------------------------------------------+ */ {}" @module.should_not be_has_metadata end it "should know if it is missing a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should 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") end - describe "when loading the medatada file", :if => Puppet.features.pson? do + describe "when loading the metadata file", :if => Puppet.features.pson? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", - :puppetversion => "0.25" + :puppetversion => "0.25", + :dependencies => [] } @text = @data.to_pson @module = Puppet::Module.new("foo") @module.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns @text end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @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 lambda { @module.load_metadata }.should 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 @module.puppetversion.should == @data[:puppetversion] end it "should fail if the discovered name is different than the metadata name" end end