Skip to content

Commit d07d37a

Browse files
author
Ciprian Badescu
committed
(PUP-10946) add max-files parameter for file and tidy resources
Adds a new parameter, `max-files`, to limit the number of resources that may be created by types supporting recursion(file, tidy). Default value for `max-files` is 0. In this case a warning is logged in case the number of files exceeds hardcoded soft limit of 1000. In case `max-files` is set to a value different that 0 and the number of files is greater than `max-files`, a Puppet::Error is raised and the resource is marked as failed.
1 parent 95130f8 commit d07d37a

File tree

9 files changed

+128
-23
lines changed

9 files changed

+128
-23
lines changed

lib/puppet/file_serving/fileset.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# Operate recursively on a path, returning a set of file paths.
66
class Puppet::FileServing::Fileset
77
attr_reader :path, :ignore, :links
8-
attr_accessor :recurse, :recurselimit, :checksum_type
8+
attr_accessor :recurse, :recurselimit, :max_files, :checksum_type
99

1010
# Produce a hash of files, with merged so that earlier files
1111
# with the same postfix win. E.g., /dir1/subfile beats /dir2/subfile.
@@ -40,6 +40,7 @@ def initialize(path, options = {})
4040
self.links = :manage
4141
@recurse = false
4242
@recurselimit = :infinite
43+
@max_files = 0
4344

4445
if options.is_a?(Puppet::Indirector::Request)
4546
initialize_from_request(options)
@@ -58,6 +59,13 @@ def initialize(path, options = {})
5859
# level deep, which Find doesn't do.
5960
def files
6061
files = perform_recursion
62+
soft_max_files = 1000
63+
64+
if max_files != 0 && files.size > max_files
65+
raise Puppet::Error.new _("The directory '%{path}' contains %{entries} entries, which exceeds the limit of %{max_files} specified by the max_files parameter for this resource. The limit may be increased, but be aware that large number of file resources can result in excessive resource consumption and degraded performance. Consider using an alternate method to manage large directory trees") % { path: path, entries: files.size, max_files: max_files }
66+
elsif max_files == 0 && files.size > soft_max_files
67+
Puppet.warning _("The directory '%{path}' contains %{entries} entries, which exceeds the default soft limit %{max_files} and may cause excessive resource consumption and degraded performance. To remove this warning set a value for `max_files` parameter or consider using an alternate method to manage large directory trees") % { path: path, entries: files.size, max_files: soft_max_files }
68+
end
6169

6270
# Now strip off the leading path, so each file becomes relative, and remove
6371
# any slashes that might end up at the beginning of the path.
@@ -96,7 +104,7 @@ def initialize_from_hash(options)
96104
end
97105

98106
def initialize_from_request(request)
99-
[:links, :ignore, :recurse, :recurselimit, :checksum_type].each do |param|
107+
[:links, :ignore, :recurse, :recurselimit, :max_files, :checksum_type].each do |param|
100108
if request.options.include?(param) # use 'include?' so the values can be false
101109
value = request.options[param]
102110
elsif request.options.include?(param.to_s)

lib/puppet/http/service/file_server.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def get_file_metadata(path:, environment:, links: :manage, checksum_type: Puppet
106106
# An array with the request response and an array of the deserialized
107107
# metadata for each file returned from the server
108108
#
109-
def get_file_metadatas(path: nil, environment:, recurse: :false, recurselimit: nil, ignore: nil, links: :manage, checksum_type: Puppet[:digest_algorithm], source_permissions: :ignore)
109+
def get_file_metadatas(path: nil, environment:, recurse: :false, recurselimit: nil, max_files: nil, ignore: nil, links: :manage, checksum_type: Puppet[:digest_algorithm], source_permissions: :ignore)
110110
validate_path(path)
111111

112112
headers = add_puppet_headers('Accept' => get_mime_types(Puppet::FileServing::Metadata).join(', '))
@@ -117,6 +117,7 @@ def get_file_metadatas(path: nil, environment:, recurse: :false, recurselimit: n
117117
params: {
118118
recurse: recurse,
119119
recurselimit: recurselimit,
120+
max_files: max_files,
120121
ignore: ignore,
121122
links: links,
122123
checksum_type: checksum_type,

lib/puppet/indirector/catalog/compiler.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def inline_metadata(catalog, checksum_type)
194194
:source_permissions => resource[:source_permissions] ? resource[:source_permissions].to_sym : :ignore,
195195
:recurse => true,
196196
:recurselimit => resource[:recurselimit],
197+
:max_files => resource[:max_files],
197198
:ignore => resource[:ignore],
198199
}
199200

lib/puppet/indirector/file_metadata/rest.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def search(request)
4646
environment: request.environment.to_s,
4747
recurse: request.options[:recurse],
4848
recurselimit: request.options[:recurselimit],
49+
max_files: request.options[:max_files],
4950
ignore: request.options[:ignore],
5051
links: request.options[:links],
5152
checksum_type: request.options[:checksum_type],

lib/puppet/type/file.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,22 @@ def self.title_patterns
220220
end
221221
end
222222

223+
newparam(:max_files) do
224+
desc "In case the resource is a directory and the recursion is enabled, puppet will
225+
generate a new resource for each file file found, possible leading to
226+
an excessive number of resources generated without any control.
227+
228+
Setting `max_files` will check the number of file resources that
229+
will eventually be created and will raise a resource argument error if the
230+
limit will be exceeded.
231+
232+
Use value `0` to disable the check. In this case, a warning is logged if
233+
the number of files exceeds 1000."
234+
235+
defaultto 0
236+
newvalues(/^[0-9]+$/)
237+
end
238+
223239
newparam(:replace, :boolean => true, :parent => Puppet::Parameter::Boolean) do
224240
desc "Whether to replace a file or symlink that already exists on the local system but
225241
whose content doesn't match what the `source` or `content` attribute
@@ -576,7 +592,7 @@ def newchild(path)
576592
options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? }
577593

578594
# These should never be passed to our children.
579-
[:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param|
595+
[:parent, :ensure, :recurse, :recurselimit, :max_files, :target, :alias, :source].each do |param|
580596
options.delete(param) if options.include?(param)
581597
end
582598

@@ -753,6 +769,7 @@ def perform_recursion(path)
753769
:links => self[:links],
754770
:recurse => (self[:recurse] == :remote ? true : self[:recurse]),
755771
:recurselimit => self[:recurselimit],
772+
:max_files => self[:max_files],
756773
:source_permissions => self[:source_permissions],
757774
:ignore => self[:ignore],
758775
:checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none,

lib/puppet/type/tidy.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@
5050
end
5151
end
5252

53+
newparam(:max_files) do
54+
desc "In case the resource is a directory and the recursion is enabled, puppet will
55+
generate a new resource for each file file found, possible leading to
56+
an excessive number of resources generated without any control.
57+
58+
Setting `max_files` will check the number of file resources that
59+
will eventually be created and will raise a resource argument error if the
60+
limit will be exceeded.
61+
62+
Use value `0` to disable the check. In this case, a warning is logged if
63+
the number of files exceeds 1000."
64+
65+
defaultto 0
66+
newvalues(/^[0-9]+$/)
67+
end
68+
5369
newparam(:matches) do
5470
desc <<-'EOT'
5571
One or more (shell type) file glob patterns, which restrict
@@ -256,9 +272,12 @@ def generate
256272

257273
case self[:recurse]
258274
when Integer, /^\d+$/
259-
parameter = { :recurse => true, :recurselimit => self[:recurse] }
275+
parameter = { :max_files => self[:max_files],
276+
:recurse => true,
277+
:recurselimit => self[:recurse] }
260278
when true, :true, :inf
261-
parameter = { :recurse => true }
279+
parameter = { :max_files => self[:max_files],
280+
:recurse => true }
262281
end
263282

264283
if parameter

spec/unit/file_serving/fileset_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
expect(set.recurselimit).to eq(3)
4747
end
4848

49+
it "accepts a 'max_files' option" do
50+
expect(Puppet::FileSystem).to receive(:lstat).with(somefile).and_return(double('stat'))
51+
set = Puppet::FileServing::Fileset.new(somefile, :recurselimit => 3, :max_files => 100)
52+
expect(set.recurselimit).to eq(3)
53+
expect(set.max_files).to eq(100)
54+
end
55+
4956
it "accepts an 'ignore' option" do
5057
expect(Puppet::FileSystem).to receive(:lstat).with(somefile).and_return(double('stat'))
5158
set = Puppet::FileServing::Fileset.new(somefile, :ignore => ".svn")
@@ -160,6 +167,29 @@ def mock_dir_structure(path, stat_method = :lstat)
160167
end
161168
end
162169

170+
def mock_big_dir_structure(path, stat_method = :lstat)
171+
allow(Puppet::FileSystem).to receive(stat_method).with(path).and_return(@dirstat)
172+
173+
# Keep track of the files we're stubbing.
174+
@files = %w{.}
175+
176+
top_names = (1..10).map {|i| "dir_#{i}" }
177+
sub_names = (1..100).map {|i| "file__#{i}" }
178+
179+
allow(Dir).to receive(:entries).with(path, encoding: Encoding::UTF_8).and_return(top_names)
180+
top_names.each do |subdir|
181+
@files << subdir # relative path
182+
subpath = File.join(path, subdir)
183+
allow(Puppet::FileSystem).to receive(stat_method).with(subpath).and_return(@dirstat)
184+
allow(Dir).to receive(:entries).with(subpath, encoding: Encoding::UTF_8).and_return(sub_names)
185+
sub_names.each do |file|
186+
@files << File.join(subdir, file) # relative path
187+
subfile_path = File.join(subpath, file)
188+
allow(Puppet::FileSystem).to receive(stat_method).with(subfile_path).and_return(@filestat)
189+
end
190+
end
191+
end
192+
163193
def setup_mocks_for_dir(mock_dir, base_path)
164194
path = File.join(base_path, mock_dir.name)
165195
allow(Puppet::FileSystem).to receive(:lstat).with(path).and_return(MockStat.new(path, true))
@@ -258,6 +288,20 @@ def directory?
258288
expect(@fileset.files.find { |file| file.include?("0") }).to be_nil
259289
end
260290

291+
it "raises exception if number of files is greater than :max_files" do
292+
mock_dir_structure(@path)
293+
@fileset.recurse = true
294+
@fileset.max_files = 22
295+
expect { @fileset.files }.to raise_error(Puppet::Error, "The directory '#{@path}' contains 28 entries, which exceeds the limit of 22 specified by the max_files parameter for this resource. The limit may be increased, but be aware that large number of file resources can result in excessive resource consumption and degraded performance. Consider using an alternate method to manage large directory trees")
296+
end
297+
298+
it "logs a warning if number of files is greater than soft max_files limit of 1000" do
299+
mock_big_dir_structure(@path)
300+
@fileset.recurse = true
301+
expect(Puppet).to receive(:warning).with("The directory '#{@path}' contains 1010 entries, which exceeds the default soft limit 1000 and may cause excessive resource consumption and degraded performance. To remove this warning set a value for `max_files` parameter or consider using an alternate method to manage large directory trees")
302+
expect { @fileset.files }.to_not raise_error
303+
end
304+
261305
it "ignores files that match a pattern given as a boolean" do
262306
mock_dir_structure(@path)
263307
@fileset.recurse = true

spec/unit/indirector/catalog/compiler_spec.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -909,9 +909,10 @@ def stubs_directory_metadata(relative_path)
909909
it "inlines child metadata" do
910910
catalog = compile_to_catalog(<<-MANIFEST, node)
911911
file { '#{path}':
912-
ensure => directory,
913-
recurse => true,
914-
source => '#{source_dir}'
912+
ensure => directory,
913+
recurse => true,
914+
source => '#{source_dir}',
915+
max_files => 1234,
915916
}
916917
MANIFEST
917918

@@ -925,6 +926,7 @@ def stubs_directory_metadata(relative_path)
925926
:source_permissions => :ignore,
926927
:recurse => true,
927928
:recurselimit => nil,
929+
:max_files => 1234,
928930
:ignore => nil,
929931
}
930932
expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, options).and_return([metadata, child_metadata])
@@ -938,14 +940,15 @@ def stubs_directory_metadata(relative_path)
938940
it "uses resource parameters when inlining metadata" do
939941
catalog = compile_to_catalog(<<-MANIFEST, node)
940942
file { '#{path}':
941-
ensure => directory,
942-
recurse => true,
943-
source => '#{source_dir}',
944-
checksum => sha256,
943+
ensure => directory,
944+
recurse => true,
945+
source => '#{source_dir}',
946+
checksum => sha256,
945947
source_permissions => use_when_creating,
946-
recurselimit => 2,
947-
ignore => 'foo.+',
948-
links => follow,
948+
recurselimit => 2,
949+
max_files => 4321,
950+
ignore => 'foo.+',
951+
links => follow,
949952
}
950953
MANIFEST
951954

@@ -956,6 +959,7 @@ def stubs_directory_metadata(relative_path)
956959
:source_permissions => :use_when_creating,
957960
:recurse => true,
958961
:recurselimit => 2,
962+
:max_files => 4321,
959963
:ignore => 'foo.+',
960964
}
961965
expect(Puppet::FileServing::Metadata.indirection).to receive(:search).with(source_dir, options).and_return([metadata, child_metadata])

spec/unit/type/tidy_spec.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,17 +195,27 @@
195195
allow(Puppet::FileServing::Fileset).to receive(:new).and_return(@fileset)
196196
end
197197

198-
it "should use a Fileset for infinite recursion" do
199-
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true).and_return(@fileset)
198+
it "should use a Fileset with default max_files for infinite recursion" do
199+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :max_files=>0).and_return(@fileset)
200200
expect(@fileset).to receive(:files).and_return(%w{. one two})
201201
allow(@tidy).to receive(:tidy?).and_return(false)
202202

203203
@tidy.generate
204204
end
205205

206-
it "should use a Fileset for limited recursion" do
206+
it "should use a Fileset with default max_files for limited recursion" do
207207
@tidy[:recurse] = 42
208-
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :recurselimit => 42).and_return(@fileset)
208+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :recurselimit => 42, :max_files=>0).and_return(@fileset)
209+
expect(@fileset).to receive(:files).and_return(%w{. one two})
210+
allow(@tidy).to receive(:tidy?).and_return(false)
211+
212+
@tidy.generate
213+
end
214+
215+
it "should use a Fileset with max_files for limited recursion" do
216+
@tidy[:recurse] = 42
217+
@tidy[:max_files] = 9876
218+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :recurselimit => 42, :max_files=>9876).and_return(@fileset)
209219
expect(@fileset).to receive(:files).and_return(%w{. one two})
210220
allow(@tidy).to receive(:tidy?).and_return(false)
211221

@@ -411,7 +421,7 @@
411421
@tidy[:recurse] = true
412422
@tidy[:rmdirs] = true
413423
fileset = double('fileset')
414-
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true).and_return(fileset)
424+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :max_files=>0).and_return(fileset)
415425
expect(fileset).to receive(:files).and_return(%w{. one two one/subone two/subtwo one/subone/ssone})
416426
allow(@tidy).to receive(:tidy?).and_return(true)
417427

@@ -433,7 +443,7 @@
433443
@tidy[:recurse] = true
434444
@tidy[:rmdirs] = true
435445
fileset = double('fileset')
436-
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true).and_return(fileset)
446+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :max_files=>0).and_return(fileset)
437447
expect(fileset).to receive(:files).and_return(%w{. a a/2 a/1 a/3})
438448
allow(@tidy).to receive(:tidy?).and_return(true)
439449

@@ -446,7 +456,7 @@
446456
@tidy[:noop] = true
447457

448458
fileset = double('fileset')
449-
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true).and_return(fileset)
459+
expect(Puppet::FileServing::Fileset).to receive(:new).with(@basepath, :recurse => true, :max_files=>0).and_return(fileset)
450460
expect(fileset).to receive(:files).and_return(%w{. a a/2 a/1 a/3})
451461
allow(@tidy).to receive(:tidy?).and_return(true)
452462

0 commit comments

Comments
 (0)