octocatalog-diff 1.5.2 → 2.1.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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.version +1 -1
  3. data/README.md +4 -4
  4. data/doc/CHANGELOG.md +50 -0
  5. data/doc/advanced-compare-file-text.md +79 -0
  6. data/doc/advanced-environments.md +5 -5
  7. data/doc/advanced-ignores.md +10 -0
  8. data/doc/advanced-puppet-master.md +25 -7
  9. data/doc/configuration-puppet.md +19 -19
  10. data/doc/configuration-puppetdb.md +8 -0
  11. data/doc/configuration.md +31 -31
  12. data/doc/dev/api/v1/calls/catalog-diff.md +6 -2
  13. data/doc/dev/api/v1/objects/diff.md +3 -3
  14. data/doc/dev/integration-tests.md +2 -2
  15. data/doc/dev/releasing.md +41 -41
  16. data/doc/dev/run-from-branch.md +23 -23
  17. data/doc/installation.md +14 -14
  18. data/doc/limitations.md +9 -9
  19. data/doc/optionsref.md +166 -11
  20. data/doc/requirements.md +6 -2
  21. data/lib/octocatalog-diff/catalog-diff/differ.rb +32 -5
  22. data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +29 -25
  23. data/lib/octocatalog-diff/catalog-util/command.rb +25 -3
  24. data/lib/octocatalog-diff/catalog-util/fileresources.rb +39 -16
  25. data/lib/octocatalog-diff/catalog.rb +22 -4
  26. data/lib/octocatalog-diff/catalog/computed.rb +2 -1
  27. data/lib/octocatalog-diff/catalog/puppetmaster.rb +43 -5
  28. data/lib/octocatalog-diff/cli.rb +38 -6
  29. data/lib/octocatalog-diff/cli/options.rb +36 -1
  30. data/lib/octocatalog-diff/cli/options/compare_file_text.rb +18 -0
  31. data/lib/octocatalog-diff/cli/options/hostname.rb +13 -2
  32. data/lib/octocatalog-diff/cli/options/puppet_master_api_version.rb +2 -2
  33. data/lib/octocatalog-diff/cli/options/puppet_master_token.rb +20 -0
  34. data/lib/octocatalog-diff/cli/options/puppet_master_token_file.rb +35 -0
  35. data/lib/octocatalog-diff/cli/options/puppet_master_update_catalog.rb +20 -0
  36. data/lib/octocatalog-diff/cli/options/puppet_master_update_facts.rb +20 -0
  37. data/lib/octocatalog-diff/cli/options/puppetdb_package_inventory.rb +18 -0
  38. data/lib/octocatalog-diff/cli/options/use_lcs.rb +14 -0
  39. data/lib/octocatalog-diff/facts/json.rb +13 -2
  40. data/lib/octocatalog-diff/facts/puppetdb.rb +43 -2
  41. data/lib/octocatalog-diff/util/parallel.rb +20 -16
  42. data/lib/octocatalog-diff/util/util.rb +2 -0
  43. data/scripts/env/env.sh +1 -1
  44. data/scripts/git-extract/git-extract.sh +1 -1
  45. data/scripts/puppet/puppet.sh +1 -1
  46. metadata +36 -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
- - Ruby 2.0 through 2.4 (we test octocatalog-diff with Ruby 2.0, 2.1, 2.2, 2.3, and 2.4)
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 (we support Puppet 3.8.7 and all versions of Puppet 4.x)
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).each do |line|
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
- if rule_attr =~ /\A(.+?)(=[\-\+~&]?>)(.+)/m
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')
@@ -510,9 +535,11 @@ module OctocatalogDiff
510
535
  catalog2_resources = catalog2_in[:catalog]
511
536
 
512
537
  @logger.debug "Entering hashdiff_initial; catalog sizes: #{catalog1_resources.size}, #{catalog2_resources.size}"
538
+ use_lcs = @opts.fetch(:use_lcs, true)
539
+ @logger.debug "HashDiff configuration: (use_lcs: #{use_lcs})"
513
540
  result = []
514
541
  hashdiff_add_remove = Set.new
515
- hashdiff_result = HashDiff.diff(catalog1_resources, catalog2_resources, delimiter: "\f")
542
+ hashdiff_result = HashDiff.diff(catalog1_resources, catalog2_resources, delimiter: "\f", use_lcs: use_lcs)
516
543
  hashdiff_result.each do |obj|
517
544
  # Regular change
518
545
  if obj[0] == '~'
@@ -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? && (diff.old_value.is_a?(String) || diff.new_value.is_a?(String))
39
- from_before = nil
40
- from_after = nil
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 =~ /^(.*)#{dir2}(.*)$/m
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 += ' appears to depend on catalog compilation directory. Suppressed from results.'
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 from_match || to_match
50
+ if o == n
66
51
  message = "Resource key #{diff.type}[#{diff.title}] #{diff.structure.join(' => ')}"
67
- message += ' may depend on catalog compilation directory, but there may be differences.'
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
@@ -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
- cmdline.concat ['master', '--compile', Shellwords.escape(@node)]
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.
@@ -13,9 +13,16 @@ module OctocatalogDiff
13
13
  # Public method: Convert file resources to text. See the description of the class
14
14
  # just above for details.
15
15
  # @param obj [OctocatalogDiff::Catalog] Catalog object (will be modified)
16
+ # @param environment [String] Environment (defaults to production)
16
17
  def self.convert_file_resources(obj, environment = 'production')
17
18
  return unless obj.valid? && obj.compilation_dir.is_a?(String) && !obj.compilation_dir.empty?
18
- _convert_file_resources(obj.resources, obj.compilation_dir, environment)
19
+ _convert_file_resources(
20
+ obj.resources,
21
+ obj.compilation_dir,
22
+ environment,
23
+ obj.options[:compare_file_text_ignore_tags],
24
+ obj.options[:tag]
25
+ )
19
26
  begin
20
27
  obj.catalog_json = ::JSON.generate(obj.catalog)
21
28
  rescue ::JSON::GeneratorError => exc
@@ -59,7 +66,7 @@ module OctocatalogDiff
59
66
  result = []
60
67
  Regexp.last_match(1).split(/:/).map(&:strip).each do |path|
61
68
  next if path.start_with?('$')
62
- result << File.expand_path(path, dir)
69
+ result.concat(Dir.glob(File.expand_path(path, dir)))
63
70
  end
64
71
  result
65
72
  else
@@ -70,8 +77,11 @@ module OctocatalogDiff
70
77
  # Internal method: Static method to convert file resources. The compilation directory is
71
78
  # required, or else this is a no-op. The passed-in array of resources is modified by this method.
72
79
  # @param resources [Array<Hash>] Array of catalog resources
73
- # @param compilation_dir [String] Compilation directory (so files can be looked up)
74
- def self._convert_file_resources(resources, compilation_dir, environment = 'production')
80
+ # @param compilation_dir [String] Compilation directory
81
+ # @param environment [String] Environment
82
+ # @param ignore_tags [Array<String>] Tags that exempt a resource from conversion
83
+ # @param to_from_tag [String] Either "to" or "from" for catalog type
84
+ def self._convert_file_resources(resources, compilation_dir, environment, ignore_tags, to_from_tag)
75
85
  # Calculate compilation directory. There is not explicit error checking here because
76
86
  # there is on-demand, explicit error checking for each file within the modification loop.
77
87
  return unless compilation_dir.is_a?(String) && compilation_dir != ''
@@ -88,14 +98,10 @@ module OctocatalogDiff
88
98
  resources.map! do |resource|
89
99
  if resource_convertible?(resource)
90
100
  path = file_path(resource['parameters']['source'], modulepaths)
91
- if path.nil?
92
- # Pass this through as a wrapped exception, because it's more likely to be something wrong
93
- # in the catalog itself than it is to be a broken setup of octocatalog-diff.
94
- message = "Errno::ENOENT: Unable to resolve '#{resource['parameters']['source']}'!"
95
- raise OctocatalogDiff::Errors::CatalogError, message
96
- end
97
101
 
98
- if File.file?(path)
102
+ if resource['tags'] && ignore_tags && (resource['tags'] & ignore_tags).any?
103
+ # Resource tagged not to be converted -- do nothing.
104
+ elsif path && File.file?(path)
99
105
  # If the file is found, read its content. If the content is all ASCII, substitute it into
100
106
  # the 'content' parameter for easier comparison. If not, instead populate the md5sum.
101
107
  # Delete the 'source' attribute as well.
@@ -103,23 +109,40 @@ module OctocatalogDiff
103
109
  is_ascii = content.force_encoding('UTF-8').ascii_only?
104
110
  resource['parameters']['content'] = is_ascii ? content : '{md5}' + Digest::MD5.hexdigest(content)
105
111
  resource['parameters'].delete('source')
106
- elsif File.exist?(path)
112
+ elsif path && File.exist?(path)
107
113
  # We are not handling recursive file installs from a directory or anything else.
108
114
  # However, the fact that we found *something* at this location indicates that the catalog
109
115
  # is probably correct. Hence, the very general .exist? check.
116
+ elsif to_from_tag == 'from'
117
+ # Don't raise an exception for an invalid source in the "from"
118
+ # catalog, because the developer may be fixing this in the "to"
119
+ # catalog. If it's broken in the "to" catalog as well, the
120
+ # exception will be raised when this code runs on that catalog.
110
121
  else
111
- # This is probably a bug
112
- # :nocov:
113
- raise "Unable to find '#{resource['parameters']['source']}' at #{path}!"
114
- # :nocov:
122
+ # Pass this through as a wrapped exception, because it's more likely to be something wrong
123
+ # in the catalog itself than it is to be a broken setup of octocatalog-diff.
124
+ #
125
+ # Example error: <OctocatalogDiff::Errors::CatalogError: Unable to resolve
126
+ # source=>'puppet:///modules/test/tmp/bar' in File[/tmp/bar]
127
+ # (/x/modules/test/manifests/init.pp:46)>
128
+ source = resource['parameters']['source']
129
+ type = resource['type']
130
+ title = resource['title']
131
+ file = resource['file'].sub(Regexp.new('^' + Regexp.escape(env_dir) + '/'), '')
132
+ line = resource['line']
133
+ message = "Unable to resolve source=>'#{source}' in #{type}[#{title}] (#{file}:#{line})"
134
+ raise OctocatalogDiff::Errors::CatalogError, message
115
135
  end
116
136
  end
137
+
117
138
  resource
118
139
  end
119
140
  end
120
141
 
121
142
  # Internal method: Determine if a resource is convertible. It is convertible if it
122
143
  # is a file resource with no declared 'content' and with a declared and parseable 'source'.
144
+ # It is not convertible if the resource is tagged with one of the tags declared by
145
+ # the option `--compare-file-text-ignore-tags`.
123
146
  # @param resource [Hash] Resource to check
124
147
  # @return [Boolean] True of resource is convertible, false if not
125
148
  def self.resource_convertible?(resource)
@@ -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
- resource(type: Regexp.last_match(1), title: Regexp.last_match(2)).nil?
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
- if resource.key?('parameters') && resource['parameters'].key?('alias')
320
- @resource_hash[resource['type']][resource['parameters']['alias']] = resource
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 each supported version of the Puppet Server Catalog API.
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[api_version]
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: { 'Accept' => 'text/pson' }, timeout: @timeout }
138
+ more_options = { headers: api[:headers], timeout: @timeout }
101
139
  post_hash = api[:parameters]
102
140
 
103
141
  response = nil