diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb index fac0d1961..b94556e84 100644 --- a/lib/puppet/module_tool/applications/application.rb +++ b/lib/puppet/module_tool/applications/application.rb @@ -1,82 +1,82 @@ require 'net/http' 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 SystemExit, "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) - Modulefile.evaluate(@metadata, modulefile_path) + Puppet::Module::Tool::ModulefileReader.evaluate(@metadata, modulefile_path) elsif require_modulefile raise SystemExit, "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 SystemExit, "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 SystemExit, "Username and Module name not provided" end if @version !~ /^(\d+)\.(\d+)\.(\d+)([a-zA-Z][a-zA-Z0-9-]*){0,1}$/ then raise SystemExit, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)" end end end end end diff --git a/lib/puppet/module_tool/modulefile.rb b/lib/puppet/module_tool/modulefile.rb index f09074cfe..35c1e9973 100644 --- a/lib/puppet/module_tool/modulefile.rb +++ b/lib/puppet/module_tool/modulefile.rb @@ -1,75 +1,75 @@ module Puppet::Module::Tool # = Modulefile # # This class provides the DSL used for evaluating the module's 'Modulefile'. # These methods are used to concisely define this module's attributes, which # are later rendered as PSON into a 'metadata.json' file. - class Modulefile + class ModulefileReader # Read the +filename+ and eval its Ruby code to set values in the Metadata # +metadata+ instance. def self.evaluate(metadata, filename) returning(new(metadata)) do |builder| if File.file?(filename) builder.instance_eval(File.read(filename.to_s), filename.to_s, 1) else Puppet.warning "No Modulefile: #{filename}" end end end # Instantiate with the Metadata +metadata+ instance. def initialize(metadata) @metadata = metadata end # Set the +full_module_name+ (e.g. "myuser-mymodule"), which will also set the # +username+ and module +name+. Required. def name(name) @metadata.full_module_name = name end # Set the module +version+ (e.g., "0.0.1"). Required. def version(version) @metadata.version = version end # Add a dependency with the full_module_name +name+ (e.g. "myuser-mymodule"), an # optional +version_requirement+ (e.g. "0.0.1") and +repository+ (a URL # string). Optional. Can be called multiple times to add many dependencies. def dependency(name, version_requirement = nil, repository = nil) @metadata.dependencies << Dependency.new(name, version_requirement, repository) end # Set the source def source(source) @metadata.source = source end # Set the author or default to +username+ def author(author) @metadata.author = author end # Set the license def license(license) @metadata.license = license end # Set the summary def summary(summary) @metadata.summary = summary end # Set the description def description(description) @metadata.description = description end # Set the project page def project_page(project_page) @metadata.project_page = project_page end end end diff --git a/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb index 6fcf75afd..069dae99c 100644 --- a/spec/integration/module_tool_spec.rb +++ b/spec/integration/module_tool_spec.rb @@ -1,477 +1,477 @@ 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) 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) 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" 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 end after :each do Puppet::Module::Tool::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::Modulefile.evaluate(metadata, modulefile) + 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(SystemExit) 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(SystemExit) 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(SystemExit) 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 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(SystemExit) 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(SystemExit) 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(SystemExit) 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 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::Module::Tool::Applications::Cleaner.run Puppet::Module::Tool::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