octocatalog-diff 1.5.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.version +1 -1
- data/README.md +4 -4
- data/doc/CHANGELOG.md +49 -0
- data/doc/advanced-filter.md +23 -0
- data/doc/advanced-ignores.md +10 -0
- data/doc/advanced-puppet-master.md +23 -5
- data/doc/configuration-puppetdb.md +11 -0
- data/doc/dev/api/v1/calls/catalog-diff.md +6 -2
- data/doc/dev/api/v1/objects/diff.md +3 -3
- data/doc/dev/releasing.md +1 -1
- data/doc/limitations.md +9 -9
- data/doc/optionsref.md +167 -11
- data/doc/requirements.md +6 -2
- data/lib/octocatalog-diff/catalog-diff/differ.rb +29 -4
- data/lib/octocatalog-diff/catalog-diff/filter.rb +2 -1
- data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +29 -25
- data/lib/octocatalog-diff/catalog-diff/filter/single_item_array.rb +44 -0
- data/lib/octocatalog-diff/catalog-util/builddir.rb +3 -3
- data/lib/octocatalog-diff/catalog-util/command.rb +25 -3
- data/lib/octocatalog-diff/catalog-util/fileresources.rb +1 -1
- data/lib/octocatalog-diff/catalog.rb +22 -4
- data/lib/octocatalog-diff/catalog/computed.rb +2 -1
- data/lib/octocatalog-diff/catalog/puppetmaster.rb +43 -5
- data/lib/octocatalog-diff/cli.rb +36 -5
- data/lib/octocatalog-diff/cli/options.rb +39 -3
- data/lib/octocatalog-diff/cli/options/hostname.rb +13 -2
- data/lib/octocatalog-diff/cli/options/pe_enc_token_file.rb +1 -1
- data/lib/octocatalog-diff/cli/options/puppet_master_api_version.rb +2 -2
- data/lib/octocatalog-diff/cli/options/puppet_master_token.rb +20 -0
- data/lib/octocatalog-diff/cli/options/puppet_master_token_file.rb +35 -0
- data/lib/octocatalog-diff/cli/options/puppet_master_update_catalog.rb +20 -0
- data/lib/octocatalog-diff/cli/options/puppet_master_update_facts.rb +20 -0
- data/lib/octocatalog-diff/cli/options/puppetdb_package_inventory.rb +18 -0
- data/lib/octocatalog-diff/cli/options/puppetdb_token.rb +17 -0
- data/lib/octocatalog-diff/cli/options/puppetdb_token_file.rb +21 -0
- data/lib/octocatalog-diff/facts/puppetdb.rb +43 -2
- data/lib/octocatalog-diff/puppetdb.rb +5 -1
- data/lib/octocatalog-diff/util/parallel.rb +20 -16
- data/lib/octocatalog-diff/util/util.rb +2 -0
- data/scripts/env/env.sh +1 -1
- data/scripts/git-extract/git-extract.sh +1 -1
- data/scripts/puppet/puppet.sh +1 -1
- metadata +37 -30
    
        data/doc/requirements.md
    CHANGED
    
    | @@ -2,10 +2,14 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            To run `octocatalog-diff` you will need these basics:
         | 
| 4 4 |  | 
| 5 | 
            -
            -  | 
| 5 | 
            +
            - An appropriate Puppet version and [corresponding ruby version](https://puppet.com/docs/puppet/5.4/system_requirements.html)
         | 
| 6 | 
            +
              - Puppet 5.x officially supports Ruby 2.4
         | 
| 7 | 
            +
              - Puppet 4.x officially supports Ruby 2.1, but seems to work fine with later versions as well
         | 
| 8 | 
            +
              - Puppet 3.8.7 -- we attempt to maintain compatibility in `octocatalog-diff` to facilitate upgrades even though this version is no longer supported by Puppet
         | 
| 9 | 
            +
              - We don't officially support Puppet 3.8.6 or before
         | 
| 6 10 | 
             
            - Mac OS, Linux, or other Unix-line operating system (Windows is not supported)
         | 
| 7 11 | 
             
            - Ability to install gems, e.g. with [rbenv](https://github.com/rbenv/rbenv) or [rvm](https://rvm.io/), or root privileges to install into the system Ruby
         | 
| 8 | 
            -
            - Puppet agent for [Linux](https://docs.puppet.com/puppet/latest/reference/install_linux.html) or [Mac OS X](https://docs.puppet.com/puppet/latest/reference/install_osx.html), or installed as a gem | 
| 12 | 
            +
            - Puppet agent for [Linux](https://docs.puppet.com/puppet/latest/reference/install_linux.html) or [Mac OS X](https://docs.puppet.com/puppet/latest/reference/install_osx.html), or installed as a gem
         | 
| 9 13 |  | 
| 10 14 | 
             
            We recommend that you also have the following to get the most out of `octocatalog-diff`, but these are not absolute requirements:
         | 
| 11 15 |  | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require 'diffy'
         | 
| 4 | 
            +
            require 'digest'
         | 
| 4 5 | 
             
            require 'hashdiff'
         | 
| 5 6 | 
             
            require 'json'
         | 
| 6 7 | 
             
            require 'set'
         | 
| @@ -11,6 +12,8 @@ require_relative '../errors' | |
| 11 12 | 
             
            require_relative '../util/util'
         | 
| 12 13 | 
             
            require_relative 'filter'
         | 
| 13 14 |  | 
| 15 | 
            +
            HashDiff = Hashdiff unless defined? HashDiff
         | 
| 16 | 
            +
             | 
| 14 17 | 
             
            module OctocatalogDiff
         | 
| 15 18 | 
             
              module CatalogDiff
         | 
| 16 19 | 
             
                # Calculate the difference between two Puppet catalogs.
         | 
| @@ -263,7 +266,7 @@ module OctocatalogDiff | |
| 263 266 |  | 
| 264 267 | 
             
                        # Handle parameters
         | 
| 265 268 | 
             
                        if k == 'parameters'
         | 
| 266 | 
            -
                          cleansed_param = cleanse_parameters_hash(v)
         | 
| 269 | 
            +
                          cleansed_param = cleanse_parameters_hash(v, resource.fetch('sensitive_parameters', []))
         | 
| 267 270 | 
             
                          hsh[k] = cleansed_param unless cleansed_param.nil? || cleansed_param.empty?
         | 
| 268 271 | 
             
                        elsif k == 'tags'
         | 
| 269 272 | 
             
                          # The order of tags is unimportant. Sort this array to avoid false diffs if order changes.
         | 
| @@ -294,10 +297,13 @@ module OctocatalogDiff | |
| 294 297 | 
             
                    # Use diffy to get only the lines that have changed in a text object.
         | 
| 295 298 | 
             
                    # As we iterate through the diff, jump out if we have our answer: either
         | 
| 296 299 | 
             
                    # true if '=~>' finds ANY match, or false if '=&>' fails to find a match.
         | 
| 297 | 
            -
                    Diffy::Diff.new(old_val, new_val, context: 0) | 
| 300 | 
            +
                    diffy_result = Diffy::Diff.new(old_val, new_val, context: 0)
         | 
| 301 | 
            +
                    newline_alerts = diffy_result.count { |line| line.strip == '\' }
         | 
| 302 | 
            +
                    diffy_result.each do |line|
         | 
| 298 303 | 
             
                      if regex.match(line.strip)
         | 
| 299 304 | 
             
                        return true if operator == '=~>'
         | 
| 300 305 | 
             
                      elsif operator == '=&>'
         | 
| 306 | 
            +
                        next if line.strip == '\' && newline_alerts == 2
         | 
| 301 307 | 
             
                        return false
         | 
| 302 308 | 
             
                      end
         | 
| 303 309 | 
             
                    end
         | 
| @@ -334,7 +340,8 @@ module OctocatalogDiff | |
| 334 340 | 
             
                    #   =->  Attribute must have been removed and equal this
         | 
| 335 341 | 
             
                    #   =~>  Change must match regexp (one line of change matching is sufficient)
         | 
| 336 342 | 
             
                    #   =&>  Change must match regexp (all lines of change MUST match regexp)
         | 
| 337 | 
            -
                     | 
| 343 | 
            +
                    #   =s>  Change must be array and contain identical elements, ignoring order
         | 
| 344 | 
            +
                    if rule_attr =~ /\A(.+?)(=[\-\+~&s]?>)(.+)/m
         | 
| 338 345 | 
             
                      rule_attr = Regexp.last_match(1)
         | 
| 339 346 | 
             
                      operator = Regexp.last_match(2)
         | 
| 340 347 | 
             
                      value = Regexp.last_match(3)
         | 
| @@ -355,6 +362,9 @@ module OctocatalogDiff | |
| 355 362 | 
             
                          raise RegexpError, "Invalid ignore regexp for #{key}: #{exc.message}"
         | 
| 356 363 | 
             
                        end
         | 
| 357 364 | 
             
                        matcher = ->(x, y) { regexp_operator_match?(operator, my_regex, x, y) }
         | 
| 365 | 
            +
                      elsif operator == '=s>'
         | 
| 366 | 
            +
                        raise ArgumentError, "Invalid ignore option for =s>, must be '='" unless value == '='
         | 
| 367 | 
            +
                        matcher = ->(x, y) { x.is_a?(Array) && y.is_a?(Array) && Set.new(x) == Set.new(y) }
         | 
| 358 368 | 
             
                      end
         | 
| 359 369 | 
             
                    end
         | 
| 360 370 |  | 
| @@ -394,6 +404,13 @@ module OctocatalogDiff | |
| 394 404 | 
             
                      return false unless rule[:title].casecmp(hsh[:title]).zero?
         | 
| 395 405 | 
             
                    end
         | 
| 396 406 |  | 
| 407 | 
            +
                    # If rule[:attr] is a regular expression, handle that case here.
         | 
| 408 | 
            +
                    if rule[:attr].is_a?(Regexp)
         | 
| 409 | 
            +
                      return false unless hsh[:attr].is_a?(String)
         | 
| 410 | 
            +
                      return false unless rule[:attr].match(hsh[:attr])
         | 
| 411 | 
            +
                      return ignore_match_true(hsh, rule)
         | 
| 412 | 
            +
                    end
         | 
| 413 | 
            +
             | 
| 397 414 | 
             
                    # Special 'attributes': Ignore specific diff types (+ add, - remove, ~ and ! change)
         | 
| 398 415 | 
             
                    if rule[:attr] =~ /\A[\-\+~!]+\Z/
         | 
| 399 416 | 
             
                      return ignore_match_true(hsh, rule) if rule[:attr].include?(diff_type)
         | 
| @@ -446,10 +463,18 @@ module OctocatalogDiff | |
| 446 463 |  | 
| 447 464 | 
             
                  # Cleanse parameters of filtered attributes.
         | 
| 448 465 | 
             
                  # @param parameters_hash [Hash] Hash of parameters
         | 
| 466 | 
            +
                  # @param sensitive_parameters [Array] Array of sensitive parameters
         | 
| 449 467 | 
             
                  # @return [Hash] Cleaned parameters hash (original input hash is not altered)
         | 
| 450 | 
            -
                  def cleanse_parameters_hash(parameters_hash)
         | 
| 468 | 
            +
                  def cleanse_parameters_hash(parameters_hash, sensitive_parameters)
         | 
| 451 469 | 
             
                    result = parameters_hash.dup
         | 
| 452 470 |  | 
| 471 | 
            +
                    # hides sensitive params. We still need to know if there's a going to
         | 
| 472 | 
            +
                    # be a diff, so we hash the value.
         | 
| 473 | 
            +
                    sensitive_parameters.each do |p|
         | 
| 474 | 
            +
                      md5 = Digest::MD5.hexdigest Marshal.dump(result[p])
         | 
| 475 | 
            +
                      result[p] = 'Sensitive [md5sum ' + md5 + ']'
         | 
| 476 | 
            +
                    end
         | 
| 477 | 
            +
             | 
| 453 478 | 
             
                    # 'before' and 'require' handle internal Puppet ordering but do not affect what
         | 
| 454 479 | 
             
                    # happens on the target machine. Don't consider these for the purpose of catalog diff.
         | 
| 455 480 | 
             
                    result.delete('before')
         | 
| @@ -2,6 +2,7 @@ require_relative '../api/v1/diff' | |
| 2 2 | 
             
            require_relative 'filter/absent_file'
         | 
| 3 3 | 
             
            require_relative 'filter/compilation_dir'
         | 
| 4 4 | 
             
            require_relative 'filter/json'
         | 
| 5 | 
            +
            require_relative 'filter/single_item_array'
         | 
| 5 6 | 
             
            require_relative 'filter/yaml'
         | 
| 6 7 |  | 
| 7 8 | 
             
            require 'stringio'
         | 
| @@ -13,7 +14,7 @@ module OctocatalogDiff | |
| 13 14 | 
             
                  attr_accessor :logger
         | 
| 14 15 |  | 
| 15 16 | 
             
                  # List the available filters here (by class name) for use in the validator method.
         | 
| 16 | 
            -
                  AVAILABLE_FILTERS = %w(AbsentFile CompilationDir JSON YAML).freeze
         | 
| 17 | 
            +
                  AVAILABLE_FILTERS = %w(AbsentFile CompilationDir JSON SingleItemArray YAML).freeze
         | 
| 17 18 |  | 
| 18 19 | 
             
                  # Public: Determine whether a particular filter exists. This can be used to validate
         | 
| 19 20 | 
             
                  # a user-submitted filter.
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require_relative '../filter'
         | 
| 4 | 
            +
            require_relative '../../util/util'
         | 
| 4 5 |  | 
| 5 6 | 
             
            module OctocatalogDiff
         | 
| 6 7 | 
             
              module CatalogDiff
         | 
| @@ -35,43 +36,46 @@ module OctocatalogDiff | |
| 35 36 |  | 
| 36 37 | 
             
                      # Check for a change where the difference in a parameter exactly corresponds to the difference in the
         | 
| 37 38 | 
             
                      # compilation directory.
         | 
| 38 | 
            -
                      if diff.change? | 
| 39 | 
            -
                         | 
| 40 | 
            -
                         | 
| 41 | 
            -
                        from_match = false
         | 
| 42 | 
            -
                        to_before = nil
         | 
| 43 | 
            -
                        to_after = nil
         | 
| 44 | 
            -
                        to_match = false
         | 
| 39 | 
            +
                      if diff.change?
         | 
| 40 | 
            +
                        o = remove_compilation_dir(diff.old_value, dir2)
         | 
| 41 | 
            +
                        n = remove_compilation_dir(diff.new_value, dir1)
         | 
| 45 42 |  | 
| 46 | 
            -
                        if diff.old_value  | 
| 47 | 
            -
                          from_before = Regexp.last_match(1) || ''
         | 
| 48 | 
            -
                          from_after = Regexp.last_match(2) || ''
         | 
| 49 | 
            -
                          from_match = true
         | 
| 50 | 
            -
                        end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                        if diff.new_value =~ /^(.*)#{dir1}(.*)$/m
         | 
| 53 | 
            -
                          to_before = Regexp.last_match(1) || ''
         | 
| 54 | 
            -
                          to_after = Regexp.last_match(2) || ''
         | 
| 55 | 
            -
                          to_match = true
         | 
| 56 | 
            -
                        end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                        if from_match && to_match && to_before == from_before && to_after == from_after
         | 
| 43 | 
            +
                        if o != diff.old_value || n != diff.new_value
         | 
| 59 44 | 
             
                          message = "Resource key #{diff.type}[#{diff.title}] #{diff.structure.join(' => ')}"
         | 
| 60 | 
            -
                          message += '  | 
| 45 | 
            +
                          message += ' may depend on catalog compilation directory, but there may be differences.'
         | 
| 46 | 
            +
                          message += ' This is included in results for now, but please verify.'
         | 
| 61 47 | 
             
                          @logger.warn message
         | 
| 62 | 
            -
                          return true
         | 
| 63 48 | 
             
                        end
         | 
| 64 49 |  | 
| 65 | 
            -
                        if  | 
| 50 | 
            +
                        if o == n
         | 
| 66 51 | 
             
                          message = "Resource key #{diff.type}[#{diff.title}] #{diff.structure.join(' => ')}"
         | 
| 67 | 
            -
                          message += '  | 
| 68 | 
            -
                          message += ' This is included in results for now, but please verify.'
         | 
| 52 | 
            +
                          message += ' appears to depend on catalog compilation directory. Suppressed from results.'
         | 
| 69 53 | 
             
                          @logger.warn message
         | 
| 54 | 
            +
                          return true
         | 
| 70 55 | 
             
                        end
         | 
| 71 56 | 
             
                      end
         | 
| 72 57 |  | 
| 73 58 | 
             
                      false
         | 
| 74 59 | 
             
                    end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    def remove_compilation_dir(v, dir)
         | 
| 62 | 
            +
                      value = OctocatalogDiff::Util::Util.deep_dup(v)
         | 
| 63 | 
            +
                      traverse(value) do |e|
         | 
| 64 | 
            +
                        e.gsub!(dir, '') if e.respond_to?(:gsub!)
         | 
| 65 | 
            +
                      end
         | 
| 66 | 
            +
                      value
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    def traverse(a)
         | 
| 70 | 
            +
                      case a
         | 
| 71 | 
            +
                      when Array
         | 
| 72 | 
            +
                        a.map { |v| traverse(v, &Proc.new) }
         | 
| 73 | 
            +
                      when Hash
         | 
| 74 | 
            +
                        traverse(a.values, &Proc.new)
         | 
| 75 | 
            +
                      else
         | 
| 76 | 
            +
                        yield a
         | 
| 77 | 
            +
                      end
         | 
| 78 | 
            +
                    end
         | 
| 75 79 | 
             
                  end
         | 
| 76 80 | 
             
                end
         | 
| 77 81 | 
             
              end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../filter'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module OctocatalogDiff
         | 
| 6 | 
            +
              module CatalogDiff
         | 
| 7 | 
            +
                class Filter
         | 
| 8 | 
            +
                  # Filter out changes in parameters when one catalog has a parameter that's an object and
         | 
| 9 | 
            +
                  # the other catalog has that same parameter as an array containing the same object.
         | 
| 10 | 
            +
                  # For example, under this filter, the following is not a change:
         | 
| 11 | 
            +
                  #   catalog1: notify => "Service[foo]"
         | 
| 12 | 
            +
                  #   catalog2: notify => ["Service[foo]"]
         | 
| 13 | 
            +
                  class SingleItemArray < OctocatalogDiff::CatalogDiff::Filter
         | 
| 14 | 
            +
                    # Public: Implement the filter for single-item arrays whose item exactly matches the
         | 
| 15 | 
            +
                    # item that's not in an array in the other catalog.
         | 
| 16 | 
            +
                    #
         | 
| 17 | 
            +
                    # @param diff [OctocatalogDiff::API::V1::Diff] Difference
         | 
| 18 | 
            +
                    # @param _options [Hash] Additional options (there are none for this filter)
         | 
| 19 | 
            +
                    # @return [Boolean] true if this should be filtered out, false otherwise
         | 
| 20 | 
            +
                    def filtered?(diff, _options = {})
         | 
| 21 | 
            +
                      # Skip additions or removals - focus only on changes
         | 
| 22 | 
            +
                      return false unless diff.change?
         | 
| 23 | 
            +
                      old_value = diff.old_value
         | 
| 24 | 
            +
                      new_value = diff.new_value
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      # Skip unless there is a single-item array under consideration
         | 
| 27 | 
            +
                      return false unless
         | 
| 28 | 
            +
                        (old_value.is_a?(Array) && old_value.size == 1) ||
         | 
| 29 | 
            +
                        (new_value.is_a?(Array) && new_value.size == 1)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      # Skip if both the old value and new value are arrays
         | 
| 32 | 
            +
                      return false if old_value.is_a?(Array) && new_value.is_a?(Array)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      # Do comparison
         | 
| 35 | 
            +
                      if old_value.is_a?(Array)
         | 
| 36 | 
            +
                        old_value.first == new_value
         | 
| 37 | 
            +
                      else
         | 
| 38 | 
            +
                        new_value.first == old_value
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -149,13 +149,13 @@ module OctocatalogDiff | |
| 149 149 | 
             
                      raise ArgumentError, 'Called install_fact_file without node, or with an empty node'
         | 
| 150 150 | 
             
                    end
         | 
| 151 151 |  | 
| 152 | 
            -
                    facts = if options[: | 
| 152 | 
            +
                    facts = if options[:facts].is_a?(OctocatalogDiff::Facts)
         | 
| 153 | 
            +
                      options[:facts].dup
         | 
| 154 | 
            +
                    elsif options[:fact_file]
         | 
| 153 155 | 
             
                      raise Errno::ENOENT, "Fact file #{options[:fact_file]} does not exist" unless File.file?(options[:fact_file])
         | 
| 154 156 | 
             
                      fact_file_opts = { fact_file_string: File.read(options[:fact_file]) }
         | 
| 155 157 | 
             
                      fact_file_opts[:backend] = Regexp.last_match(1).to_sym if options[:fact_file] =~ /.*\.(\w+)$/
         | 
| 156 158 | 
             
                      OctocatalogDiff::Facts.new(fact_file_opts)
         | 
| 157 | 
            -
                    elsif options[:facts].is_a?(OctocatalogDiff::Facts)
         | 
| 158 | 
            -
                      options[:facts].dup
         | 
| 159 159 | 
             
                    else
         | 
| 160 160 | 
             
                      raise ArgumentError, 'No facts passed to "install_fact_file" method'
         | 
| 161 161 | 
             
                    end
         | 
| @@ -54,9 +54,21 @@ module OctocatalogDiff | |
| 54 54 | 
             
                    raise ArgumentError, 'Puppet binary was not supplied' if @puppet_binary.nil?
         | 
| 55 55 | 
             
                    raise Errno::ENOENT, "Puppet binary #{@puppet_binary} doesn't exist" unless File.file?(@puppet_binary)
         | 
| 56 56 |  | 
| 57 | 
            +
                    puppet_version = Gem::Version.new(@options[:puppet_version])
         | 
| 58 | 
            +
             | 
| 57 59 | 
             
                    # Node to compile
         | 
| 58 60 | 
             
                    cmdline = []
         | 
| 59 | 
            -
                     | 
| 61 | 
            +
                    # The 'puppet master --compile' command was removed in Puppet 6.x and replaced in
         | 
| 62 | 
            +
                    # Puppet 6.5 with an identically functioning 'puppet catalog compile' command.
         | 
| 63 | 
            +
                    # From versions 6.0.0 until 6.5.0 there is no compatible invocation method.
         | 
| 64 | 
            +
                    if puppet_version < Gem::Version.new('6.0.0')
         | 
| 65 | 
            +
                      cmdline.concat ['master', '--compile', Shellwords.escape(@node)]
         | 
| 66 | 
            +
                    elsif puppet_version < Gem::Version.new('6.5.0')
         | 
| 67 | 
            +
                      raise OctocatalogDiff::Errors::PuppetVersionError,
         | 
| 68 | 
            +
                            'Octocatalog-diff does not support Puppet versions >= 6.0.0 and < 6.5.0'
         | 
| 69 | 
            +
                    else
         | 
| 70 | 
            +
                      cmdline.concat ['catalog', 'compile', Shellwords.escape(@node)]
         | 
| 71 | 
            +
                    end
         | 
| 60 72 |  | 
| 61 73 | 
             
                    # storeconfigs?
         | 
| 62 74 | 
             
                    if @options[:storeconfigs]
         | 
| @@ -93,11 +105,21 @@ module OctocatalogDiff | |
| 93 105 | 
             
                    # Some typical options for puppet
         | 
| 94 106 | 
             
                    cmdline.concat %w(
         | 
| 95 107 | 
             
                      --no-daemonize
         | 
| 96 | 
            -
                      --no-ca
         | 
| 97 108 | 
             
                      --color=false
         | 
| 98 | 
            -
                      --config_version="/bin/echo catalogscript"
         | 
| 99 109 | 
             
                    )
         | 
| 100 110 |  | 
| 111 | 
            +
                    if puppet_version < Gem::Version.new('6.0.0')
         | 
| 112 | 
            +
                      # This config_version parameter causes an error when run with Puppet 6.x. Per
         | 
| 113 | 
            +
                      # the Puppet configuration settings docs, the below config_version argument
         | 
| 114 | 
            +
                      # may not actually be valid, but for backward compatibility's sake we'll keep it
         | 
| 115 | 
            +
                      # for the versions it has always worked with:
         | 
| 116 | 
            +
                      cmdline.concat ['--config_version="/bin/echo catalogscript"']
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                      # The 'ca' configuration option was removed in Puppet 6, but we'll keep it
         | 
| 119 | 
            +
                      # for older versions:
         | 
| 120 | 
            +
                      cmdline.concat ['--no-ca']
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 101 123 | 
             
                    # Add environment - only make this variable if preserve_environments is used.
         | 
| 102 124 | 
             
                    # If preserve_environments is not used, the hard-coded 'production' here matches
         | 
| 103 125 | 
             
                    # up with the symlink created under the temporary directory structure.
         | 
| @@ -191,6 +191,8 @@ module OctocatalogDiff | |
| 191 191 | 
             
                  build
         | 
| 192 192 | 
             
                  raise OctocatalogDiff::Errors::CatalogError, 'Catalog does not appear to have been built' if !valid? && error_message.nil?
         | 
| 193 193 | 
             
                  raise OctocatalogDiff::Errors::CatalogError, error_message unless valid?
         | 
| 194 | 
            +
                  # Handle the structure returned by the /puppet/v4/catalog Puppetserver endpoint:
         | 
| 195 | 
            +
                  return @catalog['catalog']['resources'] if @catalog['catalog'].is_a?(Hash) && @catalog['catalog']['resources'].is_a?(Array)
         | 
| 194 196 | 
             
                  return @catalog['data']['resources'] if @catalog['data'].is_a?(Hash) && @catalog['data']['resources'].is_a?(Array)
         | 
| 195 197 | 
             
                  return @catalog['resources'] if @catalog['resources'].is_a?(Array)
         | 
| 196 198 | 
             
                  # This is a bug condition
         | 
| @@ -304,20 +306,36 @@ module OctocatalogDiff | |
| 304 306 | 
             
                    unless res =~ /\A([\w:]+)\[(.+)\]\z/
         | 
| 305 307 | 
             
                      raise ArgumentError, "Resource #{res} is not in the expected format"
         | 
| 306 308 | 
             
                    end
         | 
| 307 | 
            -
             | 
| 309 | 
            +
             | 
| 310 | 
            +
                    type = Regexp.last_match(1)
         | 
| 311 | 
            +
                    title = normalized_title(Regexp.last_match(2), type)
         | 
| 312 | 
            +
                    resource(type: type, title: title).nil?
         | 
| 308 313 | 
             
                  end
         | 
| 309 314 | 
             
                end
         | 
| 310 315 |  | 
| 316 | 
            +
                # Private method: Given a title string, normalize it according to the rules
         | 
| 317 | 
            +
                # used by puppet 4.10.x for file resource title normalization:
         | 
| 318 | 
            +
                # https://github.com/puppetlabs/puppet/blob/4.10.x/lib/puppet/type/file.rb#L42
         | 
| 319 | 
            +
                def normalized_title(title_string, type)
         | 
| 320 | 
            +
                  return title_string if type != 'File'
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                  matches = title_string.match(%r{^(?<normalized_path>/|.+:/|.*[^/])/*\Z}m)
         | 
| 323 | 
            +
                  matches[:normalized_path] || title_string
         | 
| 324 | 
            +
                end
         | 
| 325 | 
            +
             | 
| 311 326 | 
             
                # Private method: Build the resource hash to be used used for O(1) lookups by type and title.
         | 
| 312 327 | 
             
                # This method is called the first time the resource hash is accessed.
         | 
| 313 328 | 
             
                def build_resource_hash
         | 
| 314 329 | 
             
                  @resource_hash = {}
         | 
| 315 330 | 
             
                  resources.each do |resource|
         | 
| 316 331 | 
             
                    @resource_hash[resource['type']] ||= {}
         | 
| 317 | 
            -
                    @resource_hash[resource['type']][resource['title']] = resource
         | 
| 318 332 |  | 
| 319 | 
            -
                     | 
| 320 | 
            -
             | 
| 333 | 
            +
                    title = normalized_title(resource['title'], resource['type'])
         | 
| 334 | 
            +
                    @resource_hash[resource['type']][title] = resource
         | 
| 335 | 
            +
             | 
| 336 | 
            +
                    if resource.key?('parameters')
         | 
| 337 | 
            +
                      @resource_hash[resource['type']][resource['parameters']['alias']] = resource if resource['parameters'].key?('alias')
         | 
| 338 | 
            +
                      @resource_hash[resource['type']][resource['parameters']['name']] = resource if resource['parameters'].key?('name')
         | 
| 321 339 | 
             
                    end
         | 
| 322 340 | 
             
                  end
         | 
| 323 341 | 
             
                end
         | 
| @@ -147,7 +147,8 @@ module OctocatalogDiff | |
| 147 147 | 
             
                        puppet_binary: @puppet_binary,
         | 
| 148 148 | 
             
                        fact_file: @builddir.fact_file,
         | 
| 149 149 | 
             
                        dir: @builddir.tempdir,
         | 
| 150 | 
            -
                        enc: @builddir.enc
         | 
| 150 | 
            +
                        enc: @builddir.enc,
         | 
| 151 | 
            +
                        puppet_version: puppet_version
         | 
| 151 152 | 
             
                      )
         | 
| 152 153 | 
             
                      OctocatalogDiff::CatalogUtil::Command.new(command_opts)
         | 
| 153 154 | 
             
                    end
         | 
| @@ -62,16 +62,19 @@ module OctocatalogDiff | |
| 62 62 | 
             
                    fetch_catalog(logger)
         | 
| 63 63 | 
             
                  end
         | 
| 64 64 |  | 
| 65 | 
            -
                  # Returns a hash of parameters for  | 
| 65 | 
            +
                  # Returns a hash of parameters for the requested version of the Puppet Server Catalog API.
         | 
| 66 66 | 
             
                  # @return [Hash] Hash of parameters
         | 
| 67 67 | 
             
                  #
         | 
| 68 68 | 
             
                  # Note: The double escaping of the facts here is implemented to correspond to a long standing
         | 
| 69 69 | 
             
                  # bug in the Puppet code. See https://github.com/puppetlabs/puppet/pull/1818 and
         | 
| 70 70 | 
             
                  # https://docs.puppet.com/puppet/latest/http_api/http_catalog.html#parameters for explanation.
         | 
| 71 | 
            -
                  def puppet_catalog_api
         | 
| 72 | 
            -
                    {
         | 
| 71 | 
            +
                  def puppet_catalog_api(version)
         | 
| 72 | 
            +
                    api_style = {
         | 
| 73 73 | 
             
                      2 => {
         | 
| 74 74 | 
             
                        url: "https://#{@options[:puppet_master]}/#{@options[:branch]}/catalog/#{@node}",
         | 
| 75 | 
            +
                        headers: {
         | 
| 76 | 
            +
                          'Accept' => 'text/pson'
         | 
| 77 | 
            +
                        },
         | 
| 75 78 | 
             
                        parameters: {
         | 
| 76 79 | 
             
                          'facts_format' => 'pson',
         | 
| 77 80 | 
             
                          'facts' => CGI.escape(@facts.fudge_timestamp.without('trusted').to_pson),
         | 
| @@ -80,24 +83,59 @@ module OctocatalogDiff | |
| 80 83 | 
             
                      },
         | 
| 81 84 | 
             
                      3 => {
         | 
| 82 85 | 
             
                        url: "https://#{@options[:puppet_master]}/puppet/v3/catalog/#{@node}",
         | 
| 86 | 
            +
                        headers: {
         | 
| 87 | 
            +
                          'Accept' => 'text/pson'
         | 
| 88 | 
            +
                        },
         | 
| 83 89 | 
             
                        parameters: {
         | 
| 84 90 | 
             
                          'environment' => @options[:branch],
         | 
| 85 91 | 
             
                          'facts_format' => 'pson',
         | 
| 86 92 | 
             
                          'facts' => CGI.escape(@facts.fudge_timestamp.without('trusted').to_pson),
         | 
| 87 93 | 
             
                          'transaction_uuid' => SecureRandom.uuid
         | 
| 88 94 | 
             
                        }
         | 
| 95 | 
            +
                      },
         | 
| 96 | 
            +
                      4 => {
         | 
| 97 | 
            +
                        url: "https://#{@options[:puppet_master]}/puppet/v4/catalog",
         | 
| 98 | 
            +
                        headers: {
         | 
| 99 | 
            +
                          'Content-Type' => 'application/json'
         | 
| 100 | 
            +
                        },
         | 
| 101 | 
            +
                        parameters: {
         | 
| 102 | 
            +
                          'certname' => @node,
         | 
| 103 | 
            +
                          'persistence' => {
         | 
| 104 | 
            +
                            'facts' => @options[:puppet_master_update_facts] || false,
         | 
| 105 | 
            +
                            'catalog' => @options[:puppet_master_update_catalog] || false
         | 
| 106 | 
            +
                          },
         | 
| 107 | 
            +
                          'environment' => @options[:branch],
         | 
| 108 | 
            +
                          'facts' => { 'values' => @facts.facts['values'] },
         | 
| 109 | 
            +
                          'options' => {
         | 
| 110 | 
            +
                            'prefer_requested_environment' => true,
         | 
| 111 | 
            +
                            'capture_logs' => false,
         | 
| 112 | 
            +
                            'log_level' => 'warning'
         | 
| 113 | 
            +
                          },
         | 
| 114 | 
            +
                          'transaction_uuid' => SecureRandom.uuid
         | 
| 115 | 
            +
                        }
         | 
| 89 116 | 
             
                      }
         | 
| 90 117 | 
             
                    }
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    params = api_style[version]
         | 
| 120 | 
            +
                    return nil if params.nil?
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    unless @options[:puppet_master_token].nil?
         | 
| 123 | 
            +
                      params[:headers]['X-Authentication'] = @options[:puppet_master_token]
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    params[:parameters] = params[:parameters].to_json if version >= 4
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    params
         | 
| 91 129 | 
             
                  end
         | 
| 92 130 |  | 
| 93 131 | 
             
                  # Fetch catalog by contacting the Puppet master, sending the facts, and asking for the catalog. When the
         | 
| 94 132 | 
             
                  # catalog is returned in PSON format, parse it to JSON and then set appropriate variables.
         | 
| 95 133 | 
             
                  def fetch_catalog(logger)
         | 
| 96 134 | 
             
                    api_version = @options[:puppet_master_api_version] || DEFAULT_PUPPET_SERVER_API
         | 
| 97 | 
            -
                    api = puppet_catalog_api | 
| 135 | 
            +
                    api = puppet_catalog_api(api_version)
         | 
| 98 136 | 
             
                    raise ArgumentError, "Unsupported or invalid API version #{api_version}" unless api.is_a?(Hash)
         | 
| 99 137 |  | 
| 100 | 
            -
                    more_options = { headers:  | 
| 138 | 
            +
                    more_options = { headers: api[:headers], timeout: @timeout }
         | 
| 101 139 | 
             
                    post_hash = api[:parameters]
         | 
| 102 140 |  | 
| 103 141 | 
             
                    response = nil
         |