diff --git a/lib/puppet/forge.rb b/lib/puppet/forge.rb new file mode 100644 index 000000000..6b3c5742f --- /dev/null +++ b/lib/puppet/forge.rb @@ -0,0 +1,153 @@ +require 'net/http' +require 'open-uri' +require 'pathname' +require 'uri' +require 'puppet/forge/cache' +require 'puppet/forge/repository' + +module Puppet::Forge + class Forge + def initialize(url=Puppet.settings[:module_repository]) + @uri = URI.parse(url) + 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 search(term) + request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}") + response = repository.make_http_request(request) + + case response.code + when "200" + matches = PSON.parse(response.body) + else + raise RuntimeError, "Could not execute search (HTTP #{response.code})" + matches = [] + end + + matches + end + + # Return a Pathname object representing the path to the module + # release package in the `Puppet.settings[:module_working_dir]`. + def get_release_package(params) + cache_path = nil + case params[:source] + when :repository + if not (params[:author] && params[:modname]) + raise ArgumentError, ":author and :modename required" + end + cache_path = get_release_package_from_repository(params[:author], params[:modname], params[:version]) + when :filesystem + if not params[:filename] + raise ArgumentError, ":filename required" + end + cache_path = get_release_package_from_filesystem(params[:filename]) + else + raise ArgumentError, "Could not determine installation source" + end + + cache_path + end + + def get_releases(author, modname) + request_string = "/#{author}/#{modname}" + + begin + response = repository.make_http_request(request_string) + rescue => e + raise ArgumentError, "Could not find a release for this module (#{e.message})" + end + + results = PSON.parse(response.body) + # At this point releases look like this: + # [{"version" => "0.0.1"}, {"version" => "0.0.2"},{"version" => "0.0.3"}] + # + # Lets fix this up a bit and return something like this to the caller + # ["0.0.1", "0.0.2", "0.0.3"] + results["releases"].collect {|release| release["version"]} + end + + private + + # Locate and download a module release package from the remote forge + # repository into the `Puppet.settings[:module_working_dir]`. Do not + # unpack it, just return the location of the package on disk. + def get_release_package_from_repository(author, modname, version=nil) + release = get_release(author, modname, version) + if release['file'] + begin + cache_path = repository.retrieve(release['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 + + # 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 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 repository + @repository ||= Puppet::Forge::Repository.new(@uri) + end + + # Connect to the remote repository and locate a specific module release + # by author/name combination. If a version requirement is specified, search + # for that exact version, or grab the latest release available. + # + # Return the following response to the caller: + # + # {"file"=>"/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version"=>"0.0.3"} + # + # + def get_release(author, modname, version_requirement=nil) + request_string = "/users/#{author}/modules/#{modname}/releases/find.json" + if version_requirement + request_string + "?version=#{URI.escape(version_requirement)}" + end + request = Net::HTTP::Get.new(request_string) + + begin + response = repository.make_http_request(request) + rescue => e + raise ArgumentError, "Could not find a release for this module (#{e.message})" + end + + PSON.parse(response.body) + end + end +end + diff --git a/lib/puppet/module_tool/cache.rb b/lib/puppet/forge/cache.rb similarity index 98% rename from lib/puppet/module_tool/cache.rb rename to lib/puppet/forge/cache.rb index b25478096..253f24761 100644 --- a/lib/puppet/module_tool/cache.rb +++ b/lib/puppet/forge/cache.rb @@ -1,56 +1,55 @@ require 'uri' -module Puppet::Module::Tool - +module 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 return @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/module_tool/repository.rb b/lib/puppet/forge/repository.rb similarity index 65% rename from lib/puppet/module_tool/repository.rb rename to lib/puppet/forge/repository.rb index 7f0ad849f..2e101496b 100644 --- a/lib/puppet/module_tool/repository.rb +++ b/lib/puppet/forge/repository.rb @@ -1,79 +1,121 @@ require 'net/http' require 'digest/sha1' require 'uri' -module Puppet::Module::Tool +require 'puppet/module_tool/utils' + +module Puppet::Forge + # Directory names that should not be checksummed. + ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage'] + FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/ + REPOSITORY_URL = Puppet.settings[:module_repository] # = Repository # # This class is a file for accessing remote repositories with modules. class Repository - include Utils::Interrogation + include Puppet::Module::Tool::Utils::Interrogation 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]) @uri = url.is_a?(::URI) ? url : ::URI.parse(url) @cache = Cache.new(self) 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+. # # Options: # * :authenticate => Request authentication on the terminal. Defaults to false. def make_http_request(request, options = {}) if options[:authenticate] authenticate(request) end 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( - Puppet::Module::Tool::http_proxy_host, - Puppet::Module::Tool::http_proxy_port + http_proxy_host, + http_proxy_port ).start(@uri.host, @uri.port) do |http| http.request(request) end rescue Errno::ECONNREFUSED, SocketError raise RuntimeError, "Could not reach remote repository" end end # Set the HTTP Basic Authentication parameters for the Net::HTTPRequest # +request+ by asking the user for input on the console. def authenticate(request) Puppet.notice "Authenticating for #{@uri}" email = prompt('Email Address') password = prompt('Password', true) request.basic_auth(email, password) 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 end end diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb index c11fd58b1..6d8d4dbd3 100644 --- a/lib/puppet/module_tool.rb +++ b/lib/puppet/module_tool.rb @@ -1,97 +1,61 @@ # Load standard libraries require 'pathname' require 'fileutils' require 'puppet/module_tool/utils' # Define tool module Puppet class Module module Tool # Directory names that should not be checksummed. ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage'] 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 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 - # Read HTTP proxy configurationm from Puppet's config file, or the - # http_proxy environment variable. - def self.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 self.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 self.http_proxy_port - env = http_proxy_env - - if env and env.port then - return env.port - end - - return Puppet.settings[:http_proxy_port] - end - def self.find_module_root(path) for dir in [path, Dir.pwd].compact if File.exist?(File.join(dir, 'Modulefile')) return dir end end raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}" end end end end # Load remaining libraries require 'puppet/module_tool/applications' -require 'puppet/module_tool/cache' 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/repository' require 'puppet/module_tool/skeleton' +require 'puppet/forge/cache' +require 'puppet/forge' diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb index 5f8c0c4bf..43d5c0428 100644 --- a/lib/puppet/module_tool/applications/application.rb +++ b/lib/puppet/module_tool/applications/application.rb @@ -1,83 +1,79 @@ require 'net/http' require 'semver' module Puppet::Module::Tool module Applications class Application include Utils::Interrogation def self.run(*args) new(*args).run end attr_accessor :options def initialize(options = {}) @options = options end - def repository - @repository ||= Repository.new(@options[:module_repository]) - 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 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) elsif require_modulefile raise ArgumentError, "No Modulefile found." end end @metadata end def load_modulefile! @metadata = nil metadata(true) end # Use to extract and validate a module name and version from a # filename # Note: Must have @filename set to use this def parse_filename! @release_name = File.basename(@filename,'.tar.gz') match = /^(.*?)-(.*?)-(\d+\.\d+\.\d+.*?)$/.match(@release_name) if match then @username, @module_name, @version = match.captures else raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})" end @full_module_name = [@username, @module_name].join('-') unless @username && @module_name raise ArgumentError, "Username and Module name not provided" end unless SemVer.valid?(@version) raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)" end end end end end diff --git a/lib/puppet/module_tool/applications/cleaner.rb b/lib/puppet/module_tool/applications/cleaner.rb index c42687fc7..b811983d7 100644 --- a/lib/puppet/module_tool/applications/cleaner.rb +++ b/lib/puppet/module_tool/applications/cleaner.rb @@ -1,16 +1,16 @@ module Puppet::Module::Tool module Applications class Cleaner < Application def run - Puppet::Module::Tool::Cache.clean + Puppet::Forge::Cache.clean # Return a status Hash containing the status of the clean command # and a status message. This return value is used by the module_tool # face clean action, and the status message, return_value[:msg], is # displayed on the console. # { :status => "success", :msg => "Cleaned module cache." } end end end end diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb index ad423bd83..d76e0e308 100644 --- a/lib/puppet/module_tool/applications/installer.rb +++ b/lib/puppet/module_tool/applications/installer.rb @@ -1,89 +1,54 @@ require 'open-uri' require 'pathname' require 'tmpdir' module Puppet::Module::Tool module Applications class Installer < Application def initialize(name, options = {}) + @forge = Puppet::Forge::Forge.new + @install_params = {} + if File.exist?(name) if File.directory?(name) # TODO Unify this handling with that of Unpacker#check_clobber! raise ArgumentError, "Module already installed: #{name}" end - @source = :filesystem @filename = File.expand_path(name) + @install_params[:source] = :filesystem + @install_params[:filename] = @filename parse_filename! else - @source = :repository + @install_params[:source] = :repository begin - @username, @module_name = Puppet::Module::Tool::username_and_modname_from(name) + @install_params[:author], @install_params[:modname] = Puppet::Module::Tool::username_and_modname_from(name) rescue ArgumentError raise "Could not install module with invalid name: #{name}" end - @version_requirement = options[:version] + @install_params[:version_requirement] = options[:version] end super(options) end def force? options[:force] end def run - case @source - when :repository - if match['file'] - begin - cache_path = repository.retrieve(match['file']) - rescue OpenURI::HTTPError => e - raise RuntimeError, "Could not install module: #{e.message}" - end - module_dir = Unpacker.run(cache_path, options) - else - raise RuntimeError, "Malformed response from module repository." - end - when :filesystem - repository = Repository.new('file:///') - uri = URI.parse("file://#{URI.escape(File.expand_path(@filename))}") - cache_path = repository.retrieve(uri) - module_dir = Unpacker.run(cache_path, options) - else - raise ArgumentError, "Could not determine installation source" - end + cache_path = @forge.get_release_package(@install_params) + module_dir = Unpacker.run(cache_path, options) # Return the Pathname object representing the path to the installed # module. This return value is used by the module_tool face install # action, and displayed to on the console. # # Example return value: # # "/etc/puppet/modules/apache" # module_dir end - - private - - def match - return @match ||= begin - url = repository.uri + "/users/#{@username}/modules/#{@module_name}/releases/find.json" - if @version_requirement - url.query = "version=#{URI.escape(@version_requirement)}" - end - begin - raw_result = read_match(url) - rescue => e - raise ArgumentError, "Could not find a release for this module (#{e.message})" - end - @match = PSON.parse(raw_result) - end - end - - def read_match(url) - return url.read - end end end end diff --git a/lib/puppet/module_tool/applications/searcher.rb b/lib/puppet/module_tool/applications/searcher.rb index 0a2267b3c..97028cd44 100644 --- a/lib/puppet/module_tool/applications/searcher.rb +++ b/lib/puppet/module_tool/applications/searcher.rb @@ -1,40 +1,16 @@ module Puppet::Module::Tool module Applications class Searcher < Application def initialize(term, options = {}) @term = term + @forge = Puppet::Forge::Forge.new super(options) end def run - request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(@term)}") - response = repository.make_http_request(request) - case response - when Net::HTTPOK - matches = PSON.parse(response.body) - else - raise RuntimeError, "Could not execute search (HTTP #{response.code})" - matches = [] - 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: - # - # [ - # { - # "name" => "nginx", - # "project_url" => "http://github.com/puppetlabs/puppetlabs-nginx", - # "version" => "0.0.1", - # "full_name" => "puppetlabs/nginx" # full_name comes back from - # } # API all to the forge. - # ] - # - matches + @forge.search(@term) end end end end diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb index 6dd1fca55..119eaf323 100644 --- a/lib/puppet/module_tool/applications/unpacker.rb +++ b/lib/puppet/module_tool/applications/unpacker.rb @@ -1,70 +1,70 @@ require 'pathname' require 'tmpdir' module Puppet::Module::Tool module Applications class Unpacker < Application def initialize(filename, options = {}) @filename = Pathname.new(filename) parse_filename! super(options) @module_dir = Pathname.new(options[:install_dir]) + @module_name end def run extract_module_to_install_dir tag_revision # 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 tag_revision File.open("#{@module_dir}/REVISION", 'w') do |f| f.puts "module: #{@username}/#{@module_name}" f.puts "version: #{@version}" f.puts "url: file://#{@filename.expand_path}" f.puts "installed: #{Time.now}" end end def extract_module_to_install_dir delete_existing_installation_or_abort! - build_dir = Puppet::Module::Tool::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}" + build_dir = Puppet::Forge::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}" build_dir.mkpath begin Puppet.notice "Installing #{@filename.basename} to #{@module_dir.expand_path}" 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? if !options[:force] Puppet.warning "Existing module '#{@module_dir.expand_path}' found" response = prompt "Overwrite module installed at #{@module_dir.expand_path}? [y/N]" unless response =~ /y/i raise RuntimeError, "Aborted installation." end end Puppet.warning "Deleting #{@module_dir.expand_path}" FileUtils.rm_rf @module_dir end end end end diff --git a/lib/puppet/module_tool/dependency.rb b/lib/puppet/module_tool/dependency.rb index bb55f5945..d0eead196 100644 --- a/lib/puppet/module_tool/dependency.rb +++ b/lib/puppet/module_tool/dependency.rb @@ -1,24 +1,24 @@ module Puppet::Module::Tool 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) @version_requirement = version_requirement - @repository = repository ? Repository.new(repository) : nil + @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/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb index 1067bfab3..bd3b12649 100644 --- a/spec/integration/module_tool_spec.rb +++ b/spec/integration/module_tool_spec.rb @@ -1,477 +1,475 @@ require 'spec_helper' require 'tmpdir' require 'fileutils' # FIXME This are helper methods that could be used by other tests in the # future, should we move these to a more central location def stub_repository_read(code, body) kind = Net::HTTPResponse.send(:response_class, code.to_s) response = kind.new('1.0', code.to_s, 'HTTP MESSAGE') response.stubs(:read_body).returns(body) - Puppet::Module::Tool::Repository.any_instance.stubs(:read_response).returns(response) + Puppet::Forge::Repository.any_instance.stubs(:read_response).returns(response) end def stub_installer_read(body) Puppet::Module::Tool::Applications::Installer.any_instance.stubs(:read_match).returns(body) end def stub_cache_read(body) - Puppet::Module::Tool::Cache.any_instance.stubs(:read_retrieve).returns(body) + Puppet::Forge::Cache.any_instance.stubs(:read_retrieve).returns(body) end # Return path to temparory directory for testing. def testdir return @testdir ||= tmpdir("module_tool_testdir") end # Create a temporary testing directory, change into it, and execute the # +block+. When the block exists, remove the test directory and change back # to the previous directory. def mktestdircd(&block) previousdir = Dir.pwd rmtestdir FileUtils.mkdir_p(testdir) Dir.chdir(testdir) block.call ensure rmtestdir Dir.chdir previousdir end # Remove the temporary test directory. def rmtestdir FileUtils.rm_rf(testdir) if File.directory?(testdir) end # END helper methods # Directory that contains sample releases. RELEASE_FIXTURES_DIR = File.join(PuppetSpec::FIXTURE_DIR, "releases") # Return the pathname string to the directory containing the release fixture called +name+. def release_fixture(name) return File.join(RELEASE_FIXTURES_DIR, name) end # Copy the release fixture called +name+ into the current working directory. def install_release_fixture(name) release_fixture(name) FileUtils.cp_r(release_fixture(name), name) end describe "module_tool", :fails_on_windows => true do include PuppetSpec::Files before do @tmp_confdir = Puppet[:confdir] = tmpdir("module_tool_test_confdir") @tmp_vardir = Puppet[:vardir] = tmpdir("module_tool_test_vardir") Puppet[:module_repository] = "http://forge.puppetlabs.com" @mytmpdir = Pathname.new(tmpdir("module_tool_test")) @options = {} @options[:install_dir] = @mytmpdir @options[:module_repository] = "http://forge.puppetlabs.com" end def build_and_install_module Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) FileUtils.mv("#{@full_module_name}/pkg/#{@release_name}.tar.gz", "#{@release_name}.tar.gz") FileUtils.rm_rf(@full_module_name) Puppet::Module::Tool::Applications::Installer.run("#{@release_name}.tar.gz", @options) end # Return STDOUT and STDERR output generated from +block+ as it's run within a temporary test directory. def run(&block) mktestdircd do block.call end end before :all do @username = "myuser" @module_name = "mymodule" @full_module_name = "#{@username}-#{@module_name}" @version = "0.0.1" @release_name = "#{@full_module_name}-#{@version}" end before :each do Puppet.settings.stubs(:parse) - Puppet::Module::Tool::Cache.clean + Puppet::Forge::Cache.clean end after :each do - Puppet::Module::Tool::Cache.clean + Puppet::Forge::Cache.clean end describe "generate" do it "should generate a module if given a dashed name" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) File.directory?(@full_module_name).should == true modulefile = File.join(@full_module_name, "Modulefile") File.file?(modulefile).should == true metadata = Puppet::Module::Tool::Metadata.new Puppet::Module::Tool::ModulefileReader.evaluate(metadata, modulefile) metadata.full_module_name.should == @full_module_name metadata.username.should == @username metadata.name.should == @module_name end end it "should fail if given an undashed name" do run do lambda { Puppet::Module::Tool::Applications::Generator.run("invalid") }.should raise_error(RuntimeError) end end it "should fail if directory already exists" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) lambda { Puppet::Module::Tool::Applications::Generator.run(@full_module_name) }.should raise_error(ArgumentError) end end it "should return an array of Pathname objects representing paths of generated files" do run do return_value = Puppet::Module::Tool::Applications::Generator.run(@full_module_name) return_value.each do |generated_file| generated_file.should be_kind_of(Pathname) end return_value.should be_kind_of(Array) end end end describe "build" do it "should build a module in a directory" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) File.directory?(File.join(@full_module_name, "pkg", @release_name)).should == true File.file?(File.join(@full_module_name, "pkg", @release_name + ".tar.gz")).should == true metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") File.file?(metadata_file).should == true metadata = PSON.parse(File.read(metadata_file)) metadata["name"].should == @full_module_name metadata["version"].should == @version metadata["checksums"].should be_a_kind_of(Hash) metadata["dependencies"].should == [] metadata["types"].should == [] end end it "should build a module's checksums" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") metadata = PSON.parse(File.read(metadata_file)) metadata["checksums"].should be_a_kind_of(Hash) modulefile_path = Pathname.new(File.join(@full_module_name, "Modulefile")) metadata["checksums"]["Modulefile"].should == Digest::MD5.hexdigest(modulefile_path.read) end end it "should build a module's types and providers" do run do name = "jamtur01-apache" install_release_fixture name Puppet::Module::Tool::Applications::Builder.run(name) metadata_file = File.join(name, "pkg", "#{name}-0.0.1", "metadata.json") metadata = PSON.parse(File.read(metadata_file)) metadata["types"].size.should == 1 type = metadata["types"].first type["name"].should == "a2mod" type["doc"].should == "Manage Apache 2 modules" type["parameters"].size.should == 1 type["parameters"].first.tap do |o| o["name"].should == "name" o["doc"].should == "The name of the module to be managed" end type["properties"].size.should == 1 type["properties"].first.tap do |o| o["name"].should == "ensure" o["doc"].should =~ /present.+absent/ end type["providers"].size.should == 1 type["providers"].first.tap do |o| o["name"].should == "debian" o["doc"].should =~ /Manage Apache 2 modules on Debian-like OSes/ end end end it "should build a module's dependencies" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) modulefile = File.join(@full_module_name, "Modulefile") dependency1_name = "anotheruser-anothermodule" dependency1_requirement = ">= 1.2.3" dependency2_name = "someuser-somemodule" dependency2_requirement = "4.2" dependency2_repository = "http://some.repo" File.open(modulefile, "a") do |handle| handle.puts "dependency '#{dependency1_name}', '#{dependency1_requirement}'" handle.puts "dependency '#{dependency2_name}', '#{dependency2_requirement}', '#{dependency2_repository}'" end Puppet::Module::Tool::Applications::Builder.run(@full_module_name) metadata_file = File.join(@full_module_name, "pkg", "#{@full_module_name}-#{@version}", "metadata.json") metadata = PSON.parse(File.read(metadata_file)) metadata['dependencies'].size.should == 2 metadata['dependencies'].sort_by{|t| t['name']}.tap do |dependencies| dependencies[0].tap do |dependency1| dependency1['name'].should == dependency1_name dependency1['version_requirement'].should == dependency1_requirement dependency1['repository'].should be_nil end dependencies[1].tap do |dependency2| dependency2['name'].should == dependency2_name dependency2['version_requirement'].should == dependency2_requirement dependency2['repository'].should == dependency2_repository end end end end it "should rebuild a module in a directory" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) end end it "should build a module in the current directory" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Dir.chdir(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) File.file?(File.join("pkg", @release_name + ".tar.gz")).should == true end end it "should fail to build a module without a Modulefile" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) FileUtils.rm(File.join(@full_module_name, "Modulefile")) lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) end end it "should fail to build a module directory that doesn't exist" do run do lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) end end it "should fail to build a module in the current directory that's not a module" do run do lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) }.should raise_error(ArgumentError) end end it "should return a Pathname object representing the path to the release archive." do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name).should be_kind_of(Pathname) end end end describe "search" do it "should display matching modules" do run do stub_repository_read 200, <<-HERE [ {"full_module_name": "cli", "version": "1.0"}, {"full_module_name": "web", "version": "2.0"} ] HERE Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).size.should == 2 end end it "should display no matches" do run do stub_repository_read 200, "[]" Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).should == [] end end it "should fail if can't get a connection" do run do stub_repository_read 500, "OH NOES!!1!" lambda { Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) }.should raise_error(RuntimeError) end end it "should return an array of module metadata hashes" do run do results = <<-HERE [ {"full_module_name": "cli", "version": "1.0"}, {"full_module_name": "web", "version": "2.0"} ] HERE expected = [ {"version"=>"1.0", "full_module_name"=>"cli"}, {"version"=>"2.0", "full_module_name"=>"web"} ] stub_repository_read 200, results return_value = Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) return_value.should == expected return_value.should be_kind_of(Array) end end end describe "install" do it "should install a module to the puppet modulepath by default" do myothertmpdir = Pathname.new(tmpdir("module_tool_test_myothertmpdir")) run do @options[:install_dir] = myothertmpdir Puppet::Module::Tool.unstub(:install_dir) build_and_install_module File.directory?(myothertmpdir + @module_name).should == true File.file?(myothertmpdir + @module_name + 'metadata.json').should == true end end it "should install a module from a filesystem path" do run do build_and_install_module File.directory?(@mytmpdir + @module_name).should == true File.file?(@mytmpdir + @module_name + 'metadata.json').should == true end end it "should install a module from a webserver URL" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") FileUtils.rm_rf(@full_module_name) - stub_installer_read <<-HERE - {"file": "/foo/bar/#{@release_name}.tar.gz", "version": "#{@version}"} - HERE + release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"} + Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release) Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options) File.directory?(@mytmpdir + @module_name).should == true File.file?(@mytmpdir + @module_name + 'metadata.json').should == true end end it "should install a module from a webserver URL using a version requirement" # TODO it "should fail if module isn't a slashed name" do run do lambda { Puppet::Module::Tool::Applications::Installer.run("invalid") }.should raise_error(RuntimeError) end end it "should fail if module doesn't exist on webserver" do run do stub_installer_read "{}" lambda { Puppet::Module::Tool::Applications::Installer.run("not-found", @options) }.should raise_error(RuntimeError) end end it "should fail gracefully when receiving invalid PSON" do pending "Implement PSON error wrapper" # TODO run do stub_installer_read "1/0" lambda { Puppet::Module::Tool::Applications::Installer.run("not-found") }.should raise_error(SystemExit) end end it "should fail if installing a module that's already installed" do run do name = "myuser-mymodule" Dir.mkdir name lambda { Puppet::Module::Tool::Applications::Installer.run(name) }.should raise_error(ArgumentError) end end it "should return a Pathname object representing the path to the installed module" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") FileUtils.rm_rf(@full_module_name) - stub_installer_read <<-HERE - {"file": "/foo/bar/#{@release_name}.tar.gz", "version": "#{@version}"} - HERE + release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"} + Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release) Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options).should be_kind_of(Pathname) end end end describe "clean" do require 'puppet/module_tool' it "should clean cache" do run do build_and_install_module - Puppet::Module::Tool::Cache.base_path.directory?.should == true + Puppet::Forge::Cache.base_path.directory?.should == true Puppet::Module::Tool::Applications::Cleaner.run - Puppet::Module::Tool::Cache.base_path.directory?.should == false + Puppet::Forge::Cache.base_path.directory?.should == false end end it "should return a status Hash" do run do build_and_install_module return_value = Puppet::Module::Tool::Applications::Cleaner.run return_value.should include(:msg) return_value.should include(:status) return_value.should be_kind_of(Hash) end end end describe "changes" do it "should return an array of modified files" do run do Puppet::Module::Tool::Applications::Generator.run(@full_module_name) Puppet::Module::Tool::Applications::Builder.run(@full_module_name) Dir.chdir("#{@full_module_name}/pkg/#{@release_name}") File.open("Modulefile", "a") do |handle| handle.puts handle.puts "# Added" end return_value = Puppet::Module::Tool::Applications::Checksummer.run(".") return_value.should include("Modulefile") return_value.should be_kind_of(Array) end end end end diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb new file mode 100644 index 000000000..6d8ce38f1 --- /dev/null +++ b/spec/unit/forge/repository_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'net/http' +require 'puppet/forge/repository' +require 'puppet/forge/cache' + +describe Puppet::Forge::Repository do + describe 'instances' do + + let(:repository) { Puppet::Forge::Repository.new('http://fake.com') } + + describe '#make_http_request' do + before do + # Do a mock of the Proxy call so we can do proper expects for + # Net::HTTP + Net::HTTP.expects(:Proxy).returns(Net::HTTP) + Net::HTTP.expects(:start) + end + context "when not given an :authenticate option" do + it "should authenticate" do + repository.expects(:authenticate).never + repository.make_http_request(nil) + end + end + context "when given an :authenticate option" do + it "should authenticate" do + repository.expects(:authenticate) + repository.make_http_request(nil, :authenticate => true) + end + end + end + + describe '#authenticate' do + it "should set basic auth on the request" do + authenticated_request = stub + authenticated_request.expects(:basic_auth) + repository.expects(:prompt).twice + repository.authenticate(authenticated_request) + end + end + + describe '#retrieve' do + before do + @uri = URI.parse('http://some.url.com') + end + + it "should access the cache" do + repository.cache.expects(:retrieve).with(@uri) + repository.retrieve(@uri) + end + end + + describe 'http_proxy support' do + before :each do + ENV["http_proxy"] = nil + end + + after :each do + ENV["http_proxy"] = nil + 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 "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) + + repository.http_proxy_port.should == 7456 + repository.http_proxy_host.should == "test.com" + 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.http_proxy_host.should == "test1.com" + repository.http_proxy_port.should == 8011 + end + end + end +end diff --git a/spec/unit/forge_spec.rb b/spec/unit/forge_spec.rb new file mode 100644 index 000000000..905f1bd24 --- /dev/null +++ b/spec/unit/forge_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'puppet/forge' +require 'net/http' + +describe Puppet::Forge::Forge do + before do + Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response) + Puppet::Forge::Repository.any_instance.stubs(:retrieve).returns("/tmp/foo") + end + + let(:forge) { forge = Puppet::Forge::Forge.new('http://forge.puppetlabs.com') } + + 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 + + let(:response) { stub(:body => response_body, :code => '200') } + 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 + + it "should return a list of matches from the forge" do + forge.search('bacula').should == PSON.load(response_body) + end + end + + context "when the connection to the forge fails" do + let(:response) { stub(:body => '[]', :code => '404') } + + it "should raise an error" do + lambda { forge.search('bacula') }.should raise_error RuntimeError + end + end + end + + describe "the behavior of the get_release_package method" do + + let(:response) do + response = mock() + response.stubs(:body).returns('{"file": "/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version": "0.0.3"}') + response + end + + context "when source is not filesystem or repository" do + it "should raise an error" do + params = { :source => 'foo' } + lambda { forge.get_release_package(params) }.should + raise_error(ArgumentError, "Could not determine installation source") + end + end + + context "when the source is a repository" do + let(:params) do + { + :source => :repository, + :author => 'fakeauthor', + :modname => 'fakemodule', + :version => '0.0.1' + } + end + + it "should require author" do + params.delete(:author) + lambda { forge.get_release_package(params) }.should + raise_error(ArgumentError, ":author and :modename required") + end + + it "should require modname" do + params.delete(:modname) + lambda { forge.get_release_package(params) }.should + raise_error(ArgumentError, ":author and :modename required") + end + + it "should download the release package" do + forge.get_release_package(params).should == "/tmp/foo" + end + end + + context "when the source is a filesystem" do + it "should require filename" do + params = { :source => :filesystem } + lambda { forge.get_release_package(params) }.should + raise_error(ArgumentError, ":filename required") + end + end + end + + describe "the behavior of the get_releases method" do + let(:response) do + response = mock() + response.stubs(:body).returns('{"releases": [{"version": "0.0.1"}, {"version": "0.0.2"}, {"version": "0.0.3"}]}') + response + end + + it "should return a list of module releases" do + forge.get_releases('fakeauthor', 'fakemodule').should == ["0.0.1", "0.0.2", "0.0.3"] + end + end +end diff --git a/spec/unit/module_tool/repository_spec.rb b/spec/unit/module_tool/repository_spec.rb deleted file mode 100644 index 69be1661e..000000000 --- a/spec/unit/module_tool/repository_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' -require 'net/http' -require 'puppet/module_tool' - -describe Puppet::Module::Tool::Repository do - describe 'instances' do - before do - @repository = described_class.new('http://fake.com') - end - - describe '#make_http_request' do - before do - # Do a mock of the Proxy call so we can do proper expects for - # Net::HTTP - Net::HTTP.expects(:Proxy).returns(Net::HTTP) - Net::HTTP.expects(:start) - end - context "when not given an :authenticate option" do - it "should authenticate" do - @repository.expects(:authenticate).never - @repository.make_http_request(nil) - end - end - context "when given an :authenticate option" do - it "should authenticate" do - @repository.expects(:authenticate) - @repository.make_http_request(nil, :authenticate => true) - end - end - end - - describe '#authenticate' do - it "should set basic auth on the request" do - authenticated_request = stub - authenticated_request.expects(:basic_auth) - @repository.expects(:prompt).twice - @repository.authenticate(authenticated_request) - end - end - - describe '#retrieve' do - before do - @uri = URI.parse('http://some.url.com') - end - - it "should access the cache" do - @repository.cache.expects(:retrieve).with(@uri) - @repository.retrieve(@uri) - end - end - end -end diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb index 15ca6c766..86d421e69 100644 --- a/spec/unit/module_tool_spec.rb +++ b/spec/unit/module_tool_spec.rb @@ -1,38 +1,5 @@ require 'spec_helper' require 'puppet/module_tool' describe Puppet::Module::Tool do - describe 'http_proxy support' do - before :each do - ENV["http_proxy"] = nil - end - - after :each do - ENV["http_proxy"] = nil - end - - it "should support environment variable for port and host" do - ENV["http_proxy"] = "http://test.com:8011" - described_class.http_proxy_host.should == "test.com" - described_class.http_proxy_port.should == 8011 - 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) - - described_class.http_proxy_port.should == 7456 - described_class.http_proxy_host.should == "test.com" - 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) - - described_class.http_proxy_host.should == "test1.com" - described_class.http_proxy_port.should == 8011 - end - end end