File: //var/cache/puppet/lib/puppet/type/concat_file.rb
require 'puppet/type/file/owner'
require 'puppet/type/file/group'
require 'puppet/type/file/mode'
require 'puppet/util/checksums'
Puppet::Type.newtype(:concat_file) do
@doc = <<-DOC
@summary
Generates a file with content from fragments sharing a common unique tag.
@example
Concat_fragment <<| tag == 'unique_tag' |>>
concat_file { '/tmp/file':
tag => 'unique_tag', # Optional. Default to undef
path => '/tmp/file', # Optional. If given it overrides the resource name
owner => 'root', # Optional. Default to undef
group => 'root', # Optional. Default to undef
mode => '0644' # Optional. Default to undef
order => 'numeric' # Optional, Default to 'numeric'
ensure_newline => false # Optional, Defaults to false
}
DOC
ensurable do
desc <<-DOC
Specifies whether the destination file should exist. Setting to 'absent' tells Puppet to delete the destination file if it exists, and
negates the effect of any other parameters.
DOC
defaultvalues
defaultto { :present }
end
def exists?
self[:ensure] == :present
end
newparam(:tag) do
desc 'Required. Specifies a unique tag reference to collect all concat_fragments with the same tag.'
end
newparam(:path, namevar: true) do
desc <<-DOC
Specifies a destination file for the combined fragments. Valid options: a string containing an absolute path. Default value: the
title of your declared resource.
DOC
validate do |value|
unless Puppet::Util.absolute_path?(value, :posix) || Puppet::Util.absolute_path?(value, :windows)
raise ArgumentError, _("File paths must be fully qualified, not '%{_value}'") % { _value: value }
end
end
end
newparam(:owner, parent: Puppet::Type::File::Owner) do
desc <<-DOC
Specifies the owner of the destination file. Valid options: a string containing a username or integer containing a uid.
DOC
end
newparam(:group, parent: Puppet::Type::File::Group) do
desc <<-DOC
Specifies a permissions group for the destination file. Valid options: a string containing a group name or integer containing a
gid.
DOC
end
newparam(:mode, parent: Puppet::Type::File::Mode) do
desc <<-DOC
Specifies the permissions mode of the destination file. Valid options: a string containing a permission mode value in octal notation.
DOC
end
newparam(:order) do
desc <<-DOC
Specifies a method for sorting your fragments by name within the destination file. You can override this setting for individual
fragments by adjusting the order parameter in their concat::fragment declarations.
DOC
newvalues(:alpha, :numeric)
defaultto :numeric
end
newparam(:backup) do
desc <<-DOC
Specifies whether (and how) to back up the destination file before overwriting it. Your value gets passed on to Puppet's native file
resource for execution. Valid options: true, false, or a string representing either a target filebucket or a filename extension
beginning with ".".'
DOC
validate do |value|
unless [TrueClass, FalseClass, String].include?(value.class)
raise ArgumentError, _('Backup must be a Boolean or String')
end
end
end
newparam(:replace, boolean: true, parent: Puppet::Parameter::Boolean) do
desc 'Specifies whether to overwrite the destination file if it already exists.'
defaultto true
end
newparam(:validate_cmd) do
desc <<-DOC
Specifies a validation command to apply to the destination file. Requires Puppet version 3.5 or newer. Valid options: a string to
be passed to a file resource.
DOC
validate do |value|
unless value.is_a?(String)
raise ArgumentError, _('Validate_cmd must be a String')
end
end
end
newparam(:ensure_newline, boolean: true, parent: Puppet::Parameter::Boolean) do
desc "Specifies whether to add a line break at the end of each fragment that doesn't already end in one."
defaultto false
end
newparam(:format) do
desc <<-DOC
Specify what data type to merge the fragments as. Valid options: 'plain', 'yaml', 'json', 'json-array', 'json-pretty', 'json-array-pretty'.
DOC
newvalues(:plain, :yaml, :json, :'json-array', :'json-pretty', :'json-array-pretty')
defaultto :plain
end
newparam(:force, boolean: true, parent: Puppet::Parameter::Boolean) do
desc 'Specifies whether to merge data structures, keeping the values with higher order.'
defaultto false
end
newparam(:selinux_ignore_defaults, boolean: true, parent: Puppet::Parameter::Boolean) do
desc <<-DOC
See the file type's selinux_ignore_defaults documentention:
https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selinux_ignore_defaults.
DOC
end
newparam(:selrange) do
desc "See the file type's selrange documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrange"
validate do |value|
raise ArgumentError, _('Selrange must be a String') unless value.is_a?(String)
end
end
newparam(:selrole) do
desc "See the file type's selrole documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-selrole"
validate do |value|
raise ArgumentError, _('Selrole must be a String') unless value.is_a?(String)
end
end
newparam(:seltype) do
desc "See the file type's seltype documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seltype"
validate do |value|
raise ArgumentError, _('Seltype must be a String') unless value.is_a?(String)
end
end
newparam(:seluser) do
desc "See the file type's seluser documentation: https://docs.puppetlabs.com/references/latest/type.html#file-attribute-seluser"
validate do |value|
raise ArgumentError, _('Seluser must be a String') unless value.is_a?(String)
end
end
newparam(:show_diff, boolean: true, parent: Puppet::Parameter::Boolean) do
desc <<-DOC
Specifies whether to set the show_diff parameter for the file resource. Useful for hiding secrets stored in hiera from insecure
reporting methods.
DOC
end
# Autorequire the file we are generating below
# Why is this necessary ?
autorequire(:file) do
[self[:path]]
end
def fragments
# Collect fragments that target this resource by path, title or tag.
@fragments ||= catalog.resources.map { |resource|
next unless resource.is_a?(Puppet::Type.type(:concat_fragment))
if resource[:target] == self[:path] || resource[:target] == title ||
(resource[:tag] && resource[:tag] == self[:tag])
resource
end
}.compact
end
def decompound(d)
d.split('___', 2).map { |v| (v =~ %r{^\d+$}) ? v.to_i : v }
end
def should_content
return @generated_content if @generated_content
@generated_content = ''
content_fragments = []
fragments.each do |r|
content_fragments << ["#{r[:order]}___#{r[:name]}", fragment_content(r)]
end
sorted = if self[:order] == :numeric
content_fragments.sort do |a, b|
decompound(a[0]) <=> decompound(b[0])
end
else
content_fragments.sort_by do |a|
a_order, a_name = a[0].split('__', 2)
[a_order, a_name]
end
end
case self[:format]
when :plain
@generated_content = sorted.map { |cf| cf[1] }.join
when :yaml
content_array = sorted.map do |cf|
YAML.safe_load(cf[1])
end
content_hash = content_array.reduce({}) do |memo, current|
nested_merge(memo, current)
end
@generated_content = content_hash.to_yaml
when :json, :'json-array', :'json-pretty', :'json-array-pretty'
content_array = sorted.map do |cf|
JSON.parse(cf[1])
end
if [:json, :'json-pretty'].include?(self[:format])
content_hash = content_array.reduce({}) do |memo, current|
nested_merge(memo, current)
end
@generated_content =
if self[:format] == :json
content_hash.to_json
else
JSON.pretty_generate(content_hash)
end
else
@generated_content =
if self[:format] == :'json-array'
content_array.to_json
else
JSON.pretty_generate(content_array)
end
end
end
@generated_content
end
def nested_merge(hash1, hash2)
# If a hash is empty, simply return the other
return hash1 if hash2.empty?
return hash2 if hash1.empty?
# Unique merge for arrays
if hash1.is_a?(Array) && hash2.is_a?(Array)
return (hash1 + hash2).uniq
end
# Deep-merge Hashes; higher order value is kept
hash1.merge(hash2) do |k, v1, v2|
if v1.is_a?(Hash) && v2.is_a?(Hash)
nested_merge(v1, v2)
elsif v1.is_a?(Array) && v2.is_a?(Array)
nested_merge(v1, v2)
else
# Fail if there are duplicate keys without force
unless v1 == v2
unless self[:force]
err_message = [
"Duplicate key '#{k}' found with values '#{v1}' and #{v2}'.",
"Use 'force' attribute to merge keys.",
]
raise(_(err_message.join(' ')))
end
Puppet.debug("Key '#{k}': replacing '#{v2}' with '#{v1}'.")
end
v1
end
end
end
def fragment_content(r)
if r[:content].nil? == false
fragment_content = r[:content]
elsif r[:source].nil? == false
@source = nil
Array(r[:source]).each do |source|
if Puppet::FileServing::Metadata.indirection.find(source)
@source = source
break
end
end
raise _('Could not retrieve source(s) %{_array}') % { _array: Array(r[:source]).join(', ') } unless @source
tmp = Puppet::FileServing::Content.indirection.find(@source)
fragment_content = tmp.content unless tmp.nil?
end
if self[:ensure_newline]
newline = Puppet::Util::Platform.windows? ? "\r\n" : "\n"
fragment_content << newline unless fragment_content =~ %r{#{newline}\Z}
end
fragment_content
end
def generate
file_opts = {
ensure: (self[:ensure] == :absent) ? :absent : :file,
}
[:path,
:owner,
:group,
:mode,
:replace,
:backup,
:selinux_ignore_defaults,
:selrange,
:selrole,
:seltype,
:seluser,
:validate_cmd,
:show_diff].each do |param|
file_opts[param] = self[param] unless self[param].nil?
end
metaparams = Puppet::Type.metaparams
excluded_metaparams = [:before, :notify, :require, :subscribe, :tag]
metaparams.reject! { |param| excluded_metaparams.include? param }
metaparams.each do |metaparam|
file_opts[metaparam] = self[metaparam] unless self[metaparam].nil?
end
[Puppet::Type.type(:file).new(file_opts)]
end
def eval_generate
content = should_content
if !content.nil? && !content.empty?
catalog.resource("File[#{self[:path]}]")[:content] = content
end
[catalog.resource("File[#{self[:path]}]")]
end
end