diff --git a/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb b/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb new file mode 100644 index 000000000..f763246a0 --- /dev/null +++ b/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb @@ -0,0 +1,23 @@ +begin + test_name "puppet module should not monkey patch puppet-module" + step "Simulate the behavior of puppet-module" + puppet_module = <<-'PUPPET_MODULE' +ruby -e ' +module Puppet + class Module + module Tool + REPOSITORY_URL=1 + end + end +end +require "puppet" +puts Puppet.version +' +PUPPET_MODULE + on(master, puppet_module) do + # If we monkey patch the existing puppet-module Gem then Ruby will issue a + # warning about redefined constants. This is not a comprehensive test but + # it should catch the majority of regressions. + assert_no_match(/warning/, stderr) + end +end diff --git a/lib/puppet/face/module/build.rb b/lib/puppet/face/module/build.rb index eec482ed9..3fcbe275f 100644 --- a/lib/puppet/face/module/build.rb +++ b/lib/puppet/face/module/build.rb @@ -1,37 +1,37 @@ Puppet::Face.define(:module, '1.0.0') do action(:build) do summary "Build a module release package." description <<-EOT Prepares a local module for release on the Puppet Forge by building a ready-to-upload archive file. This action uses the Modulefile in the module directory to set metadata used by the Forge. See for more about writing modulefiles. After being built, the release archive file can be found in the module's `pkg` directory. EOT returns "Pathname object representing the path to the release archive." examples <<-EOT Build a module release: $ puppet module build puppetlabs-apache notice: Building /Users/kelseyhightower/puppetlabs-apache for release puppetlabs-apache/pkg/puppetlabs-apache-0.0.1.tar.gz EOT arguments "" when_invoked do |path, options| - Puppet::Module::Tool::Applications::Builder.run(path, options) + Puppet::ModuleTool::Applications::Builder.run(path, options) end when_rendering :console do |return_value| # Get the string representation of the Pathname object. return_value.to_s end end end diff --git a/lib/puppet/face/module/changes.rb b/lib/puppet/face/module/changes.rb index 602e423dc..9f7bcb41e 100644 --- a/lib/puppet/face/module/changes.rb +++ b/lib/puppet/face/module/changes.rb @@ -1,38 +1,38 @@ Puppet::Face.define(:module, '1.0.0') do action(:changes) do summary "Show modified files of an installed module." description <<-EOT Shows any files in a module that have been modified since it was installed. This action compares the files on disk to the md5 checksums included in the module's metadata. EOT returns "Array of strings representing paths of modified files." examples <<-EOT Show modified files of an installed module: $ puppet module changes /etc/puppet/modules/vcsrepo/ warning: 1 files modified lib/puppet/provider/vcsrepo.rb EOT arguments "" when_invoked do |path, options| - root_path = Puppet::Module::Tool.find_module_root(path) - Puppet::Module::Tool::Applications::Checksummer.run(root_path, options) + root_path = Puppet::ModuleTool.find_module_root(path) + Puppet::ModuleTool::Applications::Checksummer.run(root_path, options) end when_rendering :console do |return_value| if return_value.empty? Puppet.notice "No modified files" else Puppet.warning "#{return_value.size} files modified" end return_value.map do |changed_file| "#{changed_file}" end.join("\n") end end end diff --git a/lib/puppet/face/module/generate.rb b/lib/puppet/face/module/generate.rb index 8f1622cd6..fce1c6257 100644 --- a/lib/puppet/face/module/generate.rb +++ b/lib/puppet/face/module/generate.rb @@ -1,42 +1,42 @@ Puppet::Face.define(:module, '1.0.0') do action(:generate) do summary "Generate boilerplate for a new module." description <<-EOT Generates boilerplate for a new module by creating the directory structure and files recommended for the Puppet community's best practices. A module may need additional directories beyond this boilerplate if it provides plugins, files, or templates. EOT returns "Array of Pathname objects representing paths of generated files." examples <<-EOT Generate a new module in the current directory: $ puppet module generate puppetlabs-ssh notice: Generating module at /Users/kelseyhightower/puppetlabs-ssh puppetlabs-ssh puppetlabs-ssh/tests puppetlabs-ssh/tests/init.pp puppetlabs-ssh/spec puppetlabs-ssh/spec/spec_helper.rb puppetlabs-ssh/spec/spec.opts puppetlabs-ssh/README puppetlabs-ssh/Modulefile puppetlabs-ssh/metadata.json puppetlabs-ssh/manifests puppetlabs-ssh/manifests/init.pp EOT arguments "" when_invoked do |name, options| - Puppet::Module::Tool::Applications::Generator.run(name, options) + Puppet::ModuleTool::Applications::Generator.run(name, options) end when_rendering :console do |return_value| return_value.map {|f| f.to_s }.join("\n") end end end diff --git a/lib/puppet/face/module/install.rb b/lib/puppet/face/module/install.rb index c3e0754eb..db0a0eb91 100644 --- a/lib/puppet/face/module/install.rb +++ b/lib/puppet/face/module/install.rb @@ -1,173 +1,173 @@ # encoding: UTF-8 Puppet::Face.define(:module, '1.0.0') do action(:install) do summary "Install a module from a repository or release archive." description <<-EOT Installs a module from the Puppet Forge, from a release archive file on-disk, or from a private Forge-like repository. The specified module will be installed into the directory specified with the `--target-dir` option, which defaults to #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first}. EOT returns "Pathname object representing the path to the installed module." examples <<-EOT Install a module: $ puppet module install puppetlabs-vcsrepo Preparing to install into /etc/puppet/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /etc/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) Install a module to a specific environment: $ puppet module install puppetlabs-vcsrepo --environment development Preparing to install into /etc/puppet/environments/development/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /etc/puppet/environments/development/modules └── puppetlabs-vcsrepo (v0.0.4) Install a specific module version: $ puppet module install puppetlabs-vcsrepo -v 0.0.4 Preparing to install into /etc/puppet/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /etc/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) Install a module into a specific directory: $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules Preparing to install into /usr/share/puppet/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /usr/share/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) Install a module into a specific directory and check for dependencies in other directories: $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules --modulepath /etc/puppet/modules Preparing to install into /usr/share/puppet/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /usr/share/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) Install a module from a release archive: $ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz Preparing to install into /etc/puppet/modules ... Downloading from http://forge.puppetlabs.com ... Installing -- do not interrupt ... /etc/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) Install a module from a release archive and ignore dependencies: $ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz --ignore-dependencies Preparing to install into /etc/puppet/modules ... Installing -- do not interrupt ... /etc/puppet/modules └── puppetlabs-vcsrepo (v0.0.4) EOT arguments "" option "--force", "-f" do summary "Force overwrite of existing module, if any." description <<-EOT Force overwrite of existing module, if any. EOT end option "--target-dir DIR", "-i DIR" do summary "The directory into which modules are installed." description <<-EOT The directory into which modules are installed; defaults to the first directory in the modulepath. Specifying this option will change the installation directory, and will use the existing modulepath when checking for dependencies. If you wish to check a different set of directories for dependencies, you must also use the `--environment` or `--modulepath` options. EOT end option "--ignore-dependencies" do summary "Do not attempt to install dependencies" description <<-EOT Do not attempt to install dependencies. EOT end option "--modulepath MODULEPATH" do default_to { Puppet.settings[:modulepath] } summary "Which directories to look for modules in" description <<-EOT The list of directories to check for modules. When installing a new module, this setting determines where the module tool will look for its dependencies. If the `--target dir` option is not specified, the first directory in the modulepath will also be used as the install directory. When installing a module into an environment whose modulepath is specified in puppet.conf, you can use the `--environment` option instead, and its modulepath will be used automatically. This setting should be a list of directories separated by the path separator character. (The path separator is `:` on Unix-like platforms and `;` on Windows.) EOT end option "--version VER", "-v VER" do summary "Module version to install." description <<-EOT Module version to install; can be an exact version or a requirement string, eg '>= 1.0.3'. Defaults to latest version. EOT end option "--environment NAME" do default_to { "production" } summary "The target environment to install modules into." description <<-EOT The target environment to install modules into. Only applicable if multiple environments (with different modulepaths) have been configured in puppet.conf. EOT end when_invoked do |name, options| sep = File::PATH_SEPARATOR if options[:target_dir] options[:modulepath] = "#{options[:target_dir]}#{sep}#{options[:modulepath]}" end Puppet.settings[:modulepath] = options[:modulepath] options[:target_dir] = Puppet.settings[:modulepath].split(sep).first Puppet.notice "Preparing to install into #{options[:target_dir]} ..." - Puppet::Module::Tool::Applications::Installer.run(name, options) + Puppet::ModuleTool::Applications::Installer.run(name, options) end when_rendering :console do |return_value, name, options| if return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 else - tree = Puppet::Module::Tool.build_tree(return_value[:installed_modules], return_value[:install_dir]) + tree = Puppet::ModuleTool.build_tree(return_value[:installed_modules], return_value[:install_dir]) return_value[:install_dir] + "\n" + - Puppet::Module::Tool.format_tree(tree) + Puppet::ModuleTool.format_tree(tree) end end end end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb index e3acbd9ae..ebd0fc6cd 100644 --- a/lib/puppet/face/module/list.rb +++ b/lib/puppet/face/module/list.rb @@ -1,285 +1,285 @@ # encoding: UTF-8 Puppet::Face.define(:module, '1.0.0') do action(:list) do summary "List installed modules" description <<-HEREDOC Lists the installed puppet modules. By default, this action scans the modulepath from puppet.conf's `[main]` block; use the --modulepath option to change which directories are scanned. The output of this action includes information from the module's metadata, including version numbers and unmet module dependencies. HEREDOC returns "hash of paths to module objects" option "--environment NAME" do default_to { "production" } summary "Which environments' modules to list" description <<-EOT Which environments' modules to list. EOT end option "--modulepath MODULEPATH" do summary "Which directories to look for modules in" description <<-EOT Which directories to look for modules in; use the system path separator character (`:` on Unix-like systems and `;` on Windows) to specify multiple directories. EOT end option "--tree" do summary "Whether to show dependencies as a tree view" end examples <<-EOT List installed modules: $ puppet module list /etc/puppet/modules ├── bodepd-create_resources (v0.0.1) ├── puppetlabs-bacula (v0.0.2) ├── puppetlabs-mysql (v0.0.1) ├── puppetlabs-sqlite (v0.0.1) └── puppetlabs-stdlib (v2.2.1) /usr/share/puppet/modules (no modules installed) List installed modules in a tree view: $ puppet module list --tree /etc/puppet/modules └─┬ puppetlabs-bacula (v0.0.2) ├── puppetlabs-stdlib (v2.2.1) ├─┬ puppetlabs-mysql (v0.0.1) │ └── bodepd-create_resources (v0.0.1) └── puppetlabs-sqlite (v0.0.1) /usr/share/puppet/modules (no modules installed) List installed modules from a specified environment: $ puppet module list --environment production /etc/puppet/modules ├── bodepd-create_resources (v0.0.1) ├── puppetlabs-bacula (v0.0.2) ├── puppetlabs-mysql (v0.0.1) ├── puppetlabs-sqlite (v0.0.1) └── puppetlabs-stdlib (v2.2.1) /usr/share/puppet/modules (no modules installed) List installed modules from a specified modulepath: $ puppet module list --modulepath /usr/share/puppet/modules /usr/share/puppet/modules (no modules installed) EOT when_invoked do |options| Puppet[:modulepath] = options[:modulepath] if options[:modulepath] environment = Puppet::Node::Environment.new(options[:environment]) environment.modules_by_path end when_rendering :console do |modules_by_path, options| output = '' Puppet[:modulepath] = options[:modulepath] if options[:modulepath] environment = Puppet::Node::Environment.new(options[:production]) error_types = { :non_semantic_version => { :title => "Non semantic version dependency" }, :missing => { :title => "Missing dependency" }, :version_mismatch => { :title => "Module '%s' (v%s) fails to meet some dependencies:" } } @unmet_deps = {} error_types.each_key do |type| @unmet_deps[type] = Hash.new do |hash, key| hash[key] = { :errors => [], :parent => nil } end end # Prepare the unmet dependencies for display on the console. environment.modules.sort_by {|mod| mod.name}.each do |mod| unmet_grouped = Hash.new { |h,k| h[k] = [] } unmet_grouped = mod.unmet_dependencies.inject(unmet_grouped) do |acc, dep| acc[dep[:reason]] << dep acc end unmet_grouped.each do |type, deps| unless deps.empty? unmet_grouped[type].sort_by { |dep| dep[:name] }.each do |dep| dep_name = dep[:name].gsub('/', '-') installed_version = dep[:mod_details][:installed_version] version_constraint = dep[:version_constraint] parent_name = dep[:parent][:name].gsub('/', '-') parent_version = dep[:parent][:version] msg = "'#{parent_name}' (#{parent_version})" msg << " requires '#{dep_name}' (#{version_constraint})" @unmet_deps[type][dep[:name]][:errors] << msg @unmet_deps[type][dep[:name]][:parent] = { :name => dep[:parent][:name], :version => parent_version } @unmet_deps[type][dep[:name]][:version] = installed_version end end end end # Display unmet dependencies by category. error_display_order = [:non_semantic_version, :version_mismatch, :missing] error_display_order.each do |type| unless @unmet_deps[type].empty? @unmet_deps[type].keys.sort_by {|dep| dep }.each do |dep| name = dep.gsub('/', '-') title = error_types[type][:title] errors = @unmet_deps[type][dep][:errors] version = @unmet_deps[type][dep][:version] msg = case type when :version_mismatch title % [name, version] + "\n" when :non_semantic_version title + " '#{name}' (v#{version}):\n" else title + " '#{name}':\n" end errors.each { |error_string| msg << " #{error_string}\n" } Puppet.warning msg.chomp end end end environment.modulepath.each do |path| modules = modules_by_path[path] no_mods = modules.empty? ? ' (no modules installed)' : '' output << "#{path}#{no_mods}\n" if options[:tree] # The modules with fewest things depending on them will be the # parent of the tree. Can't assume to start with 0 dependencies # since dependencies may be cyclical. modules_by_num_requires = modules.sort_by {|m| m.required_by.size} @seen = {} tree = list_build_tree(modules_by_num_requires, [], nil, :label_unmet => true, :path => path, :label_invalid => false) else tree = [] modules.sort_by { |mod| mod.forge_name or mod.name }.each do |mod| tree << list_build_node(mod, path, :label_unmet => false, :path => path, :label_invalid => true) end end - output << Puppet::Module::Tool.format_tree(tree) + output << Puppet::ModuleTool.format_tree(tree) end output end end # Prepare a list of module objects and their dependencies for print in a # tree view. # # Returns an Array of Hashes # # Example: # # [ # { # :text => "puppetlabs-bacula (v0.0.2)", # :dependencies=> [ # { :text => "puppetlabs-stdlib (v2.2.1)", :dependencies => [] }, # { # :text => "puppetlabs-mysql (v1.0.0)" # :dependencies => [ # { # :text => "bodepd-create_resources (v0.0.1)", # :dependencies => [] # } # ] # }, # { :text => "puppetlabs-sqlite (v0.0.1)", :dependencies => [] }, # ] # } # ] # - # When the above data structure is passed to Puppet::Module::Tool.build_tree + # When the above data structure is passed to Puppet::ModuleTool.build_tree # you end up with something like this: # # /etc/puppet/modules # └─┬ puppetlabs-bacula (v0.0.2) # ├── puppetlabs-stdlib (v2.2.1) # ├─┬ puppetlabs-mysql (v1.0.0) # │ └── bodepd-create_resources (v0.0.1) # └── puppetlabs-sqlite (v0.0.1) # def list_build_tree(list, ancestors=[], parent=nil, params={}) list.map do |mod| next if @seen[(mod.forge_name or mod.name)] node = list_build_node(mod, parent, params) @seen[(mod.forge_name or mod.name)] = true unless ancestors.include?(mod) node[:dependencies] ||= [] missing_deps = mod.unmet_dependencies.select do |dep| dep[:reason] == :missing end missing_deps.map do |mis_mod| str = "#{colorize(:bg_red, 'UNMET DEPENDENCY')} #{mis_mod[:name].gsub('/', '-')} " str << "(#{colorize(:cyan, mis_mod[:version_constraint])})" node[:dependencies] << { :text => str } end node[:dependencies] += list_build_tree(mod.dependencies_as_modules, ancestors + [mod], mod, params) end node end.compact end # Prepare a module object for print in a tree view. Each node in the tree # must be a Hash in the following format: # # { :text => "puppetlabs-mysql (v1.0.0)" } # # The value of a module's :text is affected by three (3) factors: the format # of the tree, it's dependency status, and the location in the modulepath # relative to it's parent. # # Returns a Hash # def list_build_node(mod, parent, params) str = '' str << (mod.forge_name ? mod.forge_name.gsub('/', '-') : mod.name) str << ' (' + colorize(:cyan, mod.version ? "v#{mod.version}" : '???') + ')' unless File.dirname(mod.path) == params[:path] str << " [#{File.dirname(mod.path)}]" end if @unmet_deps[:version_mismatch].include?(mod.forge_name) if params[:label_invalid] str << ' ' + colorize(:red, 'invalid') elsif parent.respond_to?(:forge_name) unmet_parent = @unmet_deps[:version_mismatch][mod.forge_name][:parent] if (unmet_parent[:name] == parent.forge_name && unmet_parent[:version] == "v#{parent.version}") str << ' ' + colorize(:red, 'invalid') end end end { :text => str } end end diff --git a/lib/puppet/face/module/search.rb b/lib/puppet/face/module/search.rb index 169f64090..7632f56de 100644 --- a/lib/puppet/face/module/search.rb +++ b/lib/puppet/face/module/search.rb @@ -1,88 +1,88 @@ require 'puppet/util/terminal' Puppet::Face.define(:module, '1.0.0') do action(:search) do summary "Search a repository for a module." description <<-EOT Searches a repository for modules whose names, descriptions, or keywords match the provided search term. EOT returns "Array of module metadata hashes" examples <<-EOT Search the default repository for a module: $ puppet module search puppetlabs NAME DESCRIPTION AUTHOR KEYWORDS bacula This is a generic Apache module @puppetlabs backups EOT arguments "" when_invoked do |term, options| - Puppet::Module::Tool::Applications::Searcher.run(term, options) + Puppet::ModuleTool::Applications::Searcher.run(term, options) end when_rendering :console do |results, term, options| return "No results found for '#{term}'." if results.empty? padding = ' ' headers = { 'full_name' => 'NAME', 'desc' => 'DESCRIPTION', 'author' => 'AUTHOR', 'tag_list' => 'KEYWORDS', } min_widths = Hash[ *headers.map { |k,v| [k, v.length] }.flatten ] min_widths['full_name'] = min_widths['author'] = 12 min_width = min_widths.inject(0) { |sum,pair| sum += pair.last } + (padding.length * (headers.length - 1)) terminal_width = [Puppet::Util::Terminal.width, min_width].max columns = results.inject(min_widths) do |hash, result| { 'full_name' => [ hash['full_name'], result['full_name'].length ].max, 'desc' => [ hash['desc'], result['desc'].length ].max, 'author' => [ hash['author'], "@#{result['author']}".length ].max, 'tag_list' => [ hash['tag_list'], result['tag_list'].join(' ').length ].max, } end flex_width = terminal_width - columns['full_name'] - columns['author'] - (padding.length * (headers.length - 1)) tag_lists = results.map { |r| r['tag_list'] } while (columns['tag_list'] > flex_width / 3) longest_tag_list = tag_lists.sort_by { |tl| tl.join(' ').length }.last break if [ [], [term] ].include? longest_tag_list longest_tag_list.delete(longest_tag_list.sort_by { |t| t == term ? -1 : t.length }.last) columns['tag_list'] = tag_lists.map { |tl| tl.join(' ').length }.max end columns['tag_list'] = [ flex_width / 3, tag_lists.map { |tl| tl.join(' ').length }.max, ].max columns['desc'] = flex_width - columns['tag_list'] format = %w{full_name desc author tag_list}.map do |k| "%-#{ [ columns[k], min_widths[k] ].max }s" end.join(padding) + "\n" highlight = proc do |s| s = s.gsub(term, colorize(:green, term)) s = s.gsub(term.gsub('/', '-'), colorize(:green, term.gsub('/', '-'))) if term =~ /\// s end format % [ headers['full_name'], headers['desc'], headers['author'], headers['tag_list'] ] + results.map do |match| name, desc, author, keywords = %w{full_name desc author tag_list}.map { |k| match[k] } desc = desc[0...(columns['desc'] - 3)] + '...' if desc.length > columns['desc'] highlight[format % [ name.sub('/', '-'), desc, "@#{author}", [keywords].flatten.join(' ') ]] end.join end end end diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb index 1effa6c38..81fc4347c 100644 --- a/lib/puppet/face/module/uninstall.rb +++ b/lib/puppet/face/module/uninstall.rb @@ -1,86 +1,86 @@ Puppet::Face.define(:module, '1.0.0') do action(:uninstall) do summary "Uninstall a puppet module." description <<-EOT Uninstalls a puppet module from the modulepath (or a specific target directory). EOT returns "Hash of module objects representing uninstalled modules and related errors." examples <<-EOT Uninstall a module: $ puppet module uninstall puppetlabs-ssh Removed /etc/puppet/modules/ssh (v1.0.0) Uninstall a module from a specific directory: $ puppet module uninstall puppetlabs-ssh --modulepath /usr/share/puppet/modules Removed /usr/share/puppet/modules/ssh (v1.0.0) Uninstall a module from a specific environment: $ puppet module uninstall puppetlabs-ssh --environment development Removed /etc/puppet/environments/development/modules/ssh (v1.0.0) Uninstall a specific version of a module: $ puppet module uninstall puppetlabs-ssh --version 2.0.0 Removed /etc/puppet/modules/ssh (v2.0.0) EOT arguments "" option "--force", "-f" do summary "Force uninstall of an installed module." description <<-EOT Force the uninstall of an installed module even if there are local changes or the possibility of causing broken dependencies. EOT end option "--environment NAME" do default_to { "production" } summary "The target environment to uninstall modules from." description <<-EOT The target environment to uninstall modules from. EOT end option "--version=" do summary "The version of the module to uninstall" description <<-EOT The version of the module to uninstall. When using this option, a module matching the specified version must be installed or else an error is raised. EOT end option "--modulepath=" do summary "The target directory to search for modules." description <<-EOT The target directory to search for modules. EOT end when_invoked do |name, options| Puppet[:modulepath] = options[:modulepath] if options[:modulepath] name = name.gsub('/', '-') Puppet.notice "Preparing to uninstall '#{name}'" << (options[:version] ? " (#{colorize(:cyan, options[:version].sub(/^(?=\d)/, 'v'))})" : '') << " ..." - Puppet::Module::Tool::Applications::Uninstaller.run(name, options) + Puppet::ModuleTool::Applications::Uninstaller.run(name, options) end when_rendering :console do |return_value| if return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 else mod = return_value[:affected_modules].first "Removed '#{return_value[:module_name]}'" << (mod.version ? " (#{colorize(:cyan, mod.version.to_s.sub(/^(?=\d)/, 'v'))})" : '') << " from #{mod.modulepath}" end end end end diff --git a/lib/puppet/face/module/upgrade.rb b/lib/puppet/face/module/upgrade.rb index e62a7c535..9ad4fcc89 100644 --- a/lib/puppet/face/module/upgrade.rb +++ b/lib/puppet/face/module/upgrade.rb @@ -1,84 +1,84 @@ # encoding: UTF-8 Puppet::Face.define(:module, '1.0.0') do action(:upgrade) do summary "Upgrade a puppet module." description <<-EOT Upgrades a puppet module. EOT returns "Hash" examples <<-EOT upgrade an installed module to the latest version $ puppet module upgrade puppetlabs-apache /etc/puppet/modules └── puppetlabs-apache (v1.0.0 -> v2.4.0) upgrade an installed module to a specific version $ puppet module upgrade puppetlabs-apache --version 2.1.0 /etc/puppet/modules └── puppetlabs-apache (v1.0.0 -> v2.1.0) upgrade an installed module for a specific environment $ puppet module upgrade puppetlabs-apache --environment test /usr/share/puppet/environments/test/modules └── puppetlabs-apache (v1.0.0 -> v2.4.0) EOT arguments "" option "--force", "-f" do summary "Force upgrade of an installed module." description <<-EOT Force the upgrade of an installed module even if there are local changes or the possibility of causing broken dependencies. EOT end option "--ignore-dependencies" do summary "Do not attempt to install dependencies." description <<-EOT Do not attempt to install dependencies EOT end option "--environment NAME" do default_to { "production" } summary "The target environment to search for modules." description <<-EOT The target environment to search for modules. EOT end option "--version=" do summary "The version of the module to upgrade to." description <<-EOT The version of the module to upgrade to. EOT end when_invoked do |name, options| name = name.gsub('/', '-') Puppet.notice "Preparing to upgrade '#{name}' ..." - Puppet::Module::Tool::Applications::Upgrader.new(name, options).run + Puppet::ModuleTool::Applications::Upgrader.new(name, options).run end when_rendering :console do |return_value| if return_value[:result] == :failure Puppet.err(return_value[:error][:multiline]) exit 1 elsif return_value[:result] == :noop Puppet.err(return_value[:error][:multiline]) exit 0 else - tree = Puppet::Module::Tool.build_tree(return_value[:affected_modules], return_value[:base_dir]) + tree = Puppet::ModuleTool.build_tree(return_value[:affected_modules], return_value[:base_dir]) return_value[:base_dir] + "\n" + - Puppet::Module::Tool.format_tree(tree) + Puppet::ModuleTool.format_tree(tree) end end end end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index db27abbc4..04319cc18 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,314 +1,314 @@ require 'puppet/util/logging' require 'semver' require 'puppet/module_tool/applications' # 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, :forge_name attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file return false unless FileTest.exist?(metadata_file) metadata = PSON.parse File.read(metadata_file) return metadata.is_a?(Hash) && !metadata.keys.empty? end def initialize(name, options = {}) @name = name @path = options[:path] assert_validity if options[:environment].is_a?(Puppet::Node::Environment) @environment = options[:environment] else @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? validate_puppet_version end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do return false unless path return false unless FileTest.exist?(subpath(type)) 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) @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) 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 def modulepath File.dirname(path) if path 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 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? - changes = Puppet::Module::Tool::Applications::Checksummer.run(path) + changes = Puppet::ModuleTool::Applications::Checksummer.run(path) !changes.empty? end def local_changes - Puppet::Module::Tool::Applications::Checksummer.run(path) + 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 => e 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 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 #{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/module_tool.rb b/lib/puppet/module_tool.rb index a05ac8e3a..3516ba713 100644 --- a/lib/puppet/module_tool.rb +++ b/lib/puppet/module_tool.rb @@ -1,101 +1,99 @@ # encoding: UTF-8 # Load standard libraries require 'pathname' require 'fileutils' require 'puppet/util/colors' # Define tool module Puppet - class Module - module Tool - extend Puppet::Util::Colors + module ModuleTool + extend Puppet::Util::Colors - # Directory and names that should not be checksummed. - ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'metadata.json', 'REVISION'] - FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/ - REPOSITORY_URL = Puppet.settings[:module_repository] + # Directory and names that should not be checksummed. + ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'metadata.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 it's 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 + # Is this a directory that shouldn't be checksummed? + # + # TODO: Should this be part of Checksums? + # TODO: Rename this method to reflect it's 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 + # 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 - def self.find_module_root(path) - for dir in [path, Dir.pwd].compact - if File.exist?(File.join(dir, 'Modulefile')) - return dir - end + def self.find_module_root(path) + for dir in [path, Dir.pwd].compact + if File.exist?(File.join(dir, 'Modulefile')) + return dir end - raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}" end + raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}" + 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" + # 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] || [] - branch = format_tree(deps, level + 1) - branch.gsub!(/^#{indent} /, indent + '│') unless last_node - str << branch - end + str << (indent = " " * level) + str << (last_node ? "└" : "├") + str << "─" + str << (deps.empty? ? "─" : "┬") + str << " #{node[:text]}\n" - return str + branch = format_tree(deps, level + 1) + branch.gsub!(/^#{indent} /, indent + '│') unless last_node + str << branch end - def self.build_tree(mods, dir) - mods.each do |mod| - version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v') + return str + end - if mod[:action] == :upgrade - previous_version = mod[:previous_version].sub(/^(?!v)/, 'v') - version_string = "#{previous_version} -> #{version_string}" - end + def self.build_tree(mods, dir) + mods.each do |mod| + version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v') - mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})" - mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir - build_tree(mod[:dependencies], dir) + if mod[:action] == :upgrade + previous_version = mod[:previous_version].sub(/^(?!v)/, 'v') + version_string = "#{previous_version} -> #{version_string}" end + + mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})" + mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir + build_tree(mod[:dependencies], dir) end 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/module_tool/skeleton' require 'puppet/forge/cache' require 'puppet/forge' diff --git a/lib/puppet/module_tool/applications.rb b/lib/puppet/module_tool/applications.rb index c3fb85731..c377bc97c 100644 --- a/lib/puppet/module_tool/applications.rb +++ b/lib/puppet/module_tool/applications.rb @@ -1,17 +1,15 @@ -require 'puppet/module' +require 'puppet/module_tool' -class Puppet::Module - module Tool - module Applications - require 'puppet/module_tool/applications/application' - require 'puppet/module_tool/applications/builder' - require 'puppet/module_tool/applications/checksummer' - require 'puppet/module_tool/applications/generator' - require 'puppet/module_tool/applications/installer' - require 'puppet/module_tool/applications/searcher' - require 'puppet/module_tool/applications/unpacker' - require 'puppet/module_tool/applications/uninstaller' - require 'puppet/module_tool/applications/upgrader' - end +module Puppet::ModuleTool + module Applications + require 'puppet/module_tool/applications/application' + require 'puppet/module_tool/applications/builder' + require 'puppet/module_tool/applications/checksummer' + require 'puppet/module_tool/applications/generator' + require 'puppet/module_tool/applications/installer' + require 'puppet/module_tool/applications/searcher' + require 'puppet/module_tool/applications/unpacker' + require 'puppet/module_tool/applications/uninstaller' + require 'puppet/module_tool/applications/upgrader' end end diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb index bce0e84b1..feb0777fd 100644 --- a/lib/puppet/module_tool/applications/application.rb +++ b/lib/puppet/module_tool/applications/application.rb @@ -1,82 +1,82 @@ require 'net/http' require 'semver' require 'puppet/util/colors' -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Application include Puppet::Util::Colors def self.run(*args) new(*args).run end attr_accessor :options def initialize(options = {}) if Puppet.features.microsoft_windows? raise Puppet::Error, "`puppet module` actions are currently not supported on Microsoft Windows" end @options = options end def run raise NotImplementedError, "Should be implemented in child classes." end def discuss(response, success, failure) case response when Net::HTTPOK, Net::HTTPCreated Puppet.notice success else errors = PSON.parse(response.body)['error'] rescue "HTTP #{response.code}, #{response.body}" Puppet.warning "#{failure} (#{errors})" end end def metadata(require_modulefile = false) unless @metadata unless @path raise ArgumentError, "Could not determine module path" end - @metadata = Puppet::Module::Tool::Metadata.new + @metadata = Puppet::ModuleTool::Metadata.new contents = ContentsDescription.new(@path) contents.annotate(@metadata) checksums = Checksums.new(@path) checksums.annotate(@metadata) modulefile_path = File.join(@path, 'Modulefile') if File.file?(modulefile_path) - Puppet::Module::Tool::ModulefileReader.evaluate(@metadata, modulefile_path) + Puppet::ModuleTool::ModulefileReader.evaluate(@metadata, modulefile_path) elsif require_modulefile raise ArgumentError, "No Modulefile found." end end @metadata end def load_modulefile! @metadata = nil metadata(true) end def parse_filename(filename) if match = /^((.*?)-(.*?))-(\d+\.\d+\.\d+.*?)$/.match(File.basename(filename,'.tar.gz')) module_name, author, shortname, version = match.captures else raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})" end unless SemVer.valid?(version) raise ArgumentError, "Invalid version format: #{version} (Semantic Versions are acceptable: http://semver.org)" end return { :module_name => module_name, :author => author, :dir_name => shortname, :version => version } end end end end diff --git a/lib/puppet/module_tool/applications/builder.rb b/lib/puppet/module_tool/applications/builder.rb index 4fb9f26dc..9b9a8d31c 100644 --- a/lib/puppet/module_tool/applications/builder.rb +++ b/lib/puppet/module_tool/applications/builder.rb @@ -1,91 +1,91 @@ require 'fileutils' -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Builder < Application def initialize(path, options = {}) - @path = File.expand_path(Puppet::Module::Tool.find_module_root(path)) + @path = File.expand_path(Puppet::ModuleTool.find_module_root(path)) @pkg_path = File.join(@path, 'pkg') super(options) end def run load_modulefile! create_directory copy_contents add_metadata Puppet.notice "Building #{@path} for release" tar gzip relative = Pathname.new(File.join(@pkg_path, filename('tar.gz'))).relative_path_from(Pathname.new(Dir.pwd)) # Return the Pathname object representing the path to the release # archive just created. This return value is used by the module_tool # face build action, and displayed to on the console using the to_s # method. # # Example return value: # # # relative end private def filename(ext) ext.sub!(/^\./, '') "#{metadata.release_name}.#{ext}" end def tar tar_name = filename('tar') Dir.chdir(@pkg_path) do FileUtils.rm tar_name rescue nil unless system "tar -cf #{tar_name} #{metadata.release_name}" raise RuntimeError, "Could not create #{tar_name}" end end end def gzip Dir.chdir(@pkg_path) do FileUtils.rm filename('tar.gz') rescue nil unless system "gzip #{filename('tar')}" raise RuntimeError, "Could not compress #{filename('tar')}" end end end def create_directory FileUtils.mkdir(@pkg_path) rescue nil if File.directory?(build_path) FileUtils.rm_rf(build_path, :secure => true) end FileUtils.mkdir(build_path) end def copy_contents Dir[File.join(@path, '*')].each do |path| case File.basename(path) - when *Puppet::Module::Tool::ARTIFACTS + when *Puppet::ModuleTool::ARTIFACTS next else FileUtils.cp_r path, build_path end end end def add_metadata File.open(File.join(build_path, 'metadata.json'), 'w') do |f| f.write(PSON.pretty_generate(metadata)) end end def build_path @build_path ||= File.join(@pkg_path, metadata.release_name) end end end end diff --git a/lib/puppet/module_tool/applications/checksummer.rb b/lib/puppet/module_tool/applications/checksummer.rb index f0c3a7130..9f6814731 100644 --- a/lib/puppet/module_tool/applications/checksummer.rb +++ b/lib/puppet/module_tool/applications/checksummer.rb @@ -1,56 +1,56 @@ require 'puppet/module_tool/checksums' -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Checksummer < Application def initialize(path, options = {}) @path = Pathname.new(path) super(options) end def run changes = [] if metadata_file.exist? - sums = Puppet::Module::Tool::Checksums.new(@path) + sums = Puppet::ModuleTool::Checksums.new(@path) (metadata['checksums'] || {}).each do |child_path, canonical_checksum| # Work around an issue where modules built with an older version # of PMT would include the metadata.json file in the list of files # checksummed. This causes metadata.json to always report local # changes. next if File.basename(child_path) == "metadata.json" path = @path + child_path if canonical_checksum != sums.checksum(path) changes << child_path end end else raise ArgumentError, "No metadata.json found." end # Return an Array of strings representing file paths of files that have # been modified since this module was installed. All paths are relative # to the installed module directory. This return value is used by the # module_tool face changes action, and displayed on the console. # # Example return value: # # [ "REVISION", "manifests/init.pp"] # changes end private def metadata PSON.parse(metadata_file.read) end def metadata_file (@path + 'metadata.json') end end end end diff --git a/lib/puppet/module_tool/applications/generator.rb b/lib/puppet/module_tool/applications/generator.rb index 699fe246d..6b5bda98f 100644 --- a/lib/puppet/module_tool/applications/generator.rb +++ b/lib/puppet/module_tool/applications/generator.rb @@ -1,141 +1,141 @@ require 'pathname' require 'fileutils' require 'erb' -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Generator < Application def initialize(full_module_name, options = {}) begin @metadata = Metadata.new(:full_module_name => full_module_name) rescue ArgumentError raise "Could not generate directory #{full_module_name.inspect}, you must specify a dash-separated username and module name." end super(options) end def skeleton @skeleton ||= Skeleton.new end def get_binding binding end def run if destination.directory? raise ArgumentError, "#{destination} already exists." end Puppet.notice "Generating module at #{Dir.pwd}/#{@metadata.dashed_name}" files_created = [] skeleton.path.find do |path| if path == skeleton destination.mkpath else node = Node.on(path, self) if node node.install! files_created << node.target else Puppet.notice "Could not generate from #{path}" end end end # Return an array of Pathname objects representing file paths of files # and directories just generated. This return value is used by the # module_tool face generate action, and displayed on the console. # # Example return value: # # [ # #, # #, # #, # #, # #, # #, # #, # #, # #, # #, # # @filename unless File.exist?(@filename) parsed = parse_filename(@filename) @module_name = parsed[:module_name] @version = parsed[:version] else @source = :repository @module_name = @name.gsub('/', '-') @version = options[:version] end results = { :module_name => @module_name, :module_version => @version, :install_dir => options[:target_dir], } unless File.directory? options[:target_dir] raise MissingInstallDirectoryError, :requested_module => @module_name, :requested_version => @version || 'latest', :directory => options[:target_dir] end cached_paths = get_release_packages unless @graph.empty? Puppet.notice 'Installing -- do not interrupt ...' cached_paths.each do |hash| hash.each do |dir, path| Unpacker.new(path, @options.merge(:target_dir => dir)).run end end end rescue ModuleToolError => err results[:error] = { :oneline => err.message, :multiline => err.multiline, } else results[:result] = :success results[:installed_modules] = @graph ensure results[:result] ||= :failure end results end private - include Puppet::Module::Tool::Shared + include Puppet::ModuleTool::Shared # Return a Pathname object representing the path to the module # release package in the `Puppet.settings[:module_working_dir]`. def get_release_packages get_local_constraints if !@force && @installed.include?(@module_name) raise AlreadyInstalledError, :module_name => @module_name, :installed_version => @installed[@module_name].first.version, :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best), :local_changes => @installed[@module_name].first.local_changes end if @ignore_dependencies && @source == :filesystem @urls = {} @remote = { "#{@module_name}@#{@version}" => { } } @versions = { @module_name => [ { :vstring => @version, :semver => SemVer.new(@version) } ] } else get_remote_constraints end @graph = resolve_constraints({ @module_name => @version }) @graph.first[:tarball] = @filename if @source == :filesystem resolve_install_conflicts(@graph) unless @force # This clean call means we never "cache" the module we're installing, but this # is desired since module authors can easily rerelease modules different content but the same # version number, meaning someone with the old content cached will be very confused as to why # they can't get new content. # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install # but for now this is a quick fix to disable caching Puppet::Forge::Cache.clean download_tarballs(@graph, @graph.last[:path]) end # # Resolve installation conflicts by checking if the requested module # or one of it's dependencies conflicts with an installed module. # # Conflicts occur under the following conditions: # # When installing 'puppetlabs-foo' and an existing directory in the # target install path contains a 'foo' directory and we cannot determine # the "full name" of the installed module. # # When installing 'puppetlabs-foo' and 'pete-foo' is already installed. # This is considered a conflict because 'puppetlabs-foo' and 'pete-foo' # install into the same directory 'foo'. # def resolve_install_conflicts(graph, is_dependency = false) graph.each do |release| @environment.modules_by_path[options[:target_dir]].each do |mod| if mod.has_metadata? metadata = { :name => mod.forge_name.gsub('/', '-'), :version => mod.version } next if release[:module] == metadata[:name] else metadata = nil end if release[:module] =~ /-#{mod.name}$/ dependency_info = { :name => release[:module], :version => release[:version][:vstring] } dependency = is_dependency ? dependency_info : nil latest_version = @versions["#{@module_name}"].sort_by { |h| h[:semver] }.last[:vstring] raise InstallConflictError, :requested_module => @module_name, :requested_version => @version || "latest: v#{latest_version}", :dependency => dependency, :directory => mod.path, :metadata => metadata end resolve_install_conflicts(release[:dependencies], true) end end end # # Check if a file is a vaild module package. # --- # FIXME: Checking for a valid module package should be more robust and # use the acutal metadata contained in the package. 03132012 - Hightower # +++ # def is_module_package?(name) filename = File.expand_path(name) filename =~ /.tar.gz$/ end end end end diff --git a/lib/puppet/module_tool/applications/searcher.rb b/lib/puppet/module_tool/applications/searcher.rb index 923aaf92d..28c78375f 100644 --- a/lib/puppet/module_tool/applications/searcher.rb +++ b/lib/puppet/module_tool/applications/searcher.rb @@ -1,15 +1,15 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Searcher < Application def initialize(term, options = {}) @term = term super(options) end def run Puppet::Forge.search(@term) end end end end diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb index 5ffaecd65..006a88b8e 100644 --- a/lib/puppet/module_tool/applications/uninstaller.rb +++ b/lib/puppet/module_tool/applications/uninstaller.rb @@ -1,107 +1,107 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Uninstaller < Application - include Puppet::Module::Tool::Errors + include Puppet::ModuleTool::Errors def initialize(name, options) @name = name @options = options @errors = Hash.new {|h, k| h[k] = {}} @unfiltered = [] @installed = [] @suggestions = [] @environment = Puppet::Node::Environment.new(options[:environment]) end def run results = { :module_name => @name, :requested_version => @version, } begin find_installed_module validate_module FileUtils.rm_rf(@installed.first.path, :secure => true) results[:affected_modules] = @installed results[:result] = :success rescue ModuleToolError => err results[:error] = { :oneline => err.message, :multiline => err.multiline, } rescue => e results[:error] = { :oneline => e.message, :multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n") } ensure results[:result] ||= :failure end results end private def find_installed_module @environment.modules_by_path.values.flatten.each do |mod| mod_name = (mod.forge_name || mod.name).gsub('/', '-') if mod_name == @name @unfiltered << { :name => mod_name, :version => mod.version, :path => mod.modulepath, } if @options[:version] && mod.version next unless SemVer[@options[:version]].include?(SemVer.new(mod.version)) end @installed << mod elsif mod_name =~ /#{@name}/ @suggestions << mod_name end end if @installed.length > 1 raise MultipleInstalledError, :action => :uninstall, :module_name => @name, :installed_modules => @installed.sort_by { |mod| @environment.modulepath.index(mod.modulepath) } elsif @installed.empty? if @unfiltered.empty? raise NotInstalledError, :action => :uninstall, :suggestions => @suggestions, :module_name => @name else raise NoVersionMatchesError, :installed_modules => @unfiltered.sort_by { |mod| @environment.modulepath.index(mod[:path]) }, :version_range => @options[:version], :module_name => @name end end end def validate_module mod = @installed.first if !@options[:force] && mod.has_metadata? && mod.has_local_changes? raise LocalChangesError, :action => :uninstall, :module_name => (mod.forge_name || mod.name).gsub('/', '-'), :requested_version => @options[:version], :installed_version => mod.version end if !@options[:force] && !mod.required_by.empty? raise ModuleIsRequiredError, :module_name => (mod.forge_name || mod.name).gsub('/', '-'), :required_by => mod.required_by, :requested_version => @options[:version], :installed_version => mod.version end end end end end diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb index 67a76fbcc..e9b0b50d1 100644 --- a/lib/puppet/module_tool/applications/unpacker.rb +++ b/lib/puppet/module_tool/applications/unpacker.rb @@ -1,48 +1,48 @@ require 'pathname' require 'tmpdir' -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Unpacker < Application def initialize(filename, options = {}) @filename = Pathname.new(filename) parsed = parse_filename(filename) super(options) @module_dir = Pathname.new(options[:target_dir]) + parsed[:dir_name] end def run extract_module_to_install_dir # Return the Pathname object representing the directory where the # module release archive was unpacked the to, and the module release # name. @module_dir end private def extract_module_to_install_dir delete_existing_installation_or_abort! build_dir = Puppet::Forge::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}" build_dir.mkpath begin unless system "tar xzf #{@filename} -C #{build_dir}" raise RuntimeError, "Could not extract contents of module archive." end # grab the first directory extracted = build_dir.children.detect { |c| c.directory? } FileUtils.mv extracted, @module_dir ensure build_dir.rmtree end end def delete_existing_installation_or_abort! return unless @module_dir.exist? FileUtils.rm_rf(@module_dir, :secure => true) end end end end diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb index 1a56a6deb..94c1ddc6a 100644 --- a/lib/puppet/module_tool/applications/upgrader.rb +++ b/lib/puppet/module_tool/applications/upgrader.rb @@ -1,109 +1,109 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool module Applications class Upgrader < Application - include Puppet::Module::Tool::Errors + include Puppet::ModuleTool::Errors def initialize(name, options) @action = :upgrade @environment = Puppet::Node::Environment.new(Puppet.settings[:environment]) @module_name = name @options = options @force = options[:force] @ignore_dependencies = options[:force] || options[:ignore_dependencies] @version = options[:version] end def run begin results = { :module_name => @module_name } get_local_constraints if @installed[@module_name].length > 1 raise MultipleInstalledError, :action => :upgrade, :module_name => @module_name, :installed_modules => @installed[@module_name].sort_by { |mod| @environment.modulepath.index(mod.modulepath) } elsif @installed[@module_name].empty? raise NotInstalledError, :action => :upgrade, :module_name => @module_name end @module = @installed[@module_name].last results[:installed_version] = @module.version ? @module.version.sub(/^(?=\d)/, 'v') : nil results[:requested_version] = @version || (@conditions[@module_name].empty? ? :latest : :best) dir = @module.modulepath Puppet.notice "Found '#{@module_name}' (#{colorize(:cyan, results[:installed_version] || '???')}) in #{dir} ..." if !@options[:force] && @module.has_metadata? && @module.has_local_changes? raise LocalChangesError, :action => :upgrade, :module_name => @module_name, :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best), :installed_version => @module.version end begin get_remote_constraints rescue => e raise UnknownModuleError, results.merge(:repository => Puppet::Forge.repository.uri) else raise UnknownVersionError, results.merge(:repository => Puppet::Forge.repository.uri) if @remote.empty? end if !@options[:force] && @versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v') == (@module.version || '0.0.0').sub(/^(?=\d)/, 'v') raise VersionAlreadyInstalledError, :module_name => @module_name, :requested_version => @version || ((@conditions[@module_name].empty? ? 'latest' : 'best') + ": #{@versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v')}"), :installed_version => @installed[@module_name].last.version, :conditions => @conditions[@module_name] + [{ :module => :you, :version => @version }] end @graph = resolve_constraints({ @module_name => @version }) # This clean call means we never "cache" the module we're installing, but this # is desired since module authors can easily rerelease modules different content but the same # version number, meaning someone with the old content cached will be very confused as to why # they can't get new content. # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install # but for now this is a quick fix to disable caching Puppet::Forge::Cache.clean tarballs = download_tarballs(@graph, @graph.last[:path]) unless @graph.empty? Puppet.notice 'Upgrading -- do not interrupt ...' tarballs.each do |hash| hash.each do |dir, path| Unpacker.new(path, @options.merge(:target_dir => dir)).run end end end results[:result] = :success results[:base_dir] = @graph.first[:path] results[:affected_modules] = @graph rescue VersionAlreadyInstalledError => e results[:result] = :noop results[:error] = { :oneline => e.message, :multiline => e.multiline } rescue => e results[:error] = { :oneline => e.message, :multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n") } ensure results[:result] ||= :failure end return results end private - include Puppet::Module::Tool::Shared + include Puppet::ModuleTool::Shared end end end diff --git a/lib/puppet/module_tool/checksums.rb b/lib/puppet/module_tool/checksums.rb index a566090d6..f2c6971b2 100644 --- a/lib/puppet/module_tool/checksums.rb +++ b/lib/puppet/module_tool/checksums.rb @@ -1,52 +1,52 @@ require 'digest/md5' -module Puppet::Module::Tool +module Puppet::ModuleTool # = Checksums # # This class proides methods for generating checksums for data and adding # them to +Metadata+. class Checksums include Enumerable # Instantiate object with string +path+ to create checksums from. def initialize(path) @path = Pathname.new(path) end # Return checksum for the +Pathname+. def checksum(pathname) return Digest::MD5.hexdigest(pathname.read) end # Return checksums for object's +Pathname+, generate if it's needed. # Result is a hash of path strings to checksum strings. def data unless @data @data = {} @path.find do |descendant| - if Puppet::Module::Tool.artifact?(descendant) + if Puppet::ModuleTool.artifact?(descendant) Find.prune elsif descendant.file? path = descendant.relative_path_from(@path) @data[path.to_s] = checksum(descendant) end end end return @data end # TODO: Why? def each(&block) data.each(&block) end # Update +Metadata+'s checksums with this object's. def annotate(metadata) metadata.checksums.replace(data) end # TODO: Move the Checksummer#run checksum checking to here? end end diff --git a/lib/puppet/module_tool/contents_description.rb b/lib/puppet/module_tool/contents_description.rb index 6a590741d..0d19cd3ef 100644 --- a/lib/puppet/module_tool/contents_description.rb +++ b/lib/puppet/module_tool/contents_description.rb @@ -1,82 +1,82 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool # = ContentsDescription # # This class populates +Metadata+'s Puppet type information. class ContentsDescription # Instantiate object for string +module_path+. def initialize(module_path) @module_path = module_path end # Update +Metadata+'s Puppet type information. def annotate(metadata) metadata.types.replace data.clone end # Return types for this module. Result is an array of hashes, each of which # describes a Puppet type. The type description hash structure is: # * :name => Name of this Puppet type. # * :doc => Documentation for this type. # * :properties => Array of hashes representing the type's properties, each # containing :name and :doc. # * :parameters => Array of hashes representing the type's parameters, each # containing :name and :doc. # * :providers => Array of hashes representing the types providers, each # containing :name and :doc. # TODO Write a TypeDescription to encapsulate these structures and logic? def data unless @data @data = [] type_names = [] for module_filename in Dir[File.join(@module_path, "lib/puppet/type/*.rb")] require module_filename type_name = File.basename(module_filename, ".rb") type_names << type_name for provider_filename in Dir[File.join(@module_path, "lib/puppet/provider/#{type_name}/*.rb")] require provider_filename end end type_names.each do |type_name| if type = Puppet::Type.type(type_name.to_sym) type_hash = {:name => type_name, :doc => type.doc} type_hash[:properties] = attr_doc(type, :property) type_hash[:parameters] = attr_doc(type, :param) if type.providers.size > 0 type_hash[:providers] = provider_doc(type) end @data << type_hash else Puppet.warning "Could not find/load type: #{type_name}" end end end @data end # Return an array of hashes representing this +type+'s attrs of +kind+ # (e.g. :param or :property), each containing :name and :doc. def attr_doc(type, kind) [].tap do |attrs| type.allattrs.each do |name| if type.attrtype(name) == kind && name != :provider attrs.push(:name => name, :doc => type.attrclass(name).doc) end end end end # Return an array of hashes representing this +type+'s providers, each # containing :name and :doc. def provider_doc(type) [].tap do |providers| type.providers.sort.each do |prov| providers.push(:name => prov, :doc => type.provider(prov).doc) end end end end end diff --git a/lib/puppet/module_tool/dependency.rb b/lib/puppet/module_tool/dependency.rb index d0eead196..9ebffab2b 100644 --- a/lib/puppet/module_tool/dependency.rb +++ b/lib/puppet/module_tool/dependency.rb @@ -1,24 +1,24 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool class Dependency # Instantiates a new module dependency with a +full_module_name+ (e.g. # "myuser-mymodule"), and optional +version_requirement+ (e.g. "0.0.1") and # optional repository (a URL string). def initialize(full_module_name, version_requirement = nil, repository = nil) @full_module_name = full_module_name # TODO: add error checking, the next line raises ArgumentError when +full_module_name+ is invalid - @username, @name = Puppet::Module::Tool.username_and_modname_from(full_module_name) + @username, @name = Puppet::ModuleTool.username_and_modname_from(full_module_name) @version_requirement = version_requirement @repository = repository ? Puppet::Forge::Repository.new(repository) : nil end # Return PSON representation of this data. def to_pson(*args) result = { :name => @full_module_name } result[:version_requirement] = @version_requirement if @version_requirement && ! @version_requirement.nil? result[:repository] = @repository.to_s if @repository && ! @repository.nil? result.to_pson(*args) end end end diff --git a/lib/puppet/module_tool/errors.rb b/lib/puppet/module_tool/errors.rb index b90e5ba97..b2b36ada6 100644 --- a/lib/puppet/module_tool/errors.rb +++ b/lib/puppet/module_tool/errors.rb @@ -1,9 +1,9 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool module Errors require 'puppet/module_tool/errors/base' require 'puppet/module_tool/errors/installer' require 'puppet/module_tool/errors/uninstaller' require 'puppet/module_tool/errors/upgrader' require 'puppet/module_tool/errors/shared' end end diff --git a/lib/puppet/module_tool/errors/base.rb b/lib/puppet/module_tool/errors/base.rb index 8ec3e7599..14f97fe9e 100644 --- a/lib/puppet/module_tool/errors/base.rb +++ b/lib/puppet/module_tool/errors/base.rb @@ -1,15 +1,15 @@ -module Puppet::Module::Tool::Errors +module Puppet::ModuleTool::Errors class ModuleToolError < StandardError def v(version) (version || '???').to_s.sub(/^(?=\d)/, 'v') end def vstring if @action == :upgrade "#{v(@installed_version)} -> #{v(@requested_version)}" else "#{v(@installed_version || @requested_version)}" end end end end diff --git a/lib/puppet/module_tool/errors/installer.rb b/lib/puppet/module_tool/errors/installer.rb index 48b3b77bf..3ee2f3f82 100644 --- a/lib/puppet/module_tool/errors/installer.rb +++ b/lib/puppet/module_tool/errors/installer.rb @@ -1,90 +1,90 @@ -module Puppet::Module::Tool::Errors +module Puppet::ModuleTool::Errors class InstallError < ModuleToolError; end class AlreadyInstalledError < InstallError def initialize(options) @module_name = options[:module_name] @installed_version = v(options[:installed_version]) @requested_version = v(options[:requested_version]) @local_changes = options[:local_changes] super "'#{@module_name}' (#{@requested_version}) requested; '#{@module_name}' (#{@installed_version}) already installed" end def multiline message = [] message << "Could not install module '#{@module_name}' (#{@requested_version})" message << " Module '#{@module_name}' (#{@installed_version}) is already installed" message << " Installed module has had changes made locally" unless @local_changes.empty? message << " Use `puppet module upgrade` to install a different version" message << " Use `puppet module install --force` to re-install only this module" message.join("\n") end end class InstallConflictError < InstallError def initialize(options) @requested_module = options[:requested_module] @requested_version = v(options[:requested_version]) @dependency = options[:dependency] @directory = options[:directory] @metadata = options[:metadata] super "'#{@requested_module}' (#{@requested_version}) requested; Installation conflict" end def multiline message = [] message << "Could not install module '#{@requested_module}' (#{@requested_version})" if @dependency message << " Dependency '#{@dependency[:name]}' (#{v(@dependency[:version])}) would overwrite #{@directory}" else message << " Installation would overwrite #{@directory}" end if @metadata message << " Currently, '#{@metadata[:name]}' (#{v(@metadata[:version])}) is installed to that directory" end message << " Use `puppet module install --dir ` to install modules elsewhere" if @dependency message << " Use `puppet module install --ignore-dependencies` to install only this module" else message << " Use `puppet module install --force` to install this module anyway" end message.join("\n") end end class MissingPackageError < InstallError def initialize(options) @requested_package = options[:requested_package] super "#{@requested_package} requested; Package #{@requested_package} does not exist" end def multiline <<-MSG.strip Could not install package #{@requested_package} Package #{@requested_package} does not exist MSG end end class MissingInstallDirectoryError < InstallError def initialize(options) @requested_module = options[:requested_module] @requested_version = options[:requested_version] @directory = options[:directory] super "'#{@requested_module}' (#{@requested_version}) requested; Directory #{@directory} does not exist" end def multiline <<-MSG.strip Could not install module '#{@requested_module}' (#{@requested_version}) Directory #{@directory} does not exist MSG end end end diff --git a/lib/puppet/module_tool/errors/shared.rb b/lib/puppet/module_tool/errors/shared.rb index f24d16bfe..fb8aa8e46 100644 --- a/lib/puppet/module_tool/errors/shared.rb +++ b/lib/puppet/module_tool/errors/shared.rb @@ -1,115 +1,115 @@ -module Puppet::Module::Tool::Errors +module Puppet::ModuleTool::Errors class NoVersionsSatisfyError < ModuleToolError def initialize(options) @requested_name = options[:requested_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @dependency_name = options[:dependency_name] @conditions = options[:conditions] @action = options[:action] super "Could not #{@action} '#{@requested_name}' (#{vstring}); module '#{@dependency_name}' cannot satisfy dependencies" end def multiline same_mod = @requested_name == @dependency_name message = [] message << "Could not #{@action} module '#{@requested_name}' (#{vstring})" message << " No version of '#{@dependency_name}' will satisfy dependencies" message << " You specified '#{@requested_name}' (#{v(@requested_version)})" if same_mod message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c| " '#{c[:module]}' (#{v(c[:version])}) requires '#{@dependency_name}' (#{v(c[:dependency])})" end message << " Use `puppet module #{@action} --force` to #{@action} this module anyway" if same_mod message << " Use `puppet module #{@action} --ignore-dependencies` to #{@action} only this module" unless same_mod message.join("\n") end end class InvalidDependencyCycleError < ModuleToolError def initialize(options) @module_name = options[:module_name] @requested_module = options[:requested_module] @requested_version = options[:requested_version] @conditions = options[:conditions] @source = options[:source][1..-1] super "'#{@requested_module}' (#{v(@requested_version)}) requested; Invalid dependency cycle" end def multiline trace = [] trace << "You specified '#{@source.first[:name]}' (#{v(@requested_version)})" trace += @source[1..-1].map { |m| "which depends on '#{m[:name]}' (#{v(m[:version])})" } message = [] message << "Could not install module '#{@requested_module}' (#{v(@requested_version)})" message << " No version of '#{@module_name}' will satisfy dependencies" message << trace.map { |s| " #{s}" }.join(",\n") message << " Use `puppet module install --force` to install this module anyway" message.join("\n") end end class NotInstalledError < ModuleToolError def initialize(options) @module_name = options[:module_name] @suggestions = options[:suggestions] || [] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module is not installed" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}'" message << " Module '#{@module_name}' is not installed" message += @suggestions.map do |suggestion| " You may have meant `puppet module #{@action} #{suggestion}`" end message << " Use `puppet module install` to install this module" if @action == :upgrade message.join("\n") end end class MultipleInstalledError < ModuleToolError def initialize(options) @module_name = options[:module_name] @modules = options[:installed_modules] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module appears in multiple places in the module path" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}'" message << " Module '#{@module_name}' appears multiple places in the module path" message += @modules.map do |mod| " '#{@module_name}' (#{v(mod.version)}) was found in #{mod.modulepath}" end message << " Use the `--modulepath` option to limit the search to specific directories" message.join("\n") end end class LocalChangesError < ModuleToolError def initialize(options) @module_name = options[:module_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @action = options[:action] super "Could not #{@action} '#{@module_name}'; module is not installed" end def multiline message = [] message << "Could not #{@action} module '#{@module_name}' (#{vstring})" message << " Installed module has had changes made locally" message << " Use `puppet module #{@action} --force` to #{@action} this module anyway" message.join("\n") end end end diff --git a/lib/puppet/module_tool/errors/uninstaller.rb b/lib/puppet/module_tool/errors/uninstaller.rb index 3beee7872..18809cb6c 100644 --- a/lib/puppet/module_tool/errors/uninstaller.rb +++ b/lib/puppet/module_tool/errors/uninstaller.rb @@ -1,45 +1,45 @@ -module Puppet::Module::Tool::Errors +module Puppet::ModuleTool::Errors class UninstallError < ModuleToolError; end class NoVersionMatchesError < UninstallError def initialize(options) @module_name = options[:module_name] @modules = options[:installed_modules] @version = options[:version_range] super "Could not uninstall '#{@module_name}'; no installed version matches" end def multiline message = [] message << "Could not uninstall module '#{@module_name}' (#{v(@version)})" message << " No installed version of '#{@module_name}' matches (#{v(@version)})" message += @modules.map do |mod| " '#{mod[:name]}' (#{v(mod[:version])}) is installed in #{mod[:path]}" end message.join("\n") end end class ModuleIsRequiredError < UninstallError def initialize(options) @module_name = options[:module_name] @required_by = options[:required_by] @requested_version = options[:requested_version] @installed_version = options[:installed_version] super "Could not uninstall '#{@module_name}'; installed modules still depend upon it" end def multiline message = [] message << ("Could not uninstall module '#{@module_name}'" << (@requested_version ? " (#{v(@requested_version)})" : '')) message << " Other installed modules have dependencies on '#{@module_name}' (#{v(@installed_version)})" message += @required_by.map do |mod| " '#{mod['name']}' (#{v(mod['version'])}) requires '#{@module_name}' (#{v(mod['version_requirement'])})" end message << " Use `puppet module uninstall --force` to uninstall this module anyway" message.join("\n") end end end diff --git a/lib/puppet/module_tool/errors/upgrader.rb b/lib/puppet/module_tool/errors/upgrader.rb index abc8375b8..43893856a 100644 --- a/lib/puppet/module_tool/errors/upgrader.rb +++ b/lib/puppet/module_tool/errors/upgrader.rb @@ -1,72 +1,72 @@ -module Puppet::Module::Tool::Errors +module Puppet::ModuleTool::Errors class UpgradeError < ModuleToolError def initialize(msg) @action = :upgrade super end end class VersionAlreadyInstalledError < UpgradeError def initialize(options) @module_name = options[:module_name] @requested_version = options[:requested_version] @installed_version = options[:installed_version] @dependency_name = options[:dependency_name] @conditions = options[:conditions] super "Could not upgrade '#{@module_name}'; module is not installed" end def multiline message = [] message << "Could not upgrade module '#{@module_name}' (#{vstring})" if @conditions.length == 1 && @conditions.last[:version].nil? message << " The installed version is already the latest version" else message << " The installed version is already the best fit for the current dependencies" message += @conditions.select { |c| c[:module] == :you && c[:version] }.map do |c| " You specified '#{@module_name}' (#{v(c[:version])})" end message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c| " '#{c[:module]}' (#{v(c[:version])}) requires '#{@module_name}' (#{v(c[:dependency])})" end end message << " Use `puppet module install --force` to re-install this module" message.join("\n") end end class UnknownModuleError < UpgradeError def initialize(options) @module_name = options[:module_name] @installed_version = options[:installed_version] @requested_version = options[:requested_version] @repository = options[:repository] super "Could not upgrade '#{@module_name}'; module is unknown to #{@repository}" end def multiline message = [] message << "Could not upgrade module '#{@module_name}' (#{vstring})" message << " Module '#{@module_name}' does not exist on #{@repository}" message.join("\n") end end class UnknownVersionError < UpgradeError def initialize(options) @module_name = options[:module_name] @installed_version = options[:installed_version] @requested_version = options[:requested_version] @repository = options[:repository] super "Could not upgrade '#{@module_name}' (#{vstring}); module has no versions #{ @requested_version && "matching #{v(@requested_version)} "}published on #{@repository}" end def multiline message = [] message << "Could not upgrade module '#{@module_name}' (#{vstring})" message << " No version matching '#{@requested_version || ">= 0.0.0"}' exists on #{@repository}" message.join("\n") end end end diff --git a/lib/puppet/module_tool/metadata.rb b/lib/puppet/module_tool/metadata.rb index a84100a10..73868a2ad 100644 --- a/lib/puppet/module_tool/metadata.rb +++ b/lib/puppet/module_tool/metadata.rb @@ -1,141 +1,141 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool # = Metadata # # This class provides a data structure representing a module's metadata. # It provides some basic parsing, but other data is injected into it using # +annotate+ methods in other classes. class Metadata # The full name of the module, which is a dash-separated combination of the # +username+ and module +name+. attr_reader :full_module_name # The name of the user that owns this module. attr_reader :username # The name of this module. See also +full_module_name+. attr_reader :name # The version of this module. attr_reader :version # Instantiate from a hash, whose keys are setters in this class. def initialize(settings={}) settings.each do |key, value| send("#{key}=", value) end end # Set the full name of this module, and from it, the +username+ and # module +name+. def full_module_name=(full_module_name) @full_module_name = full_module_name - @username, @name = Puppet::Module::Tool::username_and_modname_from(full_module_name) + @username, @name = Puppet::ModuleTool::username_and_modname_from(full_module_name) end # Return an array of the module's Dependency objects. def dependencies return @dependencies ||= [] end def author @author || @username end def author=(author) @author = author end def source @source || 'UNKNOWN' end def source=(source) @source = source end def license @license || 'Apache License, Version 2.0' end def license=(license) @license = license end def summary @summary || 'UNKNOWN' end def summary=(summary) @summary = summary end def description @description || 'UNKNOWN' end def description=(description) @description = description end def project_page @project_page || 'UNKNOWN' end def project_page=(project_page) @project_page = project_page end # Return an array of the module's Puppet types, each one is a hash # containing :name and :doc. def types return @types ||= [] end # Return module's file checksums. def checksums return @checksums ||= {} end # Return the dashed name of the module, which may either be the # dash-separated combination of the +username+ and module +name+, or just # the module +name+. def dashed_name return [@username, @name].compact.join('-') end # Return the release name, which is the combination of the +dashed_name+ # of the module and its +version+ number. def release_name return [dashed_name, @version].join('-') end # Set the version of this module, ensure a string like '0.1.0' see the # Semantic Versions here: http://semver.org def version=(version) if SemVer.valid?(version) @version = version else raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)" end end # Return the PSON record representing this instance. def to_pson(*args) return { :name => @full_module_name, :version => @version, :source => source, :author => author, :license => license, :summary => summary, :description => description, :project_page => project_page, :dependencies => dependencies, :types => types, :checksums => checksums }.to_pson(*args) end end end diff --git a/lib/puppet/module_tool/modulefile.rb b/lib/puppet/module_tool/modulefile.rb index 4e91b2ab9..4b50f1160 100644 --- a/lib/puppet/module_tool/modulefile.rb +++ b/lib/puppet/module_tool/modulefile.rb @@ -1,75 +1,75 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool # = Modulefile # # This class provides the DSL used for evaluating the module's 'Modulefile'. # These methods are used to concisely define this module's attributes, which # are later rendered as PSON into a 'metadata.json' file. class ModulefileReader # Read the +filename+ and eval its Ruby code to set values in the Metadata # +metadata+ instance. def self.evaluate(metadata, filename) builder = new(metadata) if File.file?(filename) builder.instance_eval(File.read(filename.to_s), filename.to_s, 1) else Puppet.warning "No Modulefile: #{filename}" end return builder end # Instantiate with the Metadata +metadata+ instance. def initialize(metadata) @metadata = metadata end # Set the +full_module_name+ (e.g. "myuser-mymodule"), which will also set the # +username+ and module +name+. Required. def name(name) @metadata.full_module_name = name end # Set the module +version+ (e.g., "0.0.1"). Required. def version(version) @metadata.version = version end # Add a dependency with the full_module_name +name+ (e.g. "myuser-mymodule"), an # optional +version_requirement+ (e.g. "0.0.1") and +repository+ (a URL # string). Optional. Can be called multiple times to add many dependencies. def dependency(name, version_requirement = nil, repository = nil) @metadata.dependencies << Dependency.new(name, version_requirement, repository) end # Set the source def source(source) @metadata.source = source end # Set the author or default to +username+ def author(author) @metadata.author = author end # Set the license def license(license) @metadata.license = license end # Set the summary def summary(summary) @metadata.summary = summary end # Set the description def description(description) @metadata.description = description end # Set the project page def project_page(project_page) @metadata.project_page = project_page end end end diff --git a/lib/puppet/module_tool/shared_behaviors.rb b/lib/puppet/module_tool/shared_behaviors.rb index dae588e34..b7821fc91 100644 --- a/lib/puppet/module_tool/shared_behaviors.rb +++ b/lib/puppet/module_tool/shared_behaviors.rb @@ -1,161 +1,161 @@ -module Puppet::Module::Tool::Shared +module Puppet::ModuleTool::Shared - include Puppet::Module::Tool::Errors + include Puppet::ModuleTool::Errors def get_local_constraints @local = Hash.new { |h,k| h[k] = { } } @conditions = Hash.new { |h,k| h[k] = [] } @installed = Hash.new { |h,k| h[k] = [] } @environment.modules_by_path.values.flatten.each do |mod| mod_name = (mod.forge_name || mod.name).gsub('/', '-') @installed[mod_name] << mod d = @local["#{mod_name}@#{mod.version}"] (mod.dependencies || []).each do |hash| name, conditions = hash['name'], hash['version_requirement'] name = name.gsub('/', '-') d[name] = conditions @conditions[name] << { :module => mod_name, :version => mod.version, :dependency => conditions } end end end def get_remote_constraints @remote = Hash.new { |h,k| h[k] = { } } @urls = {} @versions = Hash.new { |h,k| h[k] = [] } Puppet.notice "Downloading from #{Puppet::Forge.repository.uri} ..." - author, modname = Puppet::Module::Tool.username_and_modname_from(@module_name) + author, modname = Puppet::ModuleTool.username_and_modname_from(@module_name) info = Puppet::Forge.remote_dependency_info(author, modname, @options[:version]) info.each do |pair| mod_name, releases = pair mod_name = mod_name.gsub('/', '-') releases.each do |rel| semver = SemVer.new(rel['version'] || '0.0.0') rescue SemVer.MIN @versions[mod_name] << { :vstring => rel['version'], :semver => semver } @versions[mod_name].sort! { |a, b| a[:semver] <=> b[:semver] } @urls["#{mod_name}@#{rel['version']}"] = rel['file'] d = @remote["#{mod_name}@#{rel['version']}"] (rel['dependencies'] || []).each do |name, conditions| d[name.gsub('/', '-')] = conditions end end end end def implicit_version(mod) return :latest if @conditions[mod].empty? if @conditions[mod].all? { |c| c[:queued] || c[:module] == :you } return :latest end return :best end def annotated_version(mod, versions) if versions.empty? return implicit_version(mod) else return "#{implicit_version(mod)}: #{versions.last}" end end def resolve_constraints(dependencies, source = [{:name => :you}], seen = {}, action = @action) dependencies = dependencies.map do |mod, range| source.last[:dependency] = range @conditions[mod] << { :module => source.last[:name], :version => source.last[:version], :dependency => range, :queued => true } if @force range = SemVer[@version] rescue SemVer['>= 0.0.0'] else range = (@conditions[mod]).map do |r| SemVer[r[:dependency]] rescue SemVer['>= 0.0.0'] end.inject(&:&) end if @action == :install && seen.include?(mod) next if range === seen[mod][:semver] req_module = @module_name req_versions = @versions["#{@module_name}"].map { |v| v[:semver] } raise InvalidDependencyCycleError, :module_name => mod, :source => (source + [{ :name => mod, :version => source.last[:dependency] }]), :requested_module => req_module, :requested_version => @version || annotated_version(req_module, req_versions), :conditions => @conditions end if !(@force || @installed[mod].empty? || source.last[:name] == :you) next if range === SemVer.new(@installed[mod].first.version) action = :upgrade elsif @installed[mod].empty? action = :install end if action == :upgrade @conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } } end valid_versions = @versions["#{mod}"].select { |h| range === h[:semver] } unless version = valid_versions.last req_module = @module_name req_versions = @versions["#{@module_name}"].map { |v| v[:semver] } raise NoVersionsSatisfyError, :requested_name => req_module, :requested_version => @version || annotated_version(req_module, req_versions), :installed_version => @installed[@module_name].empty? ? nil : @installed[@module_name].first.version, :dependency_name => mod, :conditions => @conditions[mod], :action => @action end seen[mod] = version { :module => mod, :version => version, :action => action, :previous_version => @installed[mod].empty? ? nil : @installed[mod].first.version, :file => @urls["#{mod}@#{version[:vstring]}"], :path => action == :install ? @options[:target_dir] : (@installed[mod].empty? ? @options[:target_dir] : @installed[mod].first.modulepath), :dependencies => [] } end.compact dependencies.each do |mod| deps = @remote["#{mod[:module]}@#{mod[:version][:vstring]}"].sort_by(&:first) mod[:dependencies] = resolve_constraints(deps, source + [{ :name => mod[:module], :version => mod[:version][:vstring] }], seen, :install) end unless @ignore_dependencies return dependencies end def download_tarballs(graph, default_path) graph.map do |release| begin if release[:tarball] cache_path = Pathname(release[:tarball]) else cache_path = Puppet::Forge.repository.retrieve(release[:file]) end rescue OpenURI::HTTPError => e raise RuntimeError, "Could not download module: #{e.message}" end [ { (release[:path] ||= default_path) => cache_path}, *download_tarballs(release[:dependencies], default_path) ] end.flatten end end diff --git a/lib/puppet/module_tool/skeleton.rb b/lib/puppet/module_tool/skeleton.rb index 4c41c7611..33f92636e 100644 --- a/lib/puppet/module_tool/skeleton.rb +++ b/lib/puppet/module_tool/skeleton.rb @@ -1,34 +1,34 @@ -module Puppet::Module::Tool +module Puppet::ModuleTool # = Skeleton # # This class provides methods for finding templates for the 'generate' action. class Skeleton # TODO Review whether the 'freeze' feature should be fixed or deleted. # def freeze! # FileUtils.rm_fr custom_path rescue nil # FileUtils.cp_r default_path, custom_path # end # Return Pathname with 'generate' templates. def path paths.detect { |path| path.directory? } end # Return Pathnames to look for 'generate' templates. def paths @paths ||= [ custom_path, default_path ] end # Return Pathname of custom templates directory. def custom_path Pathname(Puppet.settings[:module_working_dir]) + 'skeleton' end # Return Pathname of default template directory. def default_path Pathname(__FILE__).dirname + 'skeleton/templates/generator' end end end diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb index 9f67800a4..33101d338 100644 --- a/spec/unit/face/module/install_spec.rb +++ b/spec/unit/face/module/install_spec.rb @@ -1,158 +1,158 @@ require 'spec_helper' require 'puppet/face' require 'puppet/module_tool' describe "puppet module install" do subject { Puppet::Face[:module, :current] } let(:options) do {} end describe "option validation" do before do Puppet.settings[:modulepath] = fakemodpath end let(:expected_options) do { :target_dir => fakefirstpath, :modulepath => fakemodpath, :environment => 'production' } end let(:sep) { File::PATH_SEPARATOR } let(:fakefirstpath) { "/my/fake/modpath" } let(:fakesecondpath) { "/other/fake/path" } let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" } let(:fakedirpath) { "/my/fake/path" } context "without any options" do it "should require a name" do pattern = /wrong number of arguments/ expect { subject.install }.to raise_error ArgumentError, pattern end it "should not require any options" do - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once subject.install("puppetlabs-apache") end end it "should accept the --force option" do options[:force] = true expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once subject.install("puppetlabs-apache", options) end it "should accept the --target-dir option" do options[:target_dir] = "/foo/puppet/modules" expected_options.merge!(options) expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}" - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once subject.install("puppetlabs-apache", options) end it "should accept the --version option" do options[:version] = "0.0.1" expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once subject.install("puppetlabs-apache", options) end it "should accept the --ignore-dependencies option" do options[:ignore_dependencies] = true expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once subject.install("puppetlabs-apache", options) end describe "when modulepath option is passed" do let(:expected_options) { { :modulepath => fakemodpath, :environment => 'production' } } let(:options) { { :modulepath => fakemodpath } } describe "when target-dir option is not passed" do it "should set target-dir to be first path from modulepath" do expected_options[:target_dir] = fakefirstpath - Puppet::Module::Tool::Applications::Installer. + Puppet::ModuleTool::Applications::Installer. expects(:run). with("puppetlabs-apache", expected_options) Puppet::Face[:module, :current].install("puppetlabs-apache", options) Puppet.settings[:modulepath].should == fakemodpath end end describe "when target-dir option is passed" do it "should set target-dir to be first path of modulepath" do options[:target_dir] = fakedirpath expected_options[:target_dir] = fakedirpath expected_options[:modulepath] = "#{fakedirpath}#{sep}#{fakemodpath}" - Puppet::Module::Tool::Applications::Installer. + Puppet::ModuleTool::Applications::Installer. expects(:run). with("puppetlabs-apache", expected_options) Puppet::Face[:module, :current].install("puppetlabs-apache", options) Puppet.settings[:modulepath].should == "#{fakedirpath}#{sep}#{fakemodpath}" end end end describe "when modulepath option is not passed" do before do Puppet.settings[:modulepath] = fakemodpath end describe "when target-dir option is not passed" do it "should set target-dir to be first path of default mod path" do expected_options[:target_dir] = fakefirstpath expected_options[:modulepath] = fakemodpath - Puppet::Module::Tool::Applications::Installer. + Puppet::ModuleTool::Applications::Installer. expects(:run). with("puppetlabs-apache", expected_options) Puppet::Face[:module, :current].install("puppetlabs-apache", options) end end describe "when target-dir option is passed" do it "should prepend target-dir to modulepath" do options[:target_dir] = fakedirpath expected_options[:target_dir] = fakedirpath expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}" - Puppet::Module::Tool::Applications::Installer. + Puppet::ModuleTool::Applications::Installer. expects(:run). with("puppetlabs-apache", expected_options) Puppet::Face[:module, :current].install("puppetlabs-apache", options) Puppet.settings[:modulepath].should == expected_options[:modulepath] end end end end describe "inline documentation" do subject { Puppet::Face[:module, :current].get_action :install } its(:summary) { should =~ /install.*module/im } its(:description) { should =~ /install.*module/im } its(:returns) { should =~ /pathname/i } its(:examples) { should_not be_empty } %w{ license copyright summary description returns examples }.each do |doc| context "of the" do its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } end end end end diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb index 51f62bd1f..0603becff 100644 --- a/spec/unit/face/module/search_spec.rb +++ b/spec/unit/face/module/search_spec.rb @@ -1,163 +1,163 @@ require 'spec_helper' require 'puppet/face' require 'puppet/application/module' require 'puppet/module_tool' describe "puppet module search", :fails_on_windows => true do subject { Puppet::Face[:module, :current] } let(:options) do {} end describe Puppet::Application::Module do subject do app = Puppet::Application::Module.new app.stubs(:action).returns(Puppet::Face.find_action(:module, :search)) app end before { subject.render_as = :console } before { Puppet::Util::Terminal.stubs(:width).returns(100) } it 'should output nothing when receiving an empty dataset' do subject.render([], ['apache', {}]).should == "No results found for 'apache'." end it 'should output a header when receiving a non-empty dataset' do results = [ {'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] }, ] subject.render(results, ['apache', {}]).should =~ /NAME/ subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/ subject.render(results, ['apache', {}]).should =~ /AUTHOR/ subject.render(results, ['apache', {}]).should =~ /KEYWORDS/ end it 'should output the relevant fields when receiving a non-empty dataset' do results = [ {'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] }, ] subject.render(results, ['apache', {}]).should =~ /Name/ subject.render(results, ['apache', {}]).should =~ /Author/ subject.render(results, ['apache', {}]).should =~ /Summary/ subject.render(results, ['apache', {}]).should =~ /tag1/ subject.render(results, ['apache', {}]).should =~ /tag2/ end it 'should elide really long descriptions' do results = [ { 'full_name' => 'Name', 'author' => 'Author', 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', 'tag_list' => ['tag1', 'tag2'], }, ] subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/ end it 'should never truncate the module name' do results = [ { 'full_name' => 'This-module-has-a-really-really-long-name', 'author' => 'Author', 'desc' => 'Description', 'tag_list' => ['tag1', 'tag2'], }, ] subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/ end it 'should never truncate the author name' do results = [ { 'full_name' => 'Name', 'author' => 'This-author-has-a-really-really-long-name', 'desc' => 'Description', 'tag_list' => ['tag1', 'tag2'], }, ] subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/ end it 'should never remove tags that match the search term' do results = [ { 'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Description', 'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" }, }, ] subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/ end { 100 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*15}\n"\ "Name This description is really too long to fit ... @JohnnyApples tag1 tag2 taggitty3#{' '*4}\n", 70 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*5}\n"\ "Name This description is rea... @JohnnyApples tag1 tag2#{' '*4}\n", 80 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*8}\n"\ "Name This description is really too... @JohnnyApples tag1 tag2#{' '*7}\n", 200 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*48}\n"\ "Name This description is really too long to fit in a single data table, guys -- we should probably set about trunca... @JohnnyApples tag1 tag2 taggitty3#{' '*37}\n" }.each do |width, expectation| it "should resize the table to fit the screen, when #{width} columns" do results = [ { 'full_name' => 'Name', 'author' => 'JohnnyApples', 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', 'tag_list' => ['tag1', 'tag2', 'taggitty3'], }, ] Puppet::Util::Terminal.expects(:width).returns(width) result = subject.render(results, ['apache', {}]) result.lines.sort_by(&:length).last.chomp.length.should <= width result.should == expectation end end end describe "option validation" do context "without any options" do it "should require a search term" do pattern = /wrong number of arguments/ expect { subject.search }.to raise_error ArgumentError, pattern end end it "should accept the --module-repository option" do options[:module_repository] = "http://forge.example.com" - Puppet::Module::Tool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once + Puppet::ModuleTool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once subject.search("puppetlabs-apache", options) end end describe "inline documentation" do subject { Puppet::Face[:module, :current].get_action :search } its(:summary) { should =~ /search.*module/im } its(:description) { should =~ /search.*module/im } its(:returns) { should =~ /array/i } its(:examples) { should_not be_empty } %w{ license copyright summary description returns examples }.each do |doc| context "of the" do its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } end end end end diff --git a/spec/unit/face/module/uninstall_spec.rb b/spec/unit/face/module/uninstall_spec.rb index a157df509..74d82672f 100644 --- a/spec/unit/face/module/uninstall_spec.rb +++ b/spec/unit/face/module/uninstall_spec.rb @@ -1,77 +1,77 @@ require 'spec_helper' require 'puppet/face' require 'puppet/module_tool' describe "puppet module uninstall" do subject { Puppet::Face[:module, :current] } let(:options) do {} end describe "option validation" do context "without any options" do it "should require a name" do pattern = /wrong number of arguments/ expect { subject.uninstall }.to raise_error ArgumentError, pattern end it "should not require any options" do - Puppet::Module::Tool::Applications::Uninstaller.expects(:run).once + Puppet::ModuleTool::Applications::Uninstaller.expects(:run).once subject.uninstall("puppetlabs-apache") end end it "should accept the --environment option" do options[:environment] = "development" expected_options = { :environment => 'development' } - Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once subject.uninstall("puppetlabs-apache", options) end it "should accept the --modulepath option" do options[:modulepath] = "/foo/puppet/modules" expected_options = { :modulepath => '/foo/puppet/modules', :environment => 'production', } - Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once subject.uninstall("puppetlabs-apache", options) end it "should accept the --version option" do options[:version] = "1.0.0" expected_options = { :version => '1.0.0', :environment => 'production', } - Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once subject.uninstall("puppetlabs-apache", options) end it "should accept the --force flag" do options[:force] = true expected_options = { :environment => 'production', :force => true, } - Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once + Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once subject.uninstall("puppetlabs-apache", options) end end describe "inline documentation" do subject { Puppet::Face[:module, :current].get_action :uninstall } its(:summary) { should =~ /uninstall.*module/im } its(:description) { should =~ /uninstall.*module/im } its(:returns) { should =~ /hash of module objects.*/im } its(:examples) { should_not be_empty } %w{ license copyright summary description returns examples }.each do |doc| context "of the" do its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } end end end end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index adabf55b3..c6b575e15 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,864 +1,864 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/modules' require 'puppet/module_tool/checksums' 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 unmet dependencies" do before do FileTest.unstub(:exist?) @modpath = tmpdir('modpath') Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] } ) mod.unmet_dependencies.should == [{ :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 mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar=bar" }] } ) mod.unmet_dependencies.should == [{ :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 mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] } ) mod2 = PuppetSpec::Modules.create( 'foobaz', @modpath, :metadata => { :dependencies => [{ "version_requirement" => "1.0.0", "name" => "baz/foobar" }] } ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' } ) mod.unmet_dependencies.should == [{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, :mod_details => { :installed_version => "2.0.0" } }] mod2.unmet_dependencies.should == [{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => "v1.0.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/foobaz" }, :mod_details => { :installed_version => "2.0.0" } }] end it "should consider a dependency without a version requirement to be satisfied" do mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] } ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' } ) mod.unmet_dependencies.should be_empty end it "should consider a dependency without a semantic version to be unmet" do mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] } ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '5.1', :author => 'baz' } ) mod.unmet_dependencies.should == [{ :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 => [] } ) mod.unmet_dependencies.should == [] end it "should only list unmet dependencies" do mod = PuppetSpec::Modules.create( 'mymod', @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => ">= 2.2.0", "name" => "baz/notsatisfied" } ] } ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' } ) mod.unmet_dependencies.should == [{ :reason => :missing, :mod_details => { :installed_version => nil }, :parent => { :version => "v9.9.9", :name => "puppetlabs/mymod" }, :name => "baz/notsatisfied", :version_constraint => ">= 2.2.0" }] end it "should be empty when all dependencies are met" do mod = PuppetSpec::Modules.create( 'mymod2', @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => "< 2.2.0", "name" => "baz/alsosatisfied" } ] } ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' } ) PuppetSpec::Modules.create( 'alsosatisfied', @modpath, :metadata => { :version => '2.1.0', :author => 'baz' } ) mod.unmet_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 describe '#modulepath' do it "should return the directory the module is installed in, if a path exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.modulepath.should == '/a' end it "should return nil if no path exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.modulepath.should be_nil 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 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 @module.license_file.should == "#{@modpath}/mymod/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 @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 @module.metadata_file.should == "#{@modpath}/mymod/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 metadata file", :if => Puppet.features.pson? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :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 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" } ] } @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 it "should set the dependency version_requirement key" do @module.load_metadata @module.dependencies[0]['version_requirement'].should == "0.0.1" end it "should set the version_requirement key for all dependencies" do @module.load_metadata @module.dependencies[0]['version_requirement'].should == "0.0.1" @module.dependencies[1]['version_requirement'].should == "0.1.0" end end it "should fail if the discovered name is different than the metadata name" end it "should be able to tell if there are local changes", :fails_on_windows => true 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') - Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum + Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum checksummed_module.has_local_changes?.should be_true IO.binwrite(foo_path, 'foo') - Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum + Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum checksummed_module.has_local_changes?.should be_false end it "should know what other modules require it" do Puppet.settings[:modulepath] = @modpath dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :metadata => {:author => 'puppetlabs'} ) PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "puppetlabs/dependable" }] } ) PuppetSpec::Modules.create( 'wantit', @modpath, :metadata => { :author => 'spoiled', :dependencies => [{ "version_requirement" => "< 5.0.0", "name" => "puppetlabs/dependable" }] } ) dependable.required_by.should =~ [ { "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/application_spec.rb b/spec/unit/module_tool/application_spec.rb index a2ef184f7..f6b039230 100644 --- a/spec/unit/module_tool/application_spec.rb +++ b/spec/unit/module_tool/application_spec.rb @@ -1,27 +1,27 @@ require 'spec_helper' require 'puppet/module_tool' -describe Puppet::Module::Tool::Applications::Application, :fails_on_windows => true do +describe Puppet::ModuleTool::Applications::Application, :fails_on_windows => true do describe 'app' do good_versions = %w{ 1.2.4 0.0.1 0.0.0 0.0.2-git-8-g3d316d1 0.0.3-b1 10.100.10000 0.1.2-rc1 0.1.2-dev-1 0.1.2-svn12345 0.1.2-3 } bad_versions = %w{ 0.1 0 0.1.2.3 dev 0.1.2beta } before do @app = Class.new(described_class).new end good_versions.each do |ver| it "should accept version string #{ver}" do @app.parse_filename("puppetlabs-ntp-#{ver}") end end bad_versions.each do |ver| it "should not accept version string #{ver}" do lambda { @app.parse_filename("puppetlabs-ntp-#{ver}") }.should raise_error end end end end diff --git a/spec/unit/module_tool/applications/application_spec.rb b/spec/unit/module_tool/applications/application_spec.rb index 101079f61..c559e4dd9 100644 --- a/spec/unit/module_tool/applications/application_spec.rb +++ b/spec/unit/module_tool/applications/application_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' require 'puppet/module_tool/applications' -describe Puppet::Module::Tool::Applications do - module Puppet::Module::Tool +describe Puppet::ModuleTool::Applications do + module Puppet::ModuleTool module Applications class Fake < Application end end end it "should raise an error on microsoft windows" do Puppet.features.stubs(:microsoft_windows?).returns true - expect { Puppet::Module::Tool::Applications::Fake.new }.to raise_error( + expect { Puppet::ModuleTool::Applications::Fake.new }.to raise_error( Puppet::Error, "`puppet module` actions are currently not supported on Microsoft Windows" ) end end diff --git a/spec/unit/module_tool/applications/installer_spec.rb b/spec/unit/module_tool/applications/installer_spec.rb index 1ad80ee60..09e3f9a20 100644 --- a/spec/unit/module_tool/applications/installer_spec.rb +++ b/spec/unit/module_tool/applications/installer_spec.rb @@ -1,205 +1,205 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/modules' require 'semver' -describe Puppet::Module::Tool::Applications::Installer, :fails_on_windows => true do +describe Puppet::ModuleTool::Applications::Installer, :fails_on_windows => true do include PuppetSpec::Files before do FileUtils.mkdir_p(modpath1) fake_env.modulepath = [modpath1] FileUtils.touch(stdlib_pkg) Puppet.settings[:modulepath] = modpath1 Puppet::Forge.stubs(:remote_dependency_info).returns(remote_dependency_info) Puppet::Forge.stubs(:repository).returns(repository) end let(:unpacker) { stub(:run) } - let(:installer_class) { Puppet::Module::Tool::Applications::Installer } + let(:installer_class) { Puppet::ModuleTool::Applications::Installer } let(:modpath1) { File.join(tmpdir("installer"), "modpath1") } let(:stdlib_pkg) { File.join(modpath1, "pmtacceptance-stdlib-0.0.1.tar.gz") } let(:fake_env) { Puppet::Node::Environment.new('fake_env') } let(:options) { Hash[:target_dir => modpath1] } let(:repository) do repository = mock() repository.stubs(:uri => 'forge-dev.puppetlabs.com') releases = remote_dependency_info.each_key do |mod| remote_dependency_info[mod].each do |release| repository.stubs(:retrieve).with(release['file'])\ .returns("/fake_cache#{release['file']}") end end repository end let(:remote_dependency_info) do { "pmtacceptance/stdlib" => [ { "dependencies" => [], "version" => "0.0.1", "file" => "/pmtacceptance-stdlib-0.0.1.tar.gz" }, { "dependencies" => [], "version" => "0.0.2", "file" => "/pmtacceptance-stdlib-0.0.2.tar.gz" }, { "dependencies" => [], "version" => "1.0.0", "file" => "/pmtacceptance-stdlib-1.0.0.tar.gz" } ], "pmtacceptance/java" => [ { "dependencies" => [["pmtacceptance/stdlib", ">= 0.0.1"]], "version" => "1.7.0", "file" => "/pmtacceptance-java-1.7.0.tar.gz" }, { "dependencies" => [["pmtacceptance/stdlib", "1.0.0"]], "version" => "1.7.1", "file" => "/pmtacceptance-java-1.7.1.tar.gz" } ], "pmtacceptance/apollo" => [ { "dependencies" => [ ["pmtacceptance/java", "1.7.1"], ["pmtacceptance/stdlib", "0.0.1"] ], "version" => "0.0.1", "file" => "/pmtacceptance-apollo-0.0.1.tar.gz" }, { "dependencies" => [ ["pmtacceptance/java", ">= 1.7.0"], ["pmtacceptance/stdlib", ">= 1.0.0"] ], "version" => "0.0.2", "file" => "/pmtacceptance-apollo-0.0.2.tar.gz" } ] } end describe "the behavior of .is_module_package?" do it "should return true when file is a module package" do installer = installer_class.new("foo", options) installer.send(:is_module_package?, stdlib_pkg).should be_true end it "should return false when file is not a module package" do installer = installer_class.new("foo", options) installer.send(:is_module_package?, "pmtacceptance-apollo-0.0.2.tar").should be_false end end context "when the source is a repository" do it "should require a valid name" do lambda { installer_class.run('puppet', params) }.should raise_error(ArgumentError, "Could not install module with invalid name: puppet") end it "should install the requested module" do - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\ .returns(unpacker) results = installer_class.run('pmtacceptance-stdlib', options) results[:installed_modules].length == 1 results[:installed_modules][0][:module].should == "pmtacceptance-stdlib" results[:installed_modules][0][:version][:vstring].should == "1.0.0" end context "when the requested module has dependencies" do it "should install dependencies" do - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\ .returns(unpacker) - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\ .returns(unpacker) - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-java-1.7.1.tar.gz', options)\ .returns(unpacker) results = installer_class.run('pmtacceptance-apollo', options) installed_dependencies = results[:installed_modules][0][:dependencies] dependencies = installed_dependencies.inject({}) do |result, dep| result[dep[:module]] = dep[:version][:vstring] result end dependencies.length.should == 2 dependencies['pmtacceptance-java'].should == '1.7.1' dependencies['pmtacceptance-stdlib'].should == '1.0.0' end it "should install requested module if the '--force' flag is used" do options = { :force => true, :target_dir => modpath1 } - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\ .returns(unpacker) results = installer_class.run('pmtacceptance-apollo', options) results[:installed_modules][0][:module].should == "pmtacceptance-apollo" end it "should not install dependencies if the '--force' flag is used" do options = { :force => true, :target_dir => modpath1 } - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\ .returns(unpacker) results = installer_class.run('pmtacceptance-apollo', options) dependencies = results[:installed_modules][0][:dependencies] dependencies.should == [] end it "should not install dependencies if the '--ignore-dependencies' flag is used" do options = { :ignore_dependencies => true, :target_dir => modpath1 } - Puppet::Module::Tool::Applications::Unpacker.expects(:new)\ + Puppet::ModuleTool::Applications::Unpacker.expects(:new)\ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\ .returns(unpacker) results = installer_class.run('pmtacceptance-apollo', options) dependencies = results[:installed_modules][0][:dependencies] dependencies.should == [] end it "should set an error if dependencies can't be resolved" do options = { :version => '0.0.1', :target_dir => modpath1 } oneline = "'pmtacceptance-apollo' (v0.0.1) requested; Invalid dependency cycle" multiline = <<-MSG.strip Could not install module 'pmtacceptance-apollo' (v0.0.1) No version of 'pmtacceptance-stdlib' will satisfy dependencies You specified 'pmtacceptance-apollo' (v0.0.1), which depends on 'pmtacceptance-java' (v1.7.1), which depends on 'pmtacceptance-stdlib' (v1.0.0) Use `puppet module install --force` to install this module anyway MSG results = installer_class.run('pmtacceptance-apollo', options) results[:result].should == :failure results[:error][:oneline].should == oneline results[:error][:multiline].should == multiline end end context "when there are modules installed" do it "should use local version when already exists and satisfies constraints" it "should reinstall the local version if force is used" it "should upgrade local version when necessary to satisfy constraints" it "should error when a local version can't be upgraded to satisfy constraints" end context "when a local module needs upgrading to satisfy constraints but has changes" do it "should error" it "should warn and continue if force is used" end it "should error when a local version of a dependency has no version metadata" it "should error when a local version of a dependency has a non-semver version" it "should error when a local version of a dependency has a different forge name" it "should error when a local version of a dependency has no metadata" end context "when the source is a filesystem" do before do @sourcedir = tmpdir('sourcedir') end it "should error if it can't parse the name" it "should try to get_release_package_from_filesystem if it has a valid name" end end diff --git a/spec/unit/module_tool/applications/uninstaller_spec.rb b/spec/unit/module_tool/applications/uninstaller_spec.rb index e8a4b3f46..67b39fbd9 100644 --- a/spec/unit/module_tool/applications/uninstaller_spec.rb +++ b/spec/unit/module_tool/applications/uninstaller_spec.rb @@ -1,206 +1,206 @@ require 'spec_helper' require 'puppet/module_tool' require 'tmpdir' require 'puppet_spec/modules' -describe Puppet::Module::Tool::Applications::Uninstaller, :fails_on_windows => true do +describe Puppet::ModuleTool::Applications::Uninstaller, :fails_on_windows => true do include PuppetSpec::Files def mkmod(name, path, metadata=nil) modpath = File.join(path, name) FileUtils.mkdir_p(modpath) if metadata File.open(File.join(modpath, 'metadata.json'), 'w') do |f| f.write(metadata.to_pson) end end modpath end describe "the behavior of the instances" do before do - @uninstaller = Puppet::Module::Tool::Applications::Uninstaller + @uninstaller = Puppet::ModuleTool::Applications::Uninstaller FileUtils.mkdir_p(modpath1) FileUtils.mkdir_p(modpath2) fake_env.modulepath = [modpath1, modpath2] end let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") } let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") } let(:fake_env) { Puppet::Node::Environment.new('fake_env') } let(:options) { {:environment => "fake_env"} } let(:foo_metadata) do { :author => "puppetlabs", :name => "puppetlabs/foo", :version => "1.0.0", :source => "http://dummyurl/foo", :license => "Apache2", :dependencies => [], } end let(:bar_metadata) do { :author => "puppetlabs", :name => "puppetlabs/bar", :version => "1.0.0", :source => "http://dummyurl/bar", :license => "Apache2", :dependencies => [], } end context "when the module is not installed" do it "should fail" do @uninstaller.new('fakemod_not_installed', options).run[:result].should == :failure end end context "when the module is installed" do it "should uninstall the module" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].first.forge_name.should == "puppetlabs/foo" end it "should only uninstall the requested module" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) PuppetSpec::Modules.create('bar', modpath1, :metadata => bar_metadata) results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].length == 1 results[:affected_modules].first.forge_name.should == "puppetlabs/foo" end it "should uninstall fail if a module exists twice in the modpath" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) PuppetSpec::Modules.create('foo', modpath2, :metadata => foo_metadata) @uninstaller.new('puppetlabs-foo', options).run[:result].should == :failure end context "when options[:version] is specified" do it "should uninstall the module if the version matches" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) options[:version] = "1.0.0" results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].length.should == 1 results[:affected_modules].first.forge_name.should == "puppetlabs/foo" results[:affected_modules].first.version.should == "1.0.0" end it "should not uninstall the module if the version does not match" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) options[:version] = "2.0.0" @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure end end context "when the module metadata is missing" do it "should not uninstall the module" do PuppetSpec::Modules.create('foo', modpath1) @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure end end context "when the module has local changes" do it "should not uninstall the module" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) Puppet::Module.any_instance.stubs(:has_local_changes?).returns(true) @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure end end context "when the module does not have local changes" do it "should uninstall the module" do PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].length.should == 1 results[:affected_modules].first.forge_name.should == "puppetlabs/foo" end end context "when uninstalling the module will cause broken dependencies" do it "should not uninstall the module" do Puppet.settings[:modulepath] = modpath1 PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) PuppetSpec::Modules.create( 'needy', modpath1, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 1.0.0", "name" => "puppetlabs/foo" }] } ) @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure end end context "when using the --force flag" do let(:fakemod) do stub( :forge_name => 'puppetlabs/fakemod', :version => '0.0.1', :has_local_changes? => true ) end it "should ignore local changes" do foo = mkmod("foo", modpath1, foo_metadata) options[:force] = true results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].length.should == 1 results[:affected_modules].first.forge_name.should == "puppetlabs/foo" end it "should ignore broken dependencies" do Puppet.settings[:modulepath] = modpath1 PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata) PuppetSpec::Modules.create( 'needy', modpath1, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 1.0.0", "name" => "puppetlabs/foo" }] } ) options[:force] = true results = @uninstaller.new("puppetlabs-foo", options).run results[:affected_modules].length.should == 1 results[:affected_modules].first.forge_name.should == "puppetlabs/foo" end end end end end diff --git a/spec/unit/module_tool/applications/upgrader_spec.rb b/spec/unit/module_tool/applications/upgrader_spec.rb index 542e00b7a..1f5d2c502 100644 --- a/spec/unit/module_tool/applications/upgrader_spec.rb +++ b/spec/unit/module_tool/applications/upgrader_spec.rb @@ -1,37 +1,37 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/modules' require 'semver' -describe Puppet::Module::Tool::Applications::Upgrader, :fails_on_windows => true do +describe Puppet::ModuleTool::Applications::Upgrader, :fails_on_windows => true do include PuppetSpec::Files before do end it "should update the requested module" it "should not update dependencies" it "should fail when updating a dependency to an unsupported version" it "should fail when updating a module that is not installed" it "should warn when the latest version is already installed" it "should warn when the best version is already installed" context "when using the '--version' option" do it "should update an installed module to the requested version" end context "when using the '--force' flag" do it "should ignore missing dependencies" it "should ignore version constraints" it "should not update a module that is not installed" end context "when using the '--env' option" do it "should use the correct environment" end context "when there are missing dependencies" do it "should fail to upgrade the original module" it "should raise an error" end end diff --git a/spec/unit/module_tool/metadata_spec.rb b/spec/unit/module_tool/metadata_spec.rb index 85d743fbc..af86e4d68 100644 --- a/spec/unit/module_tool/metadata_spec.rb +++ b/spec/unit/module_tool/metadata_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' require 'puppet/module_tool' -describe Puppet::Module::Tool::Metadata do +describe Puppet::ModuleTool::Metadata do context "when using default values" do it "should set license to 'Apache License, Version 2.0'" do - metadata = Puppet::Module::Tool::Metadata.new + metadata = Puppet::ModuleTool::Metadata.new metadata.license.should == "Apache License, Version 2.0" end end end diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb index 1517f30f5..914b22e82 100644 --- a/spec/unit/module_tool_spec.rb +++ b/spec/unit/module_tool_spec.rb @@ -1,113 +1,113 @@ # encoding: UTF-8 require 'spec_helper' require 'puppet/module_tool' -describe Puppet::Module::Tool, :fails_on_windows => true do +describe Puppet::ModuleTool, :fails_on_windows => true do describe '.format_tree' do it 'should return an empty tree when given an empty list' do subject.format_tree([]).should == '' end it 'should return a shallow when given a list without dependencies' do list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ] subject.format_tree(list).should == <<-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' } ] } ] }, ] subject.format_tree(list).should == <<-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' } ] subject.format_tree(list).should == <<-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' } ] } ] subject.format_tree(list).should == <<-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' } ] subject.format_tree(list).should == <<-TREE ├─┬ first │ ├─┬ second │ │ └── third │ └── fourth └── fifth TREE end end end