diff --git a/lib/puppet/face/module/install.rb b/lib/puppet/face/module/install.rb index 2378c7a5f..13db76b87 100644 --- a/lib/puppet/face/module/install.rb +++ b/lib/puppet/face/module/install.rb @@ -1,136 +1,137 @@ # encoding: UTF-8 +require 'puppet/forge' 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 "--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 when_invoked do |name, options| Puppet::ModuleTool.set_option_defaults options Puppet.notice "Preparing to install into #{options[:target_dir]} ..." - Puppet::ModuleTool::Applications::Installer.run(name, options) + Puppet::ModuleTool::Applications::Installer.new(name, Puppet::Forge.new("PMT", self.version), options).run 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::ModuleTool.build_tree(return_value[:installed_modules], return_value[:install_dir]) return_value[:install_dir] + "\n" + Puppet::ModuleTool.format_tree(tree) end end end end diff --git a/lib/puppet/face/module/search.rb b/lib/puppet/face/module/search.rb index a7e5821dc..36cca8f4b 100644 --- a/lib/puppet/face/module/search.rb +++ b/lib/puppet/face/module/search.rb @@ -1,89 +1,90 @@ require 'puppet/util/terminal' +require 'puppet/forge' 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::ModuleTool.set_option_defaults options - Puppet::ModuleTool::Applications::Searcher.run(term, options) + Puppet::ModuleTool::Applications::Searcher.new(term, Puppet::Forge.new("PMT", self.version), options).run 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/upgrade.rb b/lib/puppet/face/module/upgrade.rb index 5c4cd7901..c16e3a3a5 100644 --- a/lib/puppet/face/module/upgrade.rb +++ b/lib/puppet/face/module/upgrade.rb @@ -1,77 +1,77 @@ # 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 "--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::ModuleTool.set_option_defaults options - Puppet::ModuleTool::Applications::Upgrader.new(name, options).run + Puppet::ModuleTool::Applications::Upgrader.new(name, Puppet::Forge.new("PMT", self.version), 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::ModuleTool.build_tree(return_value[:affected_modules], return_value[:base_dir]) return_value[:base_dir] + "\n" + Puppet::ModuleTool.format_tree(tree) end end end end diff --git a/lib/puppet/forge.rb b/lib/puppet/forge.rb index 722f03b95..eff05eda5 100644 --- a/lib/puppet/forge.rb +++ b/lib/puppet/forge.rb @@ -1,98 +1,113 @@ require 'net/http' require 'open-uri' require 'pathname' require 'uri' require 'puppet/forge/cache' require 'puppet/forge/repository' -module Puppet::Forge +class Puppet::Forge + # +consumer_name+ is a name to be used for identifying the consumer of the + # forge and +consumer_semver+ is a SemVer object to identify the version of + # the consumer + def initialize(consumer_name, consumer_semver) + @consumer_name = consumer_name + @consumer_semver = consumer_semver + end + # Return a list of module metadata hashes that match the search query. # This return value is used by the module_tool face install search, # and displayed to on the console. # # Example return value: # # [ # { # "author" => "puppetlabs", # "name" => "bacula", # "tag_list" => ["backup", "bacula"], # "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}], # "full_name" => "puppetlabs/bacula", # "version" => "0.0.2", # "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula", # "desc" => "bacula" # } # ] # - def self.search(term) + def search(term) server = Puppet.settings[:module_repository].sub(/^(?!https?:\/\/)/, 'http://') Puppet.notice "Searching #{server} ..." - request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}") - response = repository.make_http_request(request) + response = repository.make_http_request("/modules.json?q=#{URI.escape(term)}") case response.code when "200" matches = PSON.parse(response.body) else raise RuntimeError, "Could not execute search (HTTP #{response.code})" - matches = [] end matches end - def self.remote_dependency_info(author, mod_name, version) + def remote_dependency_info(author, mod_name, version) version_string = version ? "&version=#{version}" : '' - request = Net::HTTP::Get.new("/api/v1/releases.json?module=#{author}/#{mod_name}" + version_string) - response = repository.make_http_request(request) + response = repository.make_http_request("/api/v1/releases.json?module=#{author}/#{mod_name}#{version_string}") json = PSON.parse(response.body) rescue {} case response.code when "200" return json else error = json['error'] || '' if error =~ /^Module #{author}\/#{mod_name} has no release/ return [] else raise RuntimeError, "Could not find release information for this module (#{author}/#{mod_name}) (HTTP #{response.code})" end end end - def self.get_release_packages_from_repository(install_list) + def get_release_packages_from_repository(install_list) install_list.map do |release| modname, version, file = release cache_path = nil if file begin cache_path = repository.retrieve(file) rescue OpenURI::HTTPError => e raise RuntimeError, "Could not download module: #{e.message}" end else raise RuntimeError, "Malformed response from module repository." end cache_path end end # Locate a module release package on the local filesystem and move it # into the `Puppet.settings[:module_working_dir]`. Do not unpack it, just # return the location of the package on disk. - def self.get_release_package_from_filesystem(filename) + def get_release_package_from_filesystem(filename) if File.exist?(File.expand_path(filename)) repository = Repository.new('file:///') uri = URI.parse("file://#{URI.escape(File.expand_path(filename))}") cache_path = repository.retrieve(uri) else raise ArgumentError, "File does not exists: #{filename}" end cache_path end - def self.repository - @repository ||= Puppet::Forge::Repository.new + def retrieve(release) + repository.retrieve(release) + end + + def uri + repository.uri + end + + def repository + version = "#{@consumer_name}/#{[@consumer_semver.major, @consumer_semver.minor, @consumer_semver.tiny].join('.')}#{@consumer_semver.special}" + @repository ||= Puppet::Forge::Repository.new(Puppet[:module_repository], version) end + private :repository end diff --git a/lib/puppet/forge/cache.rb b/lib/puppet/forge/cache.rb index 592bf1ace..c9aeb55a6 100644 --- a/lib/puppet/forge/cache.rb +++ b/lib/puppet/forge/cache.rb @@ -1,55 +1,55 @@ require 'uri' -module Puppet::Forge +class Puppet::Forge # = Cache # # Provides methods for reading files from local cache, filesystem or network. class Cache # Instantiate new cahe for the +repositry+ instance. def initialize(repository, options = {}) @repository = repository @options = options end # Return filename retrieved from +uri+ instance. Will download this file and # cache it if needed. # # TODO: Add checksum support. # TODO: Add error checking. def retrieve(url) (path + File.basename(url.to_s)).tap do |cached_file| uri = url.is_a?(::URI) ? url : ::URI.parse(url) unless cached_file.file? if uri.scheme == 'file' FileUtils.cp(URI.unescape(uri.path), cached_file) else # TODO: Handle HTTPS; probably should use repository.contact data = read_retrieve(uri) cached_file.open('wb') { |f| f.write data } end end end end # Return contents of file at the given URI's +uri+. def read_retrieve(uri) return uri.read end # Return Pathname for repository's cache directory, create it if needed. def path (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath } end # Return the base Pathname for all the caches. def self.base_path Pathname(Puppet.settings[:module_working_dir]) + 'cache' end # Clean out all the caches. def self.clean base_path.rmtree if base_path.exist? end end end diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb index cacfdc6ea..2528bd350 100644 --- a/lib/puppet/forge/repository.rb +++ b/lib/puppet/forge/repository.rb @@ -1,102 +1,109 @@ require 'net/http' require 'digest/sha1' require 'uri' -module Puppet::Forge +class Puppet::Forge # = Repository # # This class is a file for accessing remote repositories with modules. class Repository - attr_reader :uri, :cache - # Instantiate a new repository instance rooted at the optional string - # +url+, else an instance of the default Puppet modules repository. - def initialize(url=Puppet[:module_repository]) + # Instantiate a new repository instance rooted at the +url+. + # The agent will report +consumer_version+ in the User-Agent to + # the repository. + def initialize(url, consumer_version) @uri = url.is_a?(::URI) ? url : ::URI.parse(url.sub(/^(?!https?:\/\/)/, 'http://')) @cache = Cache.new(self) + @consumer_version = consumer_version end # Read HTTP proxy configurationm from Puppet's config file, or the # http_proxy environment variable. def http_proxy_env proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"] || nil begin return URI.parse(proxy_env) if proxy_env rescue URI::InvalidURIError return nil end return nil end def http_proxy_host env = http_proxy_env if env and env.host then return env.host end if Puppet.settings[:http_proxy_host] == 'none' return nil end return Puppet.settings[:http_proxy_host] end def http_proxy_port env = http_proxy_env if env and env.port then return env.port end return Puppet.settings[:http_proxy_port] end - # Return a Net::HTTPResponse read for this +request+. - def make_http_request(request, options = {}) + # Return a Net::HTTPResponse read for this +request_path+. + def make_http_request(request_path) + request = Net::HTTP::Get.new(request_path, { "User-Agent" => user_agent }) if ! @uri.user.nil? && ! @uri.password.nil? request.basic_auth(@uri.user, @uri.password) end return read_response(request) end # Return a Net::HTTPResponse read from this HTTPRequest +request+. def read_response(request) begin Net::HTTP::Proxy( http_proxy_host, http_proxy_port ).start(@uri.host, @uri.port) do |http| http.request(request) end rescue Errno::ECONNREFUSED, SocketError msg = "Error: Could not connect to #{@uri}\n" msg << " There was a network communications problem\n" msg << " Check your network connection and try again\n" Puppet.err msg exit(1) end end # Return the local file name containing the data downloaded from the # repository at +release+ (e.g. "myuser-mymodule"). def retrieve(release) return cache.retrieve(@uri + release) end # Return the URI string for this repository. def to_s return @uri.to_s end # Return the cache key for this repository, this a hashed string based on # the URI. def cache_key return @cache_key ||= [ @uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''), Digest::SHA1.hexdigest(@uri.to_s) ].join('-') end + + def user_agent + "#{@consumer_version} Puppet/#{Puppet.version} (#{Facter.value(:operatingsystem)} #{Facter.value(:operatingsystemrelease)}) Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}; #{RUBY_PLATFORM})" + end + private :user_agent end end diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb index 645a4ecb5..add178007 100644 --- a/lib/puppet/module_tool/applications/installer.rb +++ b/lib/puppet/module_tool/applications/installer.rb @@ -1,183 +1,184 @@ require 'open-uri' require 'pathname' require 'tmpdir' require 'semver' require 'puppet/forge' require 'puppet/module_tool' require 'puppet/module_tool/shared_behaviors' module Puppet::ModuleTool module Applications class Installer < Application include Puppet::ModuleTool::Errors - def initialize(name, options = {}) + def initialize(name, forge, options = {}) @action = :install @environment = Puppet::Node::Environment.new(Puppet.settings[:environment]) @force = options[:force] @ignore_dependencies = options[:force] || options[:ignore_dependencies] @name = name + @forge = forge super(options) end def run begin if is_module_package?(@name) @source = :filesystem @filename = File.expand_path(@name) raise MissingPackageError, :requested_package => @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::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 + get_remote_constraints(@forge) 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]) + download_tarballs(@graph, @graph.last[:path], @forge) 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 28c78375f..cc994bafd 100644 --- a/lib/puppet/module_tool/applications/searcher.rb +++ b/lib/puppet/module_tool/applications/searcher.rb @@ -1,15 +1,16 @@ module Puppet::ModuleTool module Applications class Searcher < Application - def initialize(term, options = {}) + def initialize(term, forge, options = {}) @term = term + @forge = forge super(options) end def run - Puppet::Forge.search(@term) + @forge.search(@term) end end end end diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb index 94c1ddc6a..226f24050 100644 --- a/lib/puppet/module_tool/applications/upgrader.rb +++ b/lib/puppet/module_tool/applications/upgrader.rb @@ -1,109 +1,110 @@ module Puppet::ModuleTool module Applications class Upgrader < Application include Puppet::ModuleTool::Errors - def initialize(name, options) + def initialize(name, forge, 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] + @forge = forge 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 + get_remote_constraints(@forge) rescue => e - raise UnknownModuleError, results.merge(:repository => Puppet::Forge.repository.uri) + raise UnknownModuleError, results.merge(:repository => @forge.uri) else - raise UnknownVersionError, results.merge(:repository => Puppet::Forge.repository.uri) if @remote.empty? + raise UnknownVersionError, results.merge(:repository => @forge.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]) + tarballs = download_tarballs(@graph, @graph.last[:path], @forge) 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::ModuleTool::Shared end end end diff --git a/lib/puppet/module_tool/shared_behaviors.rb b/lib/puppet/module_tool/shared_behaviors.rb index b7821fc91..b43b16688 100644 --- a/lib/puppet/module_tool/shared_behaviors.rb +++ b/lib/puppet/module_tool/shared_behaviors.rb @@ -1,161 +1,161 @@ module Puppet::ModuleTool::Shared 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 + def get_remote_constraints(forge) @remote = Hash.new { |h,k| h[k] = { } } @urls = {} @versions = Hash.new { |h,k| h[k] = [] } - Puppet.notice "Downloading from #{Puppet::Forge.repository.uri} ..." + Puppet.notice "Downloading from #{forge.uri} ..." author, modname = Puppet::ModuleTool.username_and_modname_from(@module_name) - info = Puppet::Forge.remote_dependency_info(author, modname, @options[:version]) + info = 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) + def download_tarballs(graph, default_path, forge) graph.map do |release| begin if release[:tarball] cache_path = Pathname(release[:tarball]) else - cache_path = Puppet::Forge.repository.retrieve(release[:file]) + cache_path = forge.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) + *download_tarballs(release[:dependencies], default_path, forge) ] end.flatten end end diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb index 88d7c7c0a..f8278d369 100644 --- a/spec/unit/face/module/install_spec.rb +++ b/spec/unit/face/module/install_spec.rb @@ -1,159 +1,168 @@ require 'spec_helper' require 'puppet/face' require 'puppet/module_tool' describe "puppet module install" do include PuppetSpec::Files 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) { make_absolute("/my/fake/modpath") } let(:fakesecondpath) { make_absolute("/other/fake/path") } let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" } let(:fakedirpath) { make_absolute("/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::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + expects_installer_run_with("puppetlabs-apache", expected_options) + subject.install("puppetlabs-apache") end end it "should accept the --force option" do options[:force] = true expected_options.merge!(options) - Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + + expects_installer_run_with("puppetlabs-apache", expected_options) + subject.install("puppetlabs-apache", options) end it "should accept the --target-dir option" do options[:target_dir] = make_absolute("/foo/puppet/modules") expected_options.merge!(options) expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}" - Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + expects_installer_run_with("puppetlabs-apache", expected_options) + subject.install("puppetlabs-apache", options) end it "should accept the --version option" do options[:version] = "0.0.1" expected_options.merge!(options) - Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + + expects_installer_run_with("puppetlabs-apache", expected_options) + subject.install("puppetlabs-apache", options) end it "should accept the --ignore-dependencies option" do options[:ignore_dependencies] = true expected_options.merge!(options) - Puppet::ModuleTool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once + + expects_installer_run_with("puppetlabs-apache", expected_options) + subject.install("puppetlabs-apache", options) end describe "when modulepath option is passed" do let(:expected_options) { { :modulepath => fakemodpath, :environment => Puppet[:environment] } } 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::ModuleTool::Applications::Installer. - expects(:run). - with("puppetlabs-apache", expected_options) + expects_installer_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::ModuleTool::Applications::Installer. - expects(:run). - with("puppetlabs-apache", expected_options) + expects_installer_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::ModuleTool::Applications::Installer. - expects(:run). - with("puppetlabs-apache", expected_options) + expects_installer_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::ModuleTool::Applications::Installer. - expects(:run). - with("puppetlabs-apache", expected_options) + expects_installer_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 + + def expects_installer_run_with(name, options) + installer = mock("Installer") + forge = mock("Forge") + + Puppet::Forge.expects(:new).with("PMT", subject.version).returns(forge) + Puppet::ModuleTool::Applications::Installer.expects(:new).with("puppetlabs-apache", forge, expected_options).returns(installer) + installer.expects(:run) + end end diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb index 999b24c59..699dc117f 100644 --- a/spec/unit/face/module/search_spec.rb +++ b/spec/unit/face/module/search_spec.rb @@ -1,163 +1,169 @@ require 'spec_helper' require 'puppet/face' require 'puppet/application/module' require 'puppet/module_tool' describe "puppet module search" 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 + forge = mock("Puppet::Forge") + searcher = mock("Searcher") options[:module_repository] = "http://forge.example.com" - Puppet::ModuleTool::Applications::Searcher.expects(:run).with("puppetlabs-apache", has_entries(options)).once + + Puppet::Forge.expects(:new).with("PMT", subject.version).returns(forge) + Puppet::ModuleTool::Applications::Searcher.expects(:new).with("puppetlabs-apache", forge, has_entries(options)).returns(searcher) + searcher.expects(:run) + 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/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb index bbfc0d136..78416883e 100644 --- a/spec/unit/forge/repository_spec.rb +++ b/spec/unit/forge/repository_spec.rb @@ -1,56 +1,88 @@ require 'spec_helper' require 'net/http' require 'puppet/forge/repository' require 'puppet/forge/cache' describe Puppet::Forge::Repository do - describe 'instances' do + let(:consumer_version) { "Test/1.0" } + let(:repository) { Puppet::Forge::Repository.new('http://fake.com', consumer_version) } - let(:repository) { Puppet::Forge::Repository.new('http://fake.com') } + it "retrieve accesses the cache" do + uri = URI.parse('http://some.url.com') + repository.cache.expects(:retrieve).with(uri) - describe '#retrieve' do - before do - @uri = URI.parse('http://some.url.com') - end + repository.retrieve(uri) + end - it "should access the cache" do - repository.cache.expects(:retrieve).with(@uri) - repository.retrieve(@uri) - end + describe 'http_proxy support' do + after :each do + ENV["http_proxy"] = nil end - describe 'http_proxy support' do - before :each do - ENV["http_proxy"] = nil - end + it "supports environment variable for port and host" do + ENV["http_proxy"] = "http://test.com:8011" - after :each do - ENV["http_proxy"] = nil - end + repository.http_proxy_host.should == "test.com" + repository.http_proxy_port.should == 8011 + end - it "should support environment variable for port and host" do - ENV["http_proxy"] = "http://test.com:8011" - repository.http_proxy_host.should == "test.com" - repository.http_proxy_port.should == 8011 - end + it "supports puppet configuration for port and host" do + ENV["http_proxy"] = nil + proxy_settings_of('test.com', 7456) + + repository.http_proxy_port.should == 7456 + repository.http_proxy_host.should == "test.com" + end + + it "uses environment variable before puppet settings" do + ENV["http_proxy"] = "http://test1.com:8011" + proxy_settings_of('test2.com', 7456) + + repository.http_proxy_host.should == "test1.com" + repository.http_proxy_port.should == 8011 + end + end - it "should support puppet configuration for port and host" do - ENV["http_proxy"] = nil - Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test.com') - Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) + describe "making a request" do + before :each do + proxy_settings_of("proxy", 1234) + end - repository.http_proxy_port.should == 7456 - repository.http_proxy_host.should == "test.com" + it "returns the result object from the request" do + result = "the http response" + performs_an_http_request result do |http| + http.expects(:request).with(responds_with(:path, "the_path")) end - it "should use environment variable before puppet settings" do - ENV["http_proxy"] = "http://test1.com:8011" - Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test2.com') - Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) + repository.make_http_request("the_path").should == result + end + + it "sets the user agent for the request" do + performs_an_http_request do |http| + http.expects(:request).with() do |request| + puppet_version = /Puppet\/\d+\..*/ + os_info = /\(.*\)/ + ruby_version = /Ruby\/\d+\.\d+\.\d+-p\d+ \(\d{4}-\d{2}-\d{2}; .*\)/ - repository.http_proxy_host.should == "test1.com" - repository.http_proxy_port.should == 8011 + request["User-Agent"] =~ /^#{consumer_version} #{puppet_version} #{os_info} #{ruby_version}/ + end end + + repository.make_http_request("the_path") end + + def performs_an_http_request(result = nil, &block) + http = mock("http client") + yield http + + proxy = mock("http proxy") + proxy.expects(:start).with("fake.com", 80).yields(http).returns(result) + Net::HTTP.expects(:Proxy).with("proxy", 1234).returns(proxy) + end + end + + def proxy_settings_of(host, port) + Puppet.settings.stubs(:[]).with(:http_proxy_host).returns(host) + Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(port) end end diff --git a/spec/unit/forge_spec.rb b/spec/unit/forge_spec.rb index 95e47a03e..6e11d0d88 100644 --- a/spec/unit/forge_spec.rb +++ b/spec/unit/forge_spec.rb @@ -1,56 +1,50 @@ require 'spec_helper' require 'puppet/forge' require 'net/http' require 'puppet/module_tool' describe Puppet::Forge do - include PuppetSpec::Files - let(:response_body) do <<-EOF [ { "author": "puppetlabs", "name": "bacula", "tag_list": ["backup", "bacula"], "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}], "full_name": "puppetlabs/bacula", "version": "0.0.2", "project_url": "http://github.com/puppetlabs/puppetlabs-bacula", "desc": "bacula" } ] EOF end - let(:response) { stub(:body => response_body, :code => '200') } - before do + let(:forge) { Puppet::Forge.new("test_agent", SemVer.new("v1.0.0")) } + + def repository_responds_with(response) Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response) - Puppet::Forge::Repository.any_instance.stubs(:retrieve).returns("/tmp/foo") end - describe "the behavior of the search method" do - context "when there are matches for the search term" do - before do - Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response) - end + it "returns a list of matches from the forge when there are matches for the search term" do + response = stub(:body => response_body, :code => '200') + repository_responds_with(response) - it "should return a list of matches from the forge" do - Puppet::Forge.search('bacula').should == PSON.load(response_body) - end - end + forge.search('bacula').should == PSON.load(response_body) + end - context "when the connection to the forge fails" do - let(:response) { stub(:body => '{}', :code => '404') } + context "when the connection to the forge fails" do + before :each do + repository_responds_with(stub(:body => '{}', :code => '404')) + end - it "should raise an error for search" do - lambda { Puppet::Forge.search('bacula') }.should raise_error RuntimeError - end + it "raises an error for search" do + expect { forge.search('bacula') }.should raise_error RuntimeError + end - it "should raise an error for remote_dependency_info" do - lambda { Puppet::Forge.remote_dependency_info('puppetlabs', 'bacula', '0.0.1') }.should raise_error RuntimeError - end + it "raises an error for remote_dependency_info" do + expect { forge.remote_dependency_info('puppetlabs', 'bacula', '0.0.1') }.should raise_error RuntimeError end end - end diff --git a/spec/unit/module_tool/applications/installer_spec.rb b/spec/unit/module_tool/applications/installer_spec.rb index 6b3c8fdfc..cddcdafbe 100644 --- a/spec/unit/module_tool/applications/installer_spec.rb +++ b/spec/unit/module_tool/applications/installer_spec.rb @@ -1,222 +1,220 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/modules' require 'semver' 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::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') + let(:forge) do + forge = mock("Puppet::Forge") - releases = remote_dependency_info.each_key do |mod| + forge.stubs(:remote_dependency_info).returns(remote_dependency_info) + forge.stubs(:uri).returns('forge-dev.puppetlabs.com') + 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']}") + forge.stubs(:retrieve).with(release['file']).returns("/fake_cache#{release['file']}") end end - repository + forge 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 pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - installer = installer_class.new("foo", options) + installer = installer_class.new("foo", forge, options) installer.send(:is_module_package?, stdlib_pkg).should be_true end end it "should return false when file is not a module package" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - installer = installer_class.new("foo", options) + installer = installer_class.new("foo", forge, options) installer.send(:is_module_package?, "pmtacceptance-apollo-0.0.2.tar"). should be_false end 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 pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do 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 = installer_class.run('pmtacceptance-stdlib', forge, 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 end context "when the requested module has dependencies" do it "should install dependencies" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do Puppet::ModuleTool::Applications::Unpacker.expects(:new). with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options). returns(unpacker) Puppet::ModuleTool::Applications::Unpacker.expects(:new). with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options). returns(unpacker) 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) + results = installer_class.run('pmtacceptance-apollo', forge, 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 end it "should install requested module if the '--force' flag is used" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do options = { :force => true, :target_dir => modpath1 } 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 = installer_class.run('pmtacceptance-apollo', forge, options) results[:installed_modules][0][:module].should == "pmtacceptance-apollo" end end it "should not install dependencies if the '--force' flag is used" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do options = { :force => true, :target_dir => modpath1 } 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 = installer_class.run('pmtacceptance-apollo', forge, options) dependencies = results[:installed_modules][0][:dependencies] dependencies.should == [] end end it "should not install dependencies if the '--ignore-dependencies' flag is used" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do options = { :ignore_dependencies => true, :target_dir => modpath1 } 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 = installer_class.run('pmtacceptance-apollo', forge, options) dependencies = results[:installed_modules][0][:dependencies] dependencies.should == [] end end it "should set an error if dependencies can't be resolved" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) 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 = installer_class.run('pmtacceptance-apollo', forge, options) results[:result].should == :failure results[:error][:oneline].should == oneline results[:error][:multiline].should == multiline end 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/upgrader_spec.rb b/spec/unit/module_tool/applications/upgrader_spec.rb index 3707f8bf0..63a7a21f8 100644 --- a/spec/unit/module_tool/applications/upgrader_spec.rb +++ b/spec/unit/module_tool/applications/upgrader_spec.rb @@ -1,37 +1,34 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/modules' require 'semver' describe Puppet::ModuleTool::Applications::Upgrader 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