Update stdlib and concat to 6.1.0 both
[mirror/dsa-puppet.git] / 3rdparty / modules / concat / lib / puppet / type / concat_file.rb
index 359c963..6dd068e 100644 (file)
@@ -4,14 +4,15 @@ require 'puppet/type/file/mode'
 require 'puppet/util/checksums'
 
 Puppet::Type.newtype(:concat_file) do
-  @doc = "Gets all the file fragments and puts these into the target file.
-    This will mostly be used with exported resources.
+  @doc = <<-DOC
+    @summary
+      Generates a file with content from fragments sharing a common unique tag.
 
-    example:
+    @example
       Concat_fragment <<| tag == 'unique_tag' |>>
 
       concat_file { '/tmp/file':
-        tag            => 'unique_tag', # Mandatory
+        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
@@ -19,9 +20,14 @@ Puppet::Type.newtype(:concat_file) do
         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 }
@@ -32,33 +38,46 @@ Puppet::Type.newtype(:concat_file) do
   end
 
   newparam(:tag) do
-    desc "Tag reference to collect all concat_fragment's with the same tag"
+    desc 'Required. Specifies a unique tag reference to collect all concat_fragments with the same tag.'
   end
 
-  newparam(:path, :namevar => true) do
-    desc "The output file"
+  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) or Puppet::Util.absolute_path?(value, :windows))
-        raise ArgumentError, "File paths must be fully qualified, not '#{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 "Desired file owner."
+  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 "Desired file group."
+  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 "Desired file mode."
+  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 "Controls the ordering of fragments. Can be set to alpha or numeric."
+    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)
 
@@ -66,82 +85,215 @@ Puppet::Type.newtype(:concat_file) do
   end
 
   newparam(:backup) do
-    desc "Controls the filebucketing behavior of the final file and see File type reference for its use."
-    defaultto 'puppet'
+    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 "Whether to replace a file that already exists on the local system."
-    defaultto :true
+  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 "Validates file."
+    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(:ensure_newline, :boolean => true, :parent => Puppet::Parameter::Boolean) do
-    desc "Whether to ensure there is a newline after each fragment."
-    defaultto :false
+  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
 
-  # Inherit File parameters
-  newparam(:selinux_ignore_defaults) do
+  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) do
+  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
-  # End file parameters
 
   # 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 = ""
+    @generated_content = ''
     content_fragments = []
 
-    resources = catalog.resources.select do |r|
-      r.is_a?(Puppet::Type.type(:concat_fragment)) && r[:tag] == self[:tag]
-    end
-
-    resources.each do |r|
+    fragments.each do |r|
       content_fragments << ["#{r[:order]}___#{r[:name]}", fragment_content(r)]
     end
 
-    if self[:order] == :numeric
-      sorted = content_fragments.sort do |a, b|
-        def decompound(d)
-          d.split('___', 2).map { |v| v =~ /^\d+$/ ? v.to_i : v }
+    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
 
-        decompound(a[0]) <=> decompound(b[0])
-      end
-    else
-      sorted = content_fragments.sort_by do |a|
-        a_order, a_name = a[0].split('__', 2)
-        [a_order, a_name]
+        @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 = sorted.map { |cf| cf[1] }.join
-
     @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]
@@ -149,17 +301,18 @@ Puppet::Type.newtype(:concat_file) do
       @source = nil
       Array(r[:source]).each do |source|
         if Puppet::FileServing::Metadata.indirection.find(source)
-          @source = source 
+          @source = source
           break
         end
       end
-      self.fail "Could not retrieve source(s) #{r[:source].join(", ")}" unless @source
+      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]
-      fragment_content<<"\n" unless fragment_content =~ /\n$/
+      newline = Puppet::Util::Platform.windows? ? "\r\n" : "\n"
+      fragment_content << newline unless fragment_content =~ %r{#{newline}$}
     end
 
     fragment_content
@@ -167,7 +320,7 @@ Puppet::Type.newtype(:concat_file) do
 
   def generate
     file_opts = {
-      :ensure => self[:ensure] == :absent ? :absent : :file,
+      ensure: (self[:ensure] == :absent) ? :absent : :file,
     }
 
     [:path,
@@ -183,18 +336,16 @@ Puppet::Type.newtype(:concat_file) do
      :seluser,
      :validate_cmd,
      :show_diff].each do |param|
-      unless self[param].nil?
-        file_opts[param] = self[param]
-      end
+      file_opts[param] = self[param] unless self[param].nil?
     end
 
     metaparams = Puppet::Type.metaparams
-    excluded_metaparams = [ :before, :notify, :require, :subscribe, :tag ]
+    excluded_metaparams = [:before, :notify, :require, :subscribe, :tag]
 
     metaparams.reject! { |param| excluded_metaparams.include? param }
 
     metaparams.each do |metaparam|
-      file_opts[metaparam] = self[metaparam] if self[metaparam]
+      file_opts[metaparam] = self[metaparam] unless self[metaparam].nil?
     end
 
     [Puppet::Type.type(:file).new(file_opts)]
@@ -203,10 +354,10 @@ Puppet::Type.newtype(:concat_file) do
   def eval_generate
     content = should_content
 
-    if !content.nil? and !content.empty?
+    if !content.nil? && !content.empty?
       catalog.resource("File[#{self[:path]}]")[:content] = content
     end
 
-    [ catalog.resource("File[#{self[:path]}]") ]
+    [catalog.resource("File[#{self[:path]}]")]
   end
 end