diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index d3cba04cb..7f65e3488 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -1,77 +1,78 @@ require 'puppet/node/facts' require 'puppet/indirector/code' class Puppet::Node::Facts::Facter < Puppet::Indirector::Code desc "Retrieve facts from Facter. This provides a somewhat abstract interface between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." def destroy(facts) raise Puppet::DevError, 'You cannot destroy facts in the code store; it is only used for getting facts from Facter' end def save(facts) raise Puppet::DevError, 'You cannot save facts to the code store; it is only used for getting facts from Facter' end # Lookup a host's facts up in Facter. def find(request) Facter.reset self.class.setup_external_search_paths(request) if Puppet.features.external_facts? self.class.setup_search_paths(request) result = Puppet::Node::Facts.new(request.key, Facter.to_hash) result.add_local_facts Puppet[:stringify_facts] ? result.stringify : result.sanitize result end private def self.setup_search_paths(request) # Add any per-module fact directories to facter's search path dirs = request.environment.modulepath.collect do |dir| ['lib', 'plugins'].map do |subdirectory| Dir.glob("#{dir}/*/#{subdirectory}/facter") end end.flatten + Puppet[:factpath].split(File::PATH_SEPARATOR) dirs = dirs.select do |dir| next false unless FileTest.directory?(dir) # Even through we no longer directly load facts in the terminus, # print out each .rb in the facts directory as module # developers may find that information useful for debugging purposes if Puppet::Util::Log.sendlevel?(:info) + Puppet.info "Loading facts" Dir.glob("#{dir}/*.rb").each do |file| - Puppet.info "Loading facts from #{file}" + Puppet.debug "Loading facts from #{file}" end end true end Facter.search *dirs end def self.setup_external_search_paths(request) # Add any per-module external fact directories to facter's external search path dirs = [] request.environment.modules.each do |m| if m.has_external_facts? dir = m.plugin_fact_directory - Puppet.info "Loading external facts from #{dir}" + Puppet.debug "Loading external facts from #{dir}" dirs << dir end end # Add system external fact directory if it exists if FileTest.directory?(Puppet[:pluginfactdest]) dir = Puppet[:pluginfactdest] - Puppet.info "Loading external facts from #{dir}" + Puppet.debug "Loading external facts from #{dir}" dirs << dir end Facter.search_external dirs end end diff --git a/lib/puppet/module_tool/applications/builder.rb b/lib/puppet/module_tool/applications/builder.rb index 6ca9cab65..0390e4638 100644 --- a/lib/puppet/module_tool/applications/builder.rb +++ b/lib/puppet/module_tool/applications/builder.rb @@ -1,93 +1,107 @@ require 'fileutils' require 'json' module Puppet::ModuleTool module Applications class Builder < Application def initialize(path, options = {}) @path = File.expand_path(path) @pkg_path = File.join(@path, 'pkg') super(options) end def run load_metadata! + sanity_check create_directory copy_contents write_json Puppet.notice "Building #{@path} for release" pack relative = Pathname.new(archive_file).relative_path_from(Pathname.new(File.expand_path(Dir.pwd))) # Return the Pathname object representing the path to the release # archive just created. This return value is used by the module_tool # face build action, and displayed to on the console using the to_s # method. # # Example return value: # # # relative end private def archive_file File.join(@pkg_path, "#{metadata.release_name}.tar.gz") end def pack FileUtils.rm archive_file rescue nil tar = Puppet::ModuleTool::Tar.instance Dir.chdir(@pkg_path) do tar.pack(metadata.release_name, archive_file) end end def create_directory FileUtils.mkdir(@pkg_path) rescue nil if File.directory?(build_path) FileUtils.rm_rf(build_path, :secure => true) end FileUtils.mkdir(build_path) end def copy_contents Dir[File.join(@path, '*')].each do |path| case File.basename(path) when *Puppet::ModuleTool::ARTIFACTS next else FileUtils.cp_r path, build_path, :preserve => true end end end + def sanity_check + symlinks = Dir.glob("#{@path}/**/*", File::FNM_DOTMATCH).map { |f| Pathname.new(f) }.select(&:symlink?) + dirpath = Pathname.new @path + + unless symlinks.empty? + symlinks.each do |s| + Puppet.warning "Symlinks in modules are unsupported. Please investigate symlink #{s.relative_path_from dirpath}->#{s.realpath.relative_path_from dirpath}." + end + + raise Puppet::ModuleTool::Errors::ModuleToolError, "Found symlinks. Symlinks in modules are not allowed, please remove them." + end + end + def write_json metadata_path = File.join(build_path, 'metadata.json') if metadata.to_hash.include? 'checksums' Puppet.warning "A 'checksums' field was found in metadata.json. This field will be ignored and can safely be removed." end # TODO: This may necessarily change the order in which the metadata.json # file is packaged from what was written by the user. This is a # regretable, but required for now. File.open(metadata_path, 'w') do |f| f.write(metadata.to_json) end File.open(File.join(build_path, 'checksums.json'), 'w') do |f| f.write(PSON.pretty_generate(Checksums.new(build_path))) end end def build_path @build_path ||= File.join(@pkg_path, metadata.release_name) end end end end diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb index 6673c9b12..65819e274 100644 --- a/lib/puppet/module_tool/applications/unpacker.rb +++ b/lib/puppet/module_tool/applications/unpacker.rb @@ -1,86 +1,99 @@ require 'pathname' require 'tmpdir' require 'json' module Puppet::ModuleTool module Applications class Unpacker < Application def self.unpack(filename, target) app = self.new(filename, :target_dir => target) app.unpack + app.sanity_check app.move_into(target) end def self.harmonize_ownership(source, target) unless Puppet.features.microsoft_windows? source = Pathname.new(source) unless source.respond_to?(:stat) target = Pathname.new(target) unless target.respond_to?(:stat) FileUtils.chown_R(source.stat.uid, source.stat.gid, target) end end def initialize(filename, options = {}) @filename = Pathname.new(filename) super(options) @module_path = Pathname(options[:target_dir]) end def run unpack + sanity_check module_dir = @module_path + module_name move_into(module_dir) # Return the Pathname object representing the directory where the # module release archive was unpacked the to. return module_dir end + # @api private + # Error on symlinks and other junk + def sanity_check + symlinks = Dir.glob("#{tmpdir}/**/*", File::FNM_DOTMATCH).map { |f| Pathname.new(f) }.select(&:symlink?) + tmpdirpath = Pathname.new tmpdir + + symlinks.each do |s| + Puppet.warning "Symlinks in modules are unsupported. Please investigate symlink #{s.relative_path_from tmpdirpath}->#{s.realpath.relative_path_from tmpdirpath}." + end + end + # @api private def unpack begin Puppet::ModuleTool::Tar.instance.unpack(@filename.to_s, tmpdir, [@module_path.stat.uid, @module_path.stat.gid].join(':')) rescue Puppet::ExecutionFailure => e raise RuntimeError, "Could not extract contents of module archive: #{e.message}" end end # @api private def root_dir return @root_dir if @root_dir # Grab the first directory containing a metadata.json file metadata_file = Dir["#{tmpdir}/**/metadata.json"].sort_by(&:length)[0] if metadata_file @root_dir = Pathname.new(metadata_file).dirname else raise "No valid metadata.json found!" end end # @api private def module_name metadata = JSON.parse((root_dir + 'metadata.json').read) name = metadata['name'][/-(.*)/, 1] end # @api private def move_into(dir) dir = Pathname.new(dir) dir.rmtree if dir.exist? FileUtils.mv(root_dir, dir) ensure FileUtils.rmtree(tmpdir) end # Obtain a suitable temporary path for unpacking tarballs # # @api private # @return [String] path to temporary unpacking location def tmpdir @dir ||= Dir.mktmpdir('tmp-unpacker', Puppet::Forge::Cache.base_path) end end end end diff --git a/spec/unit/module_tool/applications/builder_spec.rb b/spec/unit/module_tool/applications/builder_spec.rb index b9d7d3d79..8891b60da 100644 --- a/spec/unit/module_tool/applications/builder_spec.rb +++ b/spec/unit/module_tool/applications/builder_spec.rb @@ -1,88 +1,109 @@ require 'spec_helper' require 'puppet/module_tool/applications' require 'puppet_spec/modules' describe Puppet::ModuleTool::Applications::Builder do include PuppetSpec::Files let(:path) { tmpdir("working_dir") } let(:module_name) { 'mymodule-mytarball' } let(:version) { '0.0.1' } let(:release_name) { "#{module_name}-#{version}" } let(:tarball) { File.join(path, 'pkg', release_name) + ".tar.gz" } let(:builder) { Puppet::ModuleTool::Applications::Builder.new(path) } shared_examples "a packagable module" do def target_exists?(file) File.exist?(File.join(path, "pkg", "#{module_name}-#{version}", file)) end it "packages the module in a tarball named after the module" do tarrer = mock('tarrer') Puppet::ModuleTool::Tar.expects(:instance).returns(tarrer) Dir.expects(:chdir).with(File.join(path, 'pkg')).yields tarrer.expects(:pack).with(release_name, tarball) builder.run + + expect(target_exists?('checksums.json')).to be true + expect(target_exists?('metadata.json')).to be true end end context 'with metadata.json' do before :each do File.open(File.join(path, 'metadata.json'), 'w') do |f| f.puts({ "name" => "#{module_name}", "version" => "#{version}", "source" => "http://github.com/testing/#{module_name}", "author" => "testing", "license" => "Apache License Version 2.0", "summary" => "Puppet testing module", "description" => "This module can be used for basic testing", "project_page" => "http://github.com/testing/#{module_name}" }.to_json) end end it_behaves_like "a packagable module" + + it "does not package with a symlink" do + FileUtils.touch(File.join(path, 'tempfile')) + File.symlink(File.join(path, 'tempfile'), File.join(path, 'tempfile2')) + + expect { + builder.run + }.to raise_error Puppet::ModuleTool::Errors::ModuleToolError, /symlinks/i + end + + it "does not package with a symlink in a subdir" do + FileUtils.mkdir(File.join(path, 'manifests')) + FileUtils.touch(File.join(path, 'manifests/tempfile.pp')) + File.symlink(File.join(path, 'manifests/tempfile.pp'), File.join(path, 'manifests/tempfile2.pp')) + + expect { + builder.run + }.to raise_error Puppet::ModuleTool::Errors::ModuleToolError, /symlinks/i + end end context 'with metadata.json containing checksums' do before :each do File.open(File.join(path, 'metadata.json'), 'w') do |f| f.puts({ "name" => "#{module_name}", "version" => "#{version}", "source" => "http://github.com/testing/#{module_name}", "author" => "testing", "license" => "Apache License Version 2.0", "summary" => "Puppet testing module", "description" => "This module can be used for basic testing", "project_page" => "http://github.com/testing/#{module_name}", "checksums" => {"README.md" => "deadbeef"} }.to_json) end end it_behaves_like "a packagable module" end - context 'with Modulefile' do before :each do File.open(File.join(path, 'Modulefile'), 'w') do |f| f.write <<-MODULEFILE name '#{module_name}' version '#{version}' source 'http://github.com/testing/#{module_name}' author 'testing' license 'Apache License Version 2.0' summary 'Puppet testing module' description 'This module can be used for basic testing' project_page 'http://github.com/testing/#{module_name}' MODULEFILE end end it_behaves_like "a packagable module" end end diff --git a/spec/unit/module_tool/applications/unpacker_spec.rb b/spec/unit/module_tool/applications/unpacker_spec.rb index 39b0c261f..1ff93e180 100644 --- a/spec/unit/module_tool/applications/unpacker_spec.rb +++ b/spec/unit/module_tool/applications/unpacker_spec.rb @@ -1,34 +1,73 @@ require 'spec_helper' require 'json' require 'puppet/module_tool/applications' require 'puppet_spec/modules' describe Puppet::ModuleTool::Applications::Unpacker do include PuppetSpec::Files let(:target) { tmpdir("unpacker") } let(:module_name) { 'myusername-mytarball' } let(:filename) { tmpdir("module") + "/module.tar.gz" } let(:working_dir) { tmpdir("working_dir") } before :each do Puppet.settings[:module_working_dir] = working_dir end it "should attempt to untar file to temporary location" do untar = mock('Tar') untar.expects(:unpack).with(filename, anything()) do |src, dest, _| FileUtils.mkdir(File.join(dest, 'extractedmodule')) File.open(File.join(dest, 'extractedmodule', 'metadata.json'), 'w+') do |file| file.puts JSON.generate('name' => module_name, 'version' => '1.0.0') end true end Puppet::ModuleTool::Tar.expects(:instance).returns(untar) Puppet::ModuleTool::Applications::Unpacker.run(filename, :target_dir => target) File.should be_directory(File.join(target, 'mytarball')) end + + it "should warn about symlinks" do + untar = mock('Tar') + untar.expects(:unpack).with(filename, anything()) do |src, dest, _| + FileUtils.mkdir(File.join(dest, 'extractedmodule')) + File.open(File.join(dest, 'extractedmodule', 'metadata.json'), 'w+') do |file| + file.puts JSON.generate('name' => module_name, 'version' => '1.0.0') + end + FileUtils.touch(File.join(dest, 'extractedmodule/tempfile')) + File.symlink(File.join(dest, 'extractedmodule/tempfile'), File.join(dest, 'extractedmodule/tempfile2')) + true + end + + Puppet::ModuleTool::Tar.expects(:instance).returns(untar) + Puppet.expects(:warning).with(regexp_matches(/symlinks/i)) + + Puppet::ModuleTool::Applications::Unpacker.run(filename, :target_dir => target) + File.should be_directory(File.join(target, 'mytarball')) + end + + it "should warn about symlinks in subdirectories" do + untar = mock('Tar') + untar.expects(:unpack).with(filename, anything()) do |src, dest, _| + FileUtils.mkdir(File.join(dest, 'extractedmodule')) + File.open(File.join(dest, 'extractedmodule', 'metadata.json'), 'w+') do |file| + file.puts JSON.generate('name' => module_name, 'version' => '1.0.0') + end + FileUtils.mkdir(File.join(dest, 'extractedmodule/manifests')) + FileUtils.touch(File.join(dest, 'extractedmodule/manifests/tempfile')) + File.symlink(File.join(dest, 'extractedmodule/manifests/tempfile'), File.join(dest, 'extractedmodule/manifests/tempfile2')) + true + end + + Puppet::ModuleTool::Tar.expects(:instance).returns(untar) + Puppet.expects(:warning).with(regexp_matches(/symlinks/i)) + + Puppet::ModuleTool::Applications::Unpacker.run(filename, :target_dir => target) + File.should be_directory(File.join(target, 'mytarball')) + end end