Update stdlib and concat to 6.1.0 both
[mirror/dsa-puppet.git] / 3rdparty / modules / concat / lib / puppet / type / concat_file.rb
1 require 'puppet/type/file/owner'
2 require 'puppet/type/file/group'
3 require 'puppet/type/file/mode'
4 require 'puppet/util/checksums'
5
6 Puppet::Type.newtype(:concat_file) do
7   @doc = <<-DOC
8     @summary
9       Generates a file with content from fragments sharing a common unique tag.
10
11     @example
12       Concat_fragment <<| tag == 'unique_tag' |>>
13
14       concat_file { '/tmp/file':
15         tag            => 'unique_tag', # Optional. Default to undef
16         path           => '/tmp/file',  # Optional. If given it overrides the resource name
17         owner          => 'root',       # Optional. Default to undef
18         group          => 'root',       # Optional. Default to undef
19         mode           => '0644'        # Optional. Default to undef
20         order          => 'numeric'     # Optional, Default to 'numeric'
21         ensure_newline => false         # Optional, Defaults to false
22       }
23   DOC
24
25   ensurable do
26     desc <<-DOC
27       Specifies whether the destination file should exist. Setting to 'absent' tells Puppet to delete the destination file if it exists, and
28       negates the effect of any other parameters.
29     DOC
30
31     defaultvalues
32
33     defaultto { :present }
34   end
35
36   def exists?
37     self[:ensure] == :present
38   end
39
40   newparam(:tag) do
41     desc 'Required. Specifies a unique tag reference to collect all concat_fragments with the same tag.'
42   end
43
44   newparam(:path, namevar: true) do
45     desc <<-DOC
46       Specifies a destination file for the combined fragments. Valid options: a string containing an absolute path. Default value: the
47       title of your declared resource.
48     DOC
49
50     validate do |value|
51       unless Puppet::Util.absolute_path?(value, :posix) || Puppet::Util.absolute_path?(value, :windows)
52         raise ArgumentError, _("File paths must be fully qualified, not '%{_value}'") % { _value: value }
53       end
54     end
55   end
56
57   newparam(:owner, parent: Puppet::Type::File::Owner) do
58     desc <<-DOC
59       Specifies the owner of the destination file. Valid options: a string containing a username or integer containing a uid.
60     DOC
61   end
62
63   newparam(:group, parent: Puppet::Type::File::Group) do
64     desc <<-DOC
65       Specifies a permissions group for the destination file. Valid options: a string containing a group name or integer containing a
66       gid.
67     DOC
68   end
69
70   newparam(:mode, parent: Puppet::Type::File::Mode) do
71     desc <<-DOC
72       Specifies the permissions mode of the destination file. Valid options: a string containing a permission mode value in octal notation.
73     DOC
74   end
75
76   newparam(:order) do
77     desc <<-DOC
78       Specifies a method for sorting your fragments by name within the destination file. You can override this setting for individual
79       fragments by adjusting the order parameter in their concat::fragment declarations.
80     DOC
81
82     newvalues(:alpha, :numeric)
83
84     defaultto :numeric
85   end
86
87   newparam(:backup) do
88     desc <<-DOC
89       Specifies whether (and how) to back up the destination file before overwriting it. Your value gets passed on to Puppet's native file
90       resource for execution. Valid options: true, false, or a string representing either a target filebucket or a filename extension
91       beginning with ".".'
92     DOC
93
94     validate do |value|
95       unless [TrueClass, FalseClass, String].include?(value.class)
96         raise ArgumentError, _('Backup must be a Boolean or String')
97       end
98     end
99   end
100
101   newparam(:replace, boolean: true, parent: Puppet::Parameter::Boolean) do
102     desc 'Specifies whether to overwrite the destination file if it already exists.'
103     defaultto true
104   end
105
106   newparam(:validate_cmd) do
107     desc <<-DOC
108       Specifies a validation command to apply to the destination file. Requires Puppet version 3.5 or newer. Valid options: a string to
109       be passed to a file resource.
110     DOC
111
112     validate do |value|
113       unless value.is_a?(String)
114         raise ArgumentError, _('Validate_cmd must be a String')
115       end
116     end
117   end
118
119   newparam(:ensure_newline, boolean: true, parent: Puppet::Parameter::Boolean) do
120     desc "Specifies whether to add a line break at the end of each fragment that doesn't already end in one."
121     defaultto false
122   end
123
124   newparam(:format) do
125     desc <<-DOC
126     Specify what data type to merge the fragments as. Valid options: 'plain', 'yaml', 'json', 'json-array', 'json-pretty', 'json-array-pretty'.
127     DOC
128
129     newvalues(:plain, :yaml, :json, :'json-array', :'json-pretty', :'json-array-pretty')
130
131     defaultto :plain
132   end
133
134   newparam(:force, boolean: true, parent: Puppet::Parameter::Boolean) do
135     desc 'Specifies whether to merge data structures, keeping the values with higher order.'
136
137     defaultto false
138   end
139
140   newparam(:selinux_ignore_defaults, boolean: true, parent: Puppet::Parameter::Boolean) do
141     desc <<-DOC
142       See the file type's selinux_ignore_defaults documentention:
143       https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selinux_ignore_defaults.
144     DOC
145   end
146
147   newparam(:selrange) do
148     desc "See the file type's selrange documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrange"
149     validate do |value|
150       raise ArgumentError, _('Selrange must be a String') unless value.is_a?(String)
151     end
152   end
153
154   newparam(:selrole) do
155     desc "See the file type's selrole documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrole"
156     validate do |value|
157       raise ArgumentError, _('Selrole must be a String') unless value.is_a?(String)
158     end
159   end
160
161   newparam(:seltype) do
162     desc "See the file type's seltype documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seltype"
163     validate do |value|
164       raise ArgumentError, _('Seltype must be a String') unless value.is_a?(String)
165     end
166   end
167
168   newparam(:seluser) do
169     desc "See the file type's seluser documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seluser"
170     validate do |value|
171       raise ArgumentError, _('Seluser must be a String') unless value.is_a?(String)
172     end
173   end
174
175   newparam(:show_diff, boolean: true, parent: Puppet::Parameter::Boolean) do
176     desc <<-DOC
177       Specifies whether to set the show_diff parameter for the file resource. Useful for hiding secrets stored in hiera from insecure
178       reporting methods.
179     DOC
180   end
181
182   # Autorequire the file we are generating below
183   # Why is this necessary ?
184   autorequire(:file) do
185     [self[:path]]
186   end
187
188   def fragments
189     # Collect fragments that target this resource by path, title or tag.
190     @fragments ||= catalog.resources.map { |resource|
191       next unless resource.is_a?(Puppet::Type.type(:concat_fragment))
192
193       if resource[:target] == self[:path] || resource[:target] == title ||
194          (resource[:tag] && resource[:tag] == self[:tag])
195         resource
196       end
197     }.compact
198   end
199
200   def decompound(d)
201     d.split('___', 2).map { |v| (v =~ %r{^\d+$}) ? v.to_i : v }
202   end
203
204   def should_content
205     return @generated_content if @generated_content
206     @generated_content = ''
207     content_fragments = []
208
209     fragments.each do |r|
210       content_fragments << ["#{r[:order]}___#{r[:name]}", fragment_content(r)]
211     end
212
213     sorted = if self[:order] == :numeric
214                content_fragments.sort do |a, b|
215                  decompound(a[0]) <=> decompound(b[0])
216                end
217              else
218                content_fragments.sort_by do |a|
219                  a_order, a_name = a[0].split('__', 2)
220                  [a_order, a_name]
221                end
222              end
223
224     case self[:format]
225     when :plain
226       @generated_content = sorted.map { |cf| cf[1] }.join
227     when :yaml
228       content_array = sorted.map do |cf|
229         YAML.safe_load(cf[1])
230       end
231       content_hash = content_array.reduce({}) do |memo, current|
232         nested_merge(memo, current)
233       end
234       @generated_content = content_hash.to_yaml
235     when :json, :'json-array', :'json-pretty', :'json-array-pretty'
236       content_array = sorted.map do |cf|
237         JSON.parse(cf[1])
238       end
239
240       if [:json, :'json-pretty'].include?(self[:format])
241         content_hash = content_array.reduce({}) do |memo, current|
242           nested_merge(memo, current)
243         end
244
245         @generated_content =
246           if self[:format] == :json
247             content_hash.to_json
248           else
249             JSON.pretty_generate(content_hash)
250           end
251       else
252         @generated_content =
253           if self[:format] == :'json-array'
254             content_array.to_json
255           else
256             JSON.pretty_generate(content_array)
257           end
258       end
259     end
260
261     @generated_content
262   end
263
264   def nested_merge(hash1, hash2)
265     # If a hash is empty, simply return the other
266     return hash1 if hash2.empty?
267     return hash2 if hash1.empty?
268
269     # Unique merge for arrays
270     if hash1.is_a?(Array) && hash2.is_a?(Array)
271       return (hash1 + hash2).uniq
272     end
273
274     # Deep-merge Hashes; higher order value is kept
275     hash1.merge(hash2) do |k, v1, v2|
276       if v1.is_a?(Hash) && v2.is_a?(Hash)
277         nested_merge(v1, v2)
278       elsif v1.is_a?(Array) && v2.is_a?(Array)
279         nested_merge(v1, v2)
280       else
281         # Fail if there are duplicate keys without force
282         unless v1 == v2
283           unless self[:force]
284             err_message = [
285               "Duplicate key '#{k}' found with values '#{v1}' and #{v2}'.",
286               "Use 'force' attribute to merge keys.",
287             ]
288             raise(_(err_message.join(' ')))
289           end
290           Puppet.debug("Key '#{k}': replacing '#{v2}' with '#{v1}'.")
291         end
292         v1
293       end
294     end
295   end
296
297   def fragment_content(r)
298     if r[:content].nil? == false
299       fragment_content = r[:content]
300     elsif r[:source].nil? == false
301       @source = nil
302       Array(r[:source]).each do |source|
303         if Puppet::FileServing::Metadata.indirection.find(source)
304           @source = source
305           break
306         end
307       end
308       raise _('Could not retrieve source(s) %{_array}') % { _array: Array(r[:source]).join(', ') } unless @source
309       tmp = Puppet::FileServing::Content.indirection.find(@source)
310       fragment_content = tmp.content unless tmp.nil?
311     end
312
313     if self[:ensure_newline]
314       newline = Puppet::Util::Platform.windows? ? "\r\n" : "\n"
315       fragment_content << newline unless fragment_content =~ %r{#{newline}$}
316     end
317
318     fragment_content
319   end
320
321   def generate
322     file_opts = {
323       ensure: (self[:ensure] == :absent) ? :absent : :file,
324     }
325
326     [:path,
327      :owner,
328      :group,
329      :mode,
330      :replace,
331      :backup,
332      :selinux_ignore_defaults,
333      :selrange,
334      :selrole,
335      :seltype,
336      :seluser,
337      :validate_cmd,
338      :show_diff].each do |param|
339       file_opts[param] = self[param] unless self[param].nil?
340     end
341
342     metaparams = Puppet::Type.metaparams
343     excluded_metaparams = [:before, :notify, :require, :subscribe, :tag]
344
345     metaparams.reject! { |param| excluded_metaparams.include? param }
346
347     metaparams.each do |metaparam|
348       file_opts[metaparam] = self[metaparam] unless self[metaparam].nil?
349     end
350
351     [Puppet::Type.type(:file).new(file_opts)]
352   end
353
354   def eval_generate
355     content = should_content
356
357     if !content.nil? && !content.empty?
358       catalog.resource("File[#{self[:path]}]")[:content] = content
359     end
360
361     [catalog.resource("File[#{self[:path]}]")]
362   end
363 end