diff --git a/acceptance/tests/pluginsync/3935_pluginsync_should_follow_symlinks.rb b/acceptance/tests/pluginsync/3935_pluginsync_should_follow_symlinks.rb new file mode 100644 index 000000000..36273b737 --- /dev/null +++ b/acceptance/tests/pluginsync/3935_pluginsync_should_follow_symlinks.rb @@ -0,0 +1,44 @@ +test_name "pluginsync should not error when modulepath is a symlink and no modules have plugin directories" + +step "Create a modulepath directory which is a symlink and includes a module without facts.d or lib directories" +basedir = master.tmpdir("symlink_modulepath") + +target = "#{basedir}/target_dir" +test_module_dir = "#{target}/module1" +link_dest = "#{basedir}/link_dest" +modulepath = "#{link_dest}" +modulepath << "#{master['sitemoduledir']}" if master.is_pe? + +apply_manifest_on(master, < true) +File { + ensure => directory, + mode => "0750", + owner => #{master.puppet['user']}, + group => #{master.puppet['group']}, +} + +file { + '#{basedir}':; + '#{target}':; + '#{test_module_dir}':; +} + +file { '#{link_dest}': + ensure => link, + target => '#{target}', +} +MANIFEST + +master_opts = { + 'main' => { + 'basemodulepath' => "#{modulepath}" + } +} + +with_puppet_running_on master, master_opts, basedir do + agents.each do |agent| + on(agent, puppet('agent', "-t --server #{master}")) + assert_no_match(/Could not retrieve information from environment production source\(s\) puppet:\/\/\/pluginfacts/, stderr) + assert_no_match(/Could not retrieve information from environment production source\(s\) puppet:\/\/\/plugins/, stderr) + end +end diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index 319546b03..c7bec3882 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -1,66 +1,67 @@ require 'puppet/configurer' require 'puppet/resource/catalog' class Puppet::Configurer::Downloader attr_reader :name, :path, :source, :ignore # Evaluate our download, returning the list of changed values. def evaluate Puppet.info "Retrieving #{name}" files = [] begin catalog.apply do |trans| trans.changed?.each do |resource| yield resource if block_given? files << resource[:path] end end rescue Puppet::Error => detail Puppet.log_exception(detail, "Could not retrieve #{name}: #{detail}") end files end def initialize(name, path, source, ignore = nil, environment = nil, source_permissions = :ignore) @name, @path, @source, @ignore, @environment, @source_permissions = name, path, source, ignore, environment, source_permissions end def catalog catalog = Puppet::Resource::Catalog.new("PluginSync", @environment) catalog.host_config = false catalog.add_resource(file) catalog end def file args = default_arguments.merge(:path => path, :source => source) args[:ignore] = ignore.split if ignore Puppet::Type.type(:file).new(args) end private def default_arguments defargs = { :path => path, :recurse => true, + :links => :follow, :source => source, :source_permissions => @source_permissions, :tag => name, :purge => true, :force => true, :backup => false, :noop => false } if !Puppet.features.microsoft_windows? defargs.merge!( { :owner => Process.uid, :group => Process.gid } ) end return defargs end end diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index 1f79ae74d..ca7e94dbf 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -1,222 +1,228 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/configurer/downloader' describe Puppet::Configurer::Downloader do require 'puppet_spec/files' include PuppetSpec::Files let(:path) { Puppet[:plugindest] } let(:source) { 'puppet://puppet/plugins' } it "should require a name" do expect { Puppet::Configurer::Downloader.new }.to raise_error(ArgumentError) end it "should require a path and a source at initialization" do expect { Puppet::Configurer::Downloader.new("name") }.to raise_error(ArgumentError) end it "should set the name, path and source appropriately" do dler = Puppet::Configurer::Downloader.new("facts", "path", "source") expect(dler.name).to eq("facts") expect(dler.path).to eq("path") expect(dler.source).to eq("source") end def downloader(options = {}) options[:name] ||= "facts" options[:path] ||= path options[:source_permissions] ||= :ignore Puppet::Configurer::Downloader.new(options[:name], options[:path], source, options[:ignore], options[:environment], options[:source_permissions]) end def generate_file_resource(options = {}) dler = downloader(options) dler.file end describe "when creating the file that does the downloading" do it "should create a file instance with the right path and source" do file = generate_file_resource(:path => path, :source => source) expect(file[:path]).to eq(path) expect(file[:source]).to eq([source]) end it "should tag the file with the downloader name" do name = "mydownloader" file = generate_file_resource(:name => name) expect(file[:tag]).to eq([name]) end it "should always recurse" do file = generate_file_resource expect(file[:recurse]).to be_truthy end + it "should follow links by default" do + file = generate_file_resource + + expect(file[:links]).to eq(:follow) + end + it "should always purge" do file = generate_file_resource expect(file[:purge]).to be_truthy end it "should never be in noop" do file = generate_file_resource expect(file[:noop]).to be_falsey end it "should set source_permissions to ignore by default" do file = generate_file_resource expect(file[:source_permissions]).to eq(:ignore) end it "should allow source_permissions to be overridden" do file = generate_file_resource(:source_permissions => :use) expect(file[:source_permissions]).to eq(:use) end describe "on POSIX", :if => Puppet.features.posix? do it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 file = generate_file_resource(:path => '/path') expect(file[:owner]).to eq(51) end it "should always set the group to the current GID" do Process.expects(:gid).returns 61 file = generate_file_resource(:path => '/path') expect(file[:group]).to eq(61) end end describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should omit the owner" do file = generate_file_resource(:path => 'C:/path') expect(file[:owner]).to be_nil end it "should omit the group" do file = generate_file_resource(:path => 'C:/path') expect(file[:group]).to be_nil end end it "should always force the download" do file = generate_file_resource expect(file[:force]).to be_truthy end it "should never back up when downloading" do file = generate_file_resource expect(file[:backup]).to be_falsey end it "should support providing an 'ignore' parameter" do file = generate_file_resource(:ignore => '.svn') expect(file[:ignore]).to eq(['.svn']) end it "should split the 'ignore' parameter on whitespace" do file = generate_file_resource(:ignore => '.svn CVS') expect(file[:ignore]).to eq(['.svn', 'CVS']) end end describe "when creating the catalog to do the downloading" do before do @path = make_absolute("/download/path") @dler = Puppet::Configurer::Downloader.new("foo", @path, make_absolute("source")) end it "should create a catalog and add the file to it" do catalog = @dler.catalog expect(catalog.resources.size).to eq(1) expect(catalog.resources.first.class).to eq(Puppet::Type::File) expect(catalog.resources.first.name).to eq(@path) end it "should specify that it is not managing a host catalog" do expect(@dler.catalog.host_config).to eq(false) end end describe "when downloading" do before do @dl_name = tmpfile("downloadpath") source_name = tmpfile("source") File.open(source_name, 'w') {|f| f.write('hola mundo') } env = Puppet::Node::Environment.remote('foo') @dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name, Puppet[:pluginsignore], env) end it "should not skip downloaded resources when filtering on tags" do Puppet[:tags] = 'maytag' @dler.evaluate expect(Puppet::FileSystem.exist?(@dl_name)).to be_truthy end it "should log that it is downloading" do Puppet.expects(:info) @dler.evaluate end it "should return all changed file paths" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) expect(@dler.evaluate).to eq(%w{/changed/file}) end it "should yield the resources if a block is given" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) yielded = nil @dler.evaluate { |r| yielded = r } expect(yielded).to eq(resource) end it "should catch and log exceptions" do Puppet.expects(:err) # The downloader creates a new catalog for each apply, and really the only object # that it is possible to stub for the purpose of generating a puppet error Puppet::Resource::Catalog.any_instance.stubs(:apply).raises(Puppet::Error, "testing") expect { @dler.evaluate }.not_to raise_error end end end