1 require 'puppet/type/file/owner'
2 require 'puppet/type/file/group'
3 require 'puppet/type/file/mode'
4 require 'puppet/util/checksums'
6 Puppet::Type.newtype(:concat_file) do
9 Generates a file with content from fragments sharing a common unique tag.
12 Concat_fragment <<| tag == 'unique_tag' |>>
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
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.
33 defaultto { :present }
37 self[:ensure] == :present
41 desc 'Required. Specifies a unique tag reference to collect all concat_fragments with the same tag.'
44 newparam(:path, namevar: true) do
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.
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 }
57 newparam(:owner, parent: Puppet::Type::File::Owner) do
59 Specifies the owner of the destination file. Valid options: a string containing a username or integer containing a uid.
63 newparam(:group, parent: Puppet::Type::File::Group) do
65 Specifies a permissions group for the destination file. Valid options: a string containing a group name or integer containing a
70 newparam(:mode, parent: Puppet::Type::File::Mode) do
72 Specifies the permissions mode of the destination file. Valid options: a string containing a permission mode value in octal notation.
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.
82 newvalues(:alpha, :numeric)
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
95 unless [TrueClass, FalseClass, String].include?(value.class)
96 raise ArgumentError, _('Backup must be a Boolean or String')
101 newparam(:replace, boolean: true, parent: Puppet::Parameter::Boolean) do
102 desc 'Specifies whether to overwrite the destination file if it already exists.'
106 newparam(:validate_cmd) do
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.
113 unless value.is_a?(String)
114 raise ArgumentError, _('Validate_cmd must be a String')
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."
126 Specify what data type to merge the fragments as. Valid options: 'plain', 'yaml', 'json', 'json-array', 'json-pretty', 'json-array-pretty'.
129 newvalues(:plain, :yaml, :json, :'json-array', :'json-pretty', :'json-array-pretty')
134 newparam(:force, boolean: true, parent: Puppet::Parameter::Boolean) do
135 desc 'Specifies whether to merge data structures, keeping the values with higher order.'
140 newparam(:selinux_ignore_defaults, boolean: true, parent: Puppet::Parameter::Boolean) do
142 See the file type's selinux_ignore_defaults documentention:
143 https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selinux_ignore_defaults.
147 newparam(:selrange) do
148 desc "See the file type's selrange documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrange"
150 raise ArgumentError, _('Selrange must be a String') unless value.is_a?(String)
154 newparam(:selrole) do
155 desc "See the file type's selrole documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrole"
157 raise ArgumentError, _('Selrole must be a String') unless value.is_a?(String)
161 newparam(:seltype) do
162 desc "See the file type's seltype documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seltype"
164 raise ArgumentError, _('Seltype must be a String') unless value.is_a?(String)
168 newparam(:seluser) do
169 desc "See the file type's seluser documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seluser"
171 raise ArgumentError, _('Seluser must be a String') unless value.is_a?(String)
175 newparam(:show_diff, boolean: true, parent: Puppet::Parameter::Boolean) do
177 Specifies whether to set the show_diff parameter for the file resource. Useful for hiding secrets stored in hiera from insecure
182 # Autorequire the file we are generating below
183 # Why is this necessary ?
184 autorequire(:file) do
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))
193 if resource[:target] == self[:path] || resource[:target] == title ||
194 (resource[:tag] && resource[:tag] == self[:tag])
201 d.split('___', 2).map { |v| (v =~ %r{^\d+$}) ? v.to_i : v }
205 return @generated_content if @generated_content
206 @generated_content = ''
207 content_fragments = []
209 fragments.each do |r|
210 content_fragments << ["#{r[:order]}___#{r[:name]}", fragment_content(r)]
213 sorted = if self[:order] == :numeric
214 content_fragments.sort do |a, b|
215 decompound(a[0]) <=> decompound(b[0])
218 content_fragments.sort_by do |a|
219 a_order, a_name = a[0].split('__', 2)
226 @generated_content = sorted.map { |cf| cf[1] }.join
228 content_array = sorted.map do |cf|
229 YAML.safe_load(cf[1])
231 content_hash = content_array.reduce({}) do |memo, current|
232 nested_merge(memo, current)
234 @generated_content = content_hash.to_yaml
235 when :json, :'json-array', :'json-pretty', :'json-array-pretty'
236 content_array = sorted.map do |cf|
240 if [:json, :'json-pretty'].include?(self[:format])
241 content_hash = content_array.reduce({}) do |memo, current|
242 nested_merge(memo, current)
246 if self[:format] == :json
249 JSON.pretty_generate(content_hash)
253 if self[:format] == :'json-array'
254 content_array.to_json
256 JSON.pretty_generate(content_array)
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?
269 # Unique merge for arrays
270 if hash1.is_a?(Array) && hash2.is_a?(Array)
271 return (hash1 + hash2).uniq
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)
278 elsif v1.is_a?(Array) && v2.is_a?(Array)
281 # Fail if there are duplicate keys without force
285 "Duplicate key '#{k}' found with values '#{v1}' and #{v2}'.",
286 "Use 'force' attribute to merge keys.",
288 raise(_(err_message.join(' ')))
290 Puppet.debug("Key '#{k}': replacing '#{v2}' with '#{v1}'.")
297 def fragment_content(r)
298 if r[:content].nil? == false
299 fragment_content = r[:content]
300 elsif r[:source].nil? == false
302 Array(r[:source]).each do |source|
303 if Puppet::FileServing::Metadata.indirection.find(source)
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?
313 if self[:ensure_newline]
314 newline = Puppet::Util::Platform.windows? ? "\r\n" : "\n"
315 fragment_content << newline unless fragment_content =~ %r{#{newline}$}
323 ensure: (self[:ensure] == :absent) ? :absent : :file,
332 :selinux_ignore_defaults,
338 :show_diff].each do |param|
339 file_opts[param] = self[param] unless self[param].nil?
342 metaparams = Puppet::Type.metaparams
343 excluded_metaparams = [:before, :notify, :require, :subscribe, :tag]
345 metaparams.reject! { |param| excluded_metaparams.include? param }
347 metaparams.each do |metaparam|
348 file_opts[metaparam] = self[metaparam] unless self[metaparam].nil?
351 [Puppet::Type.type(:file).new(file_opts)]
355 content = should_content
357 if !content.nil? && !content.empty?
358 catalog.resource("File[#{self[:path]}]")[:content] = content
361 [catalog.resource("File[#{self[:path]}]")]