octocatalog-diff 0.5.1
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 +7 -0
- data/.version +1 -0
- data/LICENSE +20 -0
- data/README.md +82 -0
- data/bin/octocatalog-diff +75 -0
- data/doc/advanced-bootstrap.md +33 -0
- data/doc/advanced-cache-dir.md +24 -0
- data/doc/advanced-catalog-only.md +37 -0
- data/doc/advanced-ci.md +13 -0
- data/doc/advanced-dynamic-ignores.md +123 -0
- data/doc/advanced-future-parser.md +11 -0
- data/doc/advanced-ignores.md +224 -0
- data/doc/advanced-output-formats.md +96 -0
- data/doc/advanced-output-hacks.md +45 -0
- data/doc/advanced-override-facts.md +67 -0
- data/doc/advanced-pe-enc.md +52 -0
- data/doc/advanced-puppet-master.md +50 -0
- data/doc/advanced-puppet-versions.md +9 -0
- data/doc/advanced-storeconfigs.md +72 -0
- data/doc/advanced-using-without-git.md +15 -0
- data/doc/advanced.md +43 -0
- data/doc/basic.md +70 -0
- data/doc/configuration-enc.md +69 -0
- data/doc/configuration-hiera.md +103 -0
- data/doc/configuration-puppetdb.md +49 -0
- data/doc/configuration.md +51 -0
- data/doc/dev/README.md +1 -0
- data/doc/dev/coverage.md +34 -0
- data/doc/dev/how-to-add-options.md +83 -0
- data/doc/dev/integration-tests.md +63 -0
- data/doc/dev/releasing.md +19 -0
- data/doc/installation.md +49 -0
- data/doc/limitations.md +34 -0
- data/doc/optionsref.md +947 -0
- data/doc/requirements.md +16 -0
- data/doc/roadmap.md +26 -0
- data/doc/similar.md +17 -0
- data/doc/troubleshooting.md +54 -0
- data/lib/octocatalog-diff.rb +12 -0
- data/lib/octocatalog-diff/bootstrap.rb +53 -0
- data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
- data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
- data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
- data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
- data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
- data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
- data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
- data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
- data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
- data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
- data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
- data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
- data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
- data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
- data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
- data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
- data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
- data/lib/octocatalog-diff/catalog.rb +209 -0
- data/lib/octocatalog-diff/catalog/computed.rb +205 -0
- data/lib/octocatalog-diff/catalog/json.rb +30 -0
- data/lib/octocatalog-diff/catalog/noop.rb +19 -0
- data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
- data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
- data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
- data/lib/octocatalog-diff/external/pson/README.md +20 -0
- data/lib/octocatalog-diff/external/pson/common.rb +370 -0
- data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
- data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
- data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
- data/lib/octocatalog-diff/external/pson/version.rb +8 -0
- data/lib/octocatalog-diff/facts.rb +125 -0
- data/lib/octocatalog-diff/facts/json.rb +20 -0
- data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
- data/lib/octocatalog-diff/facts/yaml.rb +29 -0
- data/lib/octocatalog-diff/puppetdb.rb +163 -0
- data/lib/octocatalog-diff/util/colored.rb +20 -0
- data/lib/octocatalog-diff/util/httparty.rb +158 -0
- data/lib/octocatalog-diff/util/parallel.rb +170 -0
- data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
- data/lib/octocatalog-diff/version.rb +7 -0
- metadata +386 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
# Specify the base URL for PuppetDB. This will generally look like https://puppetdb.yourdomain.com:8081
|
|
4
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
5
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
6
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:puppetdb_url) do
|
|
7
|
+
has_weight 310
|
|
8
|
+
|
|
9
|
+
def parse(parser, options)
|
|
10
|
+
parser.on('--puppetdb-url URL', 'PuppetDB base URL') do |url|
|
|
11
|
+
# Test the format of the incoming URL. Only HTTPS should really be used, but we will
|
|
12
|
+
# support HTTP begrudgingly as well.
|
|
13
|
+
obj = URI.parse(url)
|
|
14
|
+
raise ArgumentError, 'PuppetDB URL must be http or https' unless obj.is_a?(URI::HTTPS) || obj.is_a?(URI::HTTP)
|
|
15
|
+
options[:puppetdb_url] = url
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Quiet option
|
|
2
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
3
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
4
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:quiet) do
|
|
5
|
+
has_weight 120
|
|
6
|
+
|
|
7
|
+
def parse(parser, options)
|
|
8
|
+
parser.on('--[no-]quiet', '-q', 'Quiet (no status messages except errors)') do |x|
|
|
9
|
+
options[:quiet] = x
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Transient errors can cause catalog compilation problems. This adds an option to retry
|
|
2
|
+
# a failed catalog multiple times before kicking out an error message.
|
|
3
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
4
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
5
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:retry_failed_catalog) do
|
|
6
|
+
has_weight 230
|
|
7
|
+
|
|
8
|
+
def parse(parser, options)
|
|
9
|
+
parser.on('--retry-failed-catalog N', OptionParser::DecimalInteger, 'Retry building a failed catalog N times') do |x|
|
|
10
|
+
options[:retry_failed_catalog] = x
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# By specifying a directory path here, you are explicitly giving permission to the program
|
|
2
|
+
# to delete it if it believes it needs to be created (e.g., if the SHA has changed of the
|
|
3
|
+
# cached directory).
|
|
4
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
5
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
6
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:safe_to_delete_cached_master_dir) do
|
|
7
|
+
has_weight 160
|
|
8
|
+
|
|
9
|
+
def parse(parser, options)
|
|
10
|
+
parser.on('--safe-to-delete-cached-master-dir PATH', 'OK to delete cached master directory at this path') do |path_in|
|
|
11
|
+
path = File.absolute_path(path_in)
|
|
12
|
+
options[:safe_to_delete_cached_master_dir] = path
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Set storeconfigs (integration with PuppetDB for collected resources)
|
|
2
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
3
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
4
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:storeconfigs) do
|
|
5
|
+
has_weight 220
|
|
6
|
+
|
|
7
|
+
def parse(parser, options)
|
|
8
|
+
parser.on('--[no-]storeconfigs', 'Enable integration with puppetdb for collected resources') do |x|
|
|
9
|
+
options[:storeconfigs] = x
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# If enabled, this option will suppress changes to certain attributes of a file, if the
|
|
2
|
+
# file is specified to be 'absent' in the target catalog. Suppressed changes in this case
|
|
3
|
+
# include user, group, mode, and content, because a removed file has none of those.
|
|
4
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
5
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
6
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:suppress_absent_file_details) do
|
|
7
|
+
has_weight 600
|
|
8
|
+
|
|
9
|
+
def parse(parser, options)
|
|
10
|
+
parser.on('--[no-]suppress-absent-file-details', 'Suppress certain attributes of absent files') do |x|
|
|
11
|
+
options[:suppress_absent_file_details] = x
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Set the 'from' and 'to' branches, which is used to compile catalogs. A branch of '.' means to use
|
|
2
|
+
# the current contents of the base code directory without any git checkouts.
|
|
3
|
+
# @param parser [OptionParser object] The OptionParser argument
|
|
4
|
+
# @param options [Hash] Options hash being constructed; this is modified in this method.
|
|
5
|
+
OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:to_from_branch) do
|
|
6
|
+
has_weight 20
|
|
7
|
+
|
|
8
|
+
def parse(parser, options)
|
|
9
|
+
parser.on('--from FROM_BRANCH', '-f', 'Branch you are coming from') do |env|
|
|
10
|
+
options[:from_env] = env
|
|
11
|
+
end
|
|
12
|
+
parser.on('--to TO_BRANCH', '-t', 'Branch you are going to') do |env|
|
|
13
|
+
options[:to_env] = env
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require_relative '../display'
|
|
2
|
+
|
|
3
|
+
module OctocatalogDiff
|
|
4
|
+
module CatalogDiff
|
|
5
|
+
class Cli
|
|
6
|
+
# Wrapper around OctocatalogDiff::CatalogDiff::Display to set the options and
|
|
7
|
+
# output to a file or the screen depending on selection.
|
|
8
|
+
class Printer
|
|
9
|
+
# Class for thrown exceptions
|
|
10
|
+
class PrinterError < RuntimeError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Constructor
|
|
14
|
+
# @param options [Hash] Options from cli/options
|
|
15
|
+
# @param logger [Logger] Logger object
|
|
16
|
+
def initialize(options, logger)
|
|
17
|
+
@options = options
|
|
18
|
+
@logger = logger
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The method to call externally, passing in diffs. This takes the appropriate action
|
|
22
|
+
# based on options, which is either to write the result into an output file, or print
|
|
23
|
+
# the result on STDOUT. Does not return anything.
|
|
24
|
+
# @param diffs [OctocatalogDiff::CatalogDiff::Differ] Difference array
|
|
25
|
+
# @param from_dir [String] Directory in which "from" catalog was compiled
|
|
26
|
+
# @param to_dir [String] Directory in which "to" catalog was compiled
|
|
27
|
+
def printer(diffs, from_dir = nil, to_dir = nil)
|
|
28
|
+
display_opts = @options.merge(compilation_from_dir: from_dir, compilation_to_dir: to_dir)
|
|
29
|
+
diff_text = OctocatalogDiff::CatalogDiff::Display.output(diffs, display_opts, @logger)
|
|
30
|
+
if @options[:output_file].nil?
|
|
31
|
+
puts diff_text unless diff_text.empty?
|
|
32
|
+
else
|
|
33
|
+
output_to_file(diff_text)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Output to a file, handling errors related to writing files.
|
|
40
|
+
# @param diff_in [String|Array] Text to write to file
|
|
41
|
+
def output_to_file(diff_in)
|
|
42
|
+
diff_text = diff_in.is_a?(Array) ? diff_in.join("\n") : diff_in
|
|
43
|
+
File.open(@options[:output_file], 'w') { |f| f.write(diff_text) }
|
|
44
|
+
@logger.info "Wrote diff to #{@options[:output_file]}"
|
|
45
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR => exc
|
|
46
|
+
@logger.error "Cannot write to #{@options[:output_file]}: #{exc}"
|
|
47
|
+
raise PrinterError, "Cannot write to #{@options[:output_file]}: #{exc}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
require 'diffy'
|
|
2
|
+
require 'hashdiff'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
require_relative '../catalog'
|
|
8
|
+
|
|
9
|
+
module OctocatalogDiff
|
|
10
|
+
module CatalogDiff
|
|
11
|
+
# Calculate the difference between two Puppet catalogs.
|
|
12
|
+
# -----------------------------------------------------
|
|
13
|
+
# It was necessary to write our own code for this, and not just use some existing gem,
|
|
14
|
+
# for two main reasons:
|
|
15
|
+
#
|
|
16
|
+
# 1. There are things that we want to ignore when doing a Puppet catalog diff. For example
|
|
17
|
+
# we want to ignore 'before' and 'require' parameters (because those affect the order of
|
|
18
|
+
# operations only, not the end result) and we probably want to ignore 'tags' attributes
|
|
19
|
+
# and all classes. No existing code (that I could find at least) was capable of allowing
|
|
20
|
+
# you to skip stuff via arguments, without your own custom pre-processing.
|
|
21
|
+
#
|
|
22
|
+
# 2. When using the 'hashdiff' gem, there is no distinguishing between an addition of an entire
|
|
23
|
+
# new key-value pair, or the addition of an element in a deeply nested array. By way of
|
|
24
|
+
# further explanation, consider these two data structures:
|
|
25
|
+
#
|
|
26
|
+
# a = { 'foo' => 'bar', 'my_array' => [ 1, 2, 3 ] }
|
|
27
|
+
# b = { 'foo' => 'bar', 'my_array' => [ 1, 2, 3, 4 ], 'another_key' => 'another_value'
|
|
28
|
+
#
|
|
29
|
+
# The hashdiff gem would report the differences between a and b to be:
|
|
30
|
+
# + 4
|
|
31
|
+
# + another_key => another_value
|
|
32
|
+
#
|
|
33
|
+
# We want to distinguish (without a whole bunch of convoluted code) between these two situations.
|
|
34
|
+
# One was a true addition (adding a key) while one was a change (adding element to array). This
|
|
35
|
+
# distinction becomes even more important when considering top-level changes vs. changes to arrays
|
|
36
|
+
# or hashes nested within the catalog.
|
|
37
|
+
#
|
|
38
|
+
# Therefore, the algorithm implemented here is as follows:
|
|
39
|
+
#
|
|
40
|
+
# 1. Pre-process the catalog JSON files to:
|
|
41
|
+
# - Sort the 'tags' array, since the order of tags does not matter to Puppet
|
|
42
|
+
# - Pull out additions of entire key-value pairs (above, 'another_key' => 'another_value')
|
|
43
|
+
#
|
|
44
|
+
# 2. Everything left consists of key-value pairs where the key exists in both old and new. Pass this
|
|
45
|
+
# to the 'hashdiff' gem.
|
|
46
|
+
#
|
|
47
|
+
# 3. Filter any differences to remove attributes, types, or resources that have been explicitly ignored.
|
|
48
|
+
#
|
|
49
|
+
# 4. Reformat any '+' or '-' reported by hashdiff to be changes to the keys, rather than outright
|
|
50
|
+
# additions.
|
|
51
|
+
#
|
|
52
|
+
# The heavy lifting is still handled by 'hashdiff' but we're pre-simplifying the input and post-processing
|
|
53
|
+
# the output to make it easier to deal with later.
|
|
54
|
+
class Differ
|
|
55
|
+
# This class is to distinguish handled errors from unhandled ones, for spec testing.
|
|
56
|
+
class DifferError < RuntimeError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Constructor
|
|
60
|
+
# @param catalog1_in [OctocatalogDiff::Catalog] First catalog to compare
|
|
61
|
+
# @param catalog2_in [OctocatalogDiff::Catalog] Second catalog to compare
|
|
62
|
+
def initialize(opts, catalog1_in, catalog2_in)
|
|
63
|
+
@catalog1 = catalog_resources(catalog1_in, 'First catalog')
|
|
64
|
+
@catalog2 = catalog_resources(catalog2_in, 'Second catalog')
|
|
65
|
+
@logger = opts.fetch(:logger, Logger.new(StringIO.new))
|
|
66
|
+
@diff_result = nil
|
|
67
|
+
@ignore = Set.new
|
|
68
|
+
ignore(opts.fetch(:ignore, []))
|
|
69
|
+
@opts = opts
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Difference - calculates and then returns the diff of this objects
|
|
73
|
+
# Each diff result is an array like this:
|
|
74
|
+
# [ <String> '+|-|~|!', <String> Key name, <Object> Old object, <Object> New object ]
|
|
75
|
+
# @return [Array<Diff results>] Results of the diff
|
|
76
|
+
def diff
|
|
77
|
+
@diff_result ||= catdiff
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Ignore - ignored items can be set by Type, Title, or Attribute; setting multiple in
|
|
81
|
+
# a hash is interpreted as AND. The collection of all ignored items is interpreted as OR.
|
|
82
|
+
# @param ignore [Hash<type: xxx, title: yyy, attr: zzz>] Ignore type/title/attr (can pass array also)
|
|
83
|
+
# @return [OctocatalogDiff::CatalogDiff::Differ] This object, modified
|
|
84
|
+
def ignore(ignores = [])
|
|
85
|
+
ignore_array = ignores.is_a?(Array) ? ignores : [ignores]
|
|
86
|
+
ignore_array.each do |item|
|
|
87
|
+
raise ArgumentError, "Argument #{item.inspect} to ignore is not a hash" unless item.is_a?(Hash)
|
|
88
|
+
unless item.key?(:type) || item.key?(:title) || item.key?(:attr)
|
|
89
|
+
raise ArgumentError, "Argument #{item.inspect} does not contain :type, :title, or :attr"
|
|
90
|
+
end
|
|
91
|
+
item[:type] ||= '*'
|
|
92
|
+
item[:title] ||= '*'
|
|
93
|
+
item[:attr] ||= '*'
|
|
94
|
+
|
|
95
|
+
# Support wildcards in title
|
|
96
|
+
if item[:title].is_a?(String) && item[:title] != '*' && item[:title].include?('*')
|
|
97
|
+
item[:title] = Regexp.new("\\A#{Regexp.escape(item[:title]).gsub('\*', '.*')}\\Z", 'i')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@ignore.add(item)
|
|
101
|
+
end
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Return catalog1 with filter_and_cleanups applied.
|
|
106
|
+
# This is in the public section because it's called from spec tests as well
|
|
107
|
+
# as being called internally.
|
|
108
|
+
# @return [Array<Resource Hashes>] Filtered resources in catalog
|
|
109
|
+
def catalog1
|
|
110
|
+
filter_and_cleanup(@catalog1)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Return catalog2 with filter_and_cleanups applied.
|
|
114
|
+
# This is in the public section because it's called from spec tests as well
|
|
115
|
+
# as being called internally.
|
|
116
|
+
# @return [Array<Resource Hashes>] Filtered resources in catalog
|
|
117
|
+
def catalog2
|
|
118
|
+
filter_and_cleanup(@catalog2)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Actually perform the catalog diff. This implements the 3-part algorithm described in the
|
|
124
|
+
# comment block at the top of this file.
|
|
125
|
+
def catdiff
|
|
126
|
+
@logger.debug "Entering catdiff; catalog sizes: #{@catalog1.size}, #{@catalog2.size}"
|
|
127
|
+
|
|
128
|
+
# Compute '+' and '-' from resources that exist in one catalog but not another.
|
|
129
|
+
# After this returns,
|
|
130
|
+
# result = Array<'+|-', key, value> (Additions/subtractions of entire resources)
|
|
131
|
+
# remaining1 & remaining2 = Hash<Serialized Type+Title, Value> (resources in each catalog)
|
|
132
|
+
# Note that remaining1.keys == remaining2.keys after running this
|
|
133
|
+
result, remaining1, remaining2 = preprocess_diff
|
|
134
|
+
|
|
135
|
+
# Call the hashdiff gem.
|
|
136
|
+
# After this returns,
|
|
137
|
+
# initial_hashdiff_result = Array<'~', key, oldvalue, newvalue>
|
|
138
|
+
# hashdiff_add_remove = Array<Serialized tokens with nested changes>
|
|
139
|
+
initial_hashdiff_result, hashdiff_add_remove = hashdiff_initial(remaining1, remaining2)
|
|
140
|
+
result.concat initial_hashdiff_result
|
|
141
|
+
|
|
142
|
+
# Compute '!' which is elements of arrays or hashes within the 'hashdiff' change set that
|
|
143
|
+
# have been added. See explanation in point #2 in main comment block at the top of this file.
|
|
144
|
+
hashdiff_nested_changes_result = hashdiff_nested_changes(hashdiff_add_remove, remaining1, remaining2)
|
|
145
|
+
result.concat hashdiff_nested_changes_result
|
|
146
|
+
|
|
147
|
+
# Remove resources that have been explicitly ignored
|
|
148
|
+
filter_diffs_for_ignored_items(result)
|
|
149
|
+
|
|
150
|
+
# If a file has ensure => absent, there are certain parameters that don't matter anymore. Filter
|
|
151
|
+
# out any such parameters from the result array.
|
|
152
|
+
filter_diffs_for_absent_files(result) if @opts[:suppress_absent_file_details]
|
|
153
|
+
|
|
154
|
+
# That's it!
|
|
155
|
+
@logger.debug "Exiting catdiff; change count: #{result.size}"
|
|
156
|
+
result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Filter the differences for any items that were ignored, by some combination of type, title, and
|
|
160
|
+
# attribute. This modifies the array itself by selecting only items that do not meet the ignored
|
|
161
|
+
# filter.
|
|
162
|
+
def filter_diffs_for_ignored_items(result)
|
|
163
|
+
result.reject! { |item| ignored?(item) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# If a file has ensure => absent, there are certain parameters that don't matter anymore. Filter
|
|
167
|
+
# out any such parameters from the result array.
|
|
168
|
+
# @param result [Array] Diff result list (modified by this method)
|
|
169
|
+
def filter_diffs_for_absent_files(result)
|
|
170
|
+
@logger.debug "Entering filter_diffs_for_absent_files with #{result.size} diffs"
|
|
171
|
+
|
|
172
|
+
# Scan for files in the result that are file resources with ensure => absent.
|
|
173
|
+
absent_files = Set.new
|
|
174
|
+
result.each do |diff|
|
|
175
|
+
next unless diff[0] == '~' || diff[0] == '!'
|
|
176
|
+
next unless diff[1] =~ /^File\f(.+)\fparameters\fensure$/
|
|
177
|
+
next unless ['absent', 'false', false].include?(diff[3])
|
|
178
|
+
absent_files.add Regexp.last_match(1)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# If there are any absent files, remove all diffs referencing that file, except for
|
|
182
|
+
# the change to 'ensure'.
|
|
183
|
+
if absent_files.any?
|
|
184
|
+
keep = %w(ensure backup force provider)
|
|
185
|
+
result.map! do |diff|
|
|
186
|
+
if (diff[0] == '!' || diff[0] == '~') && diff[1] =~ /^File\f(.+)\fparameters\f(.+)$/
|
|
187
|
+
if absent_files.include?(Regexp.last_match(1)) && !keep.include?(Regexp.last_match(2))
|
|
188
|
+
@logger.debug "Removing file=#{Regexp.last_match(1)} parameter=#{Regexp.last_match(2)} for absent file"
|
|
189
|
+
nil
|
|
190
|
+
else
|
|
191
|
+
diff
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
diff
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
result.compact!
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
@logger.debug "Exiting filter_diffs_for_absent_files with #{result.size} diffs"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Pre-processing of a catalog.
|
|
204
|
+
# - Remove 'before' and 'require' from parameters
|
|
205
|
+
# - Sort 'tags' array, or remove the tags array if tags are being ignored
|
|
206
|
+
# @param catalog_resources [Array<Hash>] Catalog resources
|
|
207
|
+
# @return [Array<Hash>] Array of cleaned resources
|
|
208
|
+
def filter_and_cleanup(catalog_resources)
|
|
209
|
+
result = []
|
|
210
|
+
catalog_resources.each do |resource|
|
|
211
|
+
# Exported resources are skipped (this is specifically testing that the value is
|
|
212
|
+
# equal to the boolean true, not just that the value exists or something similar)
|
|
213
|
+
next if resource['exported'] == true
|
|
214
|
+
|
|
215
|
+
# This will be the modified hash added to result
|
|
216
|
+
hsh = {}
|
|
217
|
+
hsh['type'] = resource.fetch('type', '')
|
|
218
|
+
hsh['title'] = resource.fetch('title', '')
|
|
219
|
+
|
|
220
|
+
# Special case for something like:
|
|
221
|
+
# file { 'my-own-resource-name':
|
|
222
|
+
# path => '/var/lib/puppet/my-file.txt'
|
|
223
|
+
# }
|
|
224
|
+
#
|
|
225
|
+
# The catalog-diff will treat the file above as "File\f/var/lib/puppet/my-file.txt" since the
|
|
226
|
+
# name that was given to the resource has no effect on how the file is deployed.
|
|
227
|
+
#
|
|
228
|
+
# Note that if the file was specified like this:
|
|
229
|
+
# file { '/var/lib/puppet/my-file.txt': }
|
|
230
|
+
#
|
|
231
|
+
# That also is "File\f/var/lib/puppet/my-file.txt" and that's what we want.
|
|
232
|
+
if resource.fetch('type', '') == 'File' && resource.key?('parameters') && resource['parameters'].key?('path')
|
|
233
|
+
hsh['title'] = resource['parameters']['path']
|
|
234
|
+
resource['parameters'].delete('path')
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Process each attribute in the resource
|
|
238
|
+
resource.each do |k, v|
|
|
239
|
+
# Title was pre-processed
|
|
240
|
+
next if k == 'title' || k == 'type'
|
|
241
|
+
|
|
242
|
+
# Handle parameters
|
|
243
|
+
if k == 'parameters'
|
|
244
|
+
cleansed_param = cleanse_parameters_hash(v)
|
|
245
|
+
hsh[k] = cleansed_param unless cleansed_param.nil? || cleansed_param.empty?
|
|
246
|
+
elsif k == 'tags'
|
|
247
|
+
# The order of tags is unimportant. Sort this array to avoid false diffs if order changes.
|
|
248
|
+
# Also if tags is empty, don't add. Most uses of catalog diff will want to ignore tags,
|
|
249
|
+
# and if you're ignoring tags you won't get here anyway. Also, don't add empty array of tags.
|
|
250
|
+
unless @opts[:ignore_tags]
|
|
251
|
+
hsh[k] = v.sort if v.is_a?(Array) && v.any?
|
|
252
|
+
end
|
|
253
|
+
elsif k == 'file' || k == 'line'
|
|
254
|
+
# We don't care, for the purposes of catalog-diff, from which manifest and line this resource originated.
|
|
255
|
+
# However, we may report this to the user, so we will keep it in here for now.
|
|
256
|
+
hsh[k] = v
|
|
257
|
+
else
|
|
258
|
+
# Default case: just use the existing value as-is.
|
|
259
|
+
hsh[k] = v
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
result << hsh unless hsh.empty?
|
|
264
|
+
end
|
|
265
|
+
result
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Logic to match attribute regular expressions. Called by lambda function in attr_match_rule?.
|
|
269
|
+
# @param operator [String] Either =~> (any regexp match) or =&> (all diffs must match regexp)
|
|
270
|
+
# @param regex [Regexp] Regex object
|
|
271
|
+
# @param old_val [String] Value from first catalog
|
|
272
|
+
# @param new_val [String] Value from first catalog
|
|
273
|
+
# @return [Boolean] True if condition is satisfied, false otherwise
|
|
274
|
+
def regexp_operator_match?(operator, regex, old_val, new_val)
|
|
275
|
+
# Use diffy to get only the lines that have changed in a text object.
|
|
276
|
+
# As we iterate through the diff, jump out if we have our answer: either
|
|
277
|
+
# true if '=~>' finds ANY match, or false if '=&>' fails to find a match.
|
|
278
|
+
Diffy::Diff.new(old_val, new_val, context: 0).each do |line|
|
|
279
|
+
if regex.match(line.strip)
|
|
280
|
+
return true if operator == '=~>'
|
|
281
|
+
elsif operator == '=&>'
|
|
282
|
+
return false
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# At this point, we did not return out of the loop early. This means that for
|
|
287
|
+
# '=~>' no matches were found at all, so we should return false. Or for '=&>'
|
|
288
|
+
# every diff matched, so we should return true.
|
|
289
|
+
operator == '=~>' ? false : true
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Determine whether a particular attribute matches a rule
|
|
293
|
+
# @param rule [Hash] Rule
|
|
294
|
+
# @param attrib [String] String representation of attribute
|
|
295
|
+
# @param old_val [?] Old value
|
|
296
|
+
# @param new_val [?] New value
|
|
297
|
+
# @return [Boolean] True if attribute matches rule
|
|
298
|
+
def attr_match_rule?(rule, attrib, old_val, new_val)
|
|
299
|
+
matcher = ->(_x, _y) { true }
|
|
300
|
+
rule_attr = rule[:attr].dup
|
|
301
|
+
|
|
302
|
+
# Start with '+' or '-' indicates attribute was added or removed
|
|
303
|
+
if rule_attr.start_with?('+')
|
|
304
|
+
return false unless old_val.nil?
|
|
305
|
+
rule_attr.sub!(/^\+/, '')
|
|
306
|
+
elsif rule_attr.start_with?('-')
|
|
307
|
+
return false unless new_val.nil?
|
|
308
|
+
rule_attr.sub!(/^-/, '')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Conditions that match the attribute value or regular expression
|
|
312
|
+
# Operators supported include:
|
|
313
|
+
# => String equality
|
|
314
|
+
# =+> Attribute must have been added and equal this
|
|
315
|
+
# =-> Attribute must have been removed and equal this
|
|
316
|
+
# =~> Change must match regexp (one line of change matching is sufficient)
|
|
317
|
+
# =&> Change must match regexp (all lines of change MUST match regexp)
|
|
318
|
+
if rule_attr =~ /\A(.+?)(=[\-\+~&]?>)(.+)/m
|
|
319
|
+
rule_attr = Regexp.last_match(1)
|
|
320
|
+
operator = Regexp.last_match(2)
|
|
321
|
+
value = Regexp.last_match(3)
|
|
322
|
+
if operator == '=>'
|
|
323
|
+
# String equality test
|
|
324
|
+
matcher = ->(x, y) { x == value || y == value }
|
|
325
|
+
elsif operator == '=+>'
|
|
326
|
+
# String equality test only of the new value
|
|
327
|
+
matcher = ->(_x, y) { y == value }
|
|
328
|
+
elsif operator == '=->'
|
|
329
|
+
# String equality test only of the old value
|
|
330
|
+
matcher = ->(x, _y) { x == value }
|
|
331
|
+
elsif operator == '=~>' || operator == '=&>'
|
|
332
|
+
begin
|
|
333
|
+
my_regex = Regexp.new(value, Regexp::IGNORECASE)
|
|
334
|
+
rescue RegexpError => exc
|
|
335
|
+
key = "#{rule[:type]}[#{rule[:title]}] #{rule_attr.gsub(/\f/, '::')} =~ #{value}"
|
|
336
|
+
raise RegexpError, "Invalid ignore regexp for #{key}: #{exc.message}"
|
|
337
|
+
end
|
|
338
|
+
matcher = ->(x, y) { regexp_operator_match?(operator, my_regex, x, y) }
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
if rule_attr =~ /\f/
|
|
343
|
+
beginning = rule_attr.start_with?("\f") ? '\A' : '(\A|\f)'
|
|
344
|
+
ending = '(\f|\Z)'
|
|
345
|
+
rule_attr.gsub!(/^\f+/, '')
|
|
346
|
+
hash_attr_regexp = Regexp.new(beginning + Regexp.escape(rule_attr) + ending, Regexp::IGNORECASE)
|
|
347
|
+
return attrib.match(hash_attr_regexp) && matcher.call(old_val, new_val)
|
|
348
|
+
else
|
|
349
|
+
s = attrib.downcase.split(/\f/)
|
|
350
|
+
return s.include?(rule_attr.downcase) && matcher.call(old_val, new_val)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Determine if a particular item matches a particular ignore pattern
|
|
355
|
+
# @param rule [Hash] Ignore rule
|
|
356
|
+
# @param diff_type [String] One of +, -, ~, !
|
|
357
|
+
# @param hsh [Hash] { type: title: attr: } parsed resource name
|
|
358
|
+
# @param old_val [?] Old value
|
|
359
|
+
# @param new_val [?] New value
|
|
360
|
+
# @return [Boolean] True if the item matched the rule
|
|
361
|
+
def ignore_match?(rule_in, diff_type, hsh, old_val, new_val)
|
|
362
|
+
rule = rule_in.dup
|
|
363
|
+
|
|
364
|
+
# Type matches?
|
|
365
|
+
return false unless rule[:type] == '*' || rule[:type].casecmp(hsh[:type]).zero?
|
|
366
|
+
|
|
367
|
+
# Title matches? (Support regexp and string)
|
|
368
|
+
if rule[:title].is_a?(Regexp)
|
|
369
|
+
return false unless hsh[:title].match(rule[:title])
|
|
370
|
+
elsif rule[:title] != '*'
|
|
371
|
+
return false unless rule[:title].casecmp(hsh[:title]).zero?
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Special 'attributes': Ignore specific diff types (+ add, - remove, ~ and ! change)
|
|
375
|
+
if rule[:attr] =~ /\A[\-\+~!]+\Z/
|
|
376
|
+
return ignore_match_true(hsh, rule) if rule[:attr].include?(diff_type)
|
|
377
|
+
return false
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Attribute matches?
|
|
381
|
+
return ignore_match_true(hsh, rule) if hsh[:attr].nil? && rule[:attr].nil?
|
|
382
|
+
return ignore_match_true(hsh, rule) if rule[:attr] == '*'
|
|
383
|
+
return false if hsh[:attr].nil?
|
|
384
|
+
|
|
385
|
+
# Attributes that match values
|
|
386
|
+
if rule[:attr].is_a?(Array)
|
|
387
|
+
rule[:attr].each do |attrib|
|
|
388
|
+
return false unless attr_match_rule?(rule.merge(attr: attrib), hsh[:attr], old_val, new_val)
|
|
389
|
+
end
|
|
390
|
+
else
|
|
391
|
+
return false unless attr_match_rule?(rule, hsh[:attr], old_val, new_val)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Still here? Must be true.
|
|
395
|
+
ignore_match_true(hsh, rule)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Debugging for ignore_match: This logs a debug message for an ignored diff and then returns true.
|
|
399
|
+
# @param hsh [Hash] Item that is being checked
|
|
400
|
+
# @param rule [Hash] Ignore rule
|
|
401
|
+
# @return [Boolean] Always returns true
|
|
402
|
+
def ignore_match_true(hsh, rule)
|
|
403
|
+
@logger.debug "Ignoring #{hsh.inspect}, matches #{rule.inspect}"
|
|
404
|
+
true
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Determine if a given item is ignored
|
|
408
|
+
# @param diff [Array] Diff
|
|
409
|
+
# @return [Boolean] True to ignore resource, false not to ignore
|
|
410
|
+
def ignored?(diff)
|
|
411
|
+
key = diff[1]
|
|
412
|
+
hsh = if key =~ /\A([^\f]+)\f([^\f]+)\Z/
|
|
413
|
+
{ type: Regexp.last_match(1), title: Regexp.last_match(2) }
|
|
414
|
+
else
|
|
415
|
+
s = key.split(/\f/, 3)
|
|
416
|
+
{ type: s[0], title: s[1], attr: s[2] }
|
|
417
|
+
end
|
|
418
|
+
@ignore.each do |rule|
|
|
419
|
+
return true if ignore_match?(rule, diff[0], hsh, diff[2], diff[3])
|
|
420
|
+
end
|
|
421
|
+
false
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Cleanse parameters of filtered attributes.
|
|
425
|
+
# @param parameters_hash [Hash] Hash of parameters
|
|
426
|
+
# @return [Hash] Cleaned parameters hash (original input hash is not altered)
|
|
427
|
+
def cleanse_parameters_hash(parameters_hash)
|
|
428
|
+
result = parameters_hash.dup
|
|
429
|
+
|
|
430
|
+
# 'before' and 'require' handle internal Puppet ordering but do not affect what
|
|
431
|
+
# happens on the target machine. Don't consider these for the purpose of catalog diff.
|
|
432
|
+
result.delete('before')
|
|
433
|
+
result.delete('require')
|
|
434
|
+
|
|
435
|
+
# Sort arrays for parameters where the order is unimportant
|
|
436
|
+
%w(notify subscribe tag).each { |key| result[key].sort! if result[key].is_a?(Array) }
|
|
437
|
+
|
|
438
|
+
# Return the result
|
|
439
|
+
result
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Pre-process catalog resources by looking for additions and removals. This is required to distinguish between
|
|
443
|
+
# top-level addition/removal of resources, and addition/removal of elements from arrays and hashes nested within
|
|
444
|
+
# resources (those too will be reported as +/- by hashdiff, but we want to see them as changes).
|
|
445
|
+
# @return [Array<['+|-', Key, Hash]>, Array<(catalog1 hashes)>, Array<(catalog2 hashes)>] Data
|
|
446
|
+
def preprocess_diff
|
|
447
|
+
@logger.debug "Entering preprocess_diff; catalog sizes: #{@catalog1.size}, #{@catalog2.size}"
|
|
448
|
+
|
|
449
|
+
# Do the pre-processing: filter_and_cleanup catalogs of resources that do not matter, and then run
|
|
450
|
+
# through each to tokenize the entries for initial comparison.
|
|
451
|
+
# NOTE: 'catalog1' and 'catalog2' are methods above that call filter_and_cleanup(@catalogX)
|
|
452
|
+
|
|
453
|
+
catalog1_result = resources_as_hashes_with_serialized_keys(catalog1)
|
|
454
|
+
catalog1_resources = catalog1_result[:catalog]
|
|
455
|
+
|
|
456
|
+
catalog2_result = resources_as_hashes_with_serialized_keys(catalog2)
|
|
457
|
+
catalog2_resources = catalog2_result[:catalog]
|
|
458
|
+
|
|
459
|
+
# Call out all added and removed keys, and delete these from further consideration.
|
|
460
|
+
# (That way, 'hashdiff' will only be used to compare keys existing in both old and new.)
|
|
461
|
+
result = []
|
|
462
|
+
added_keys = catalog2_resources.keys - catalog1_resources.keys
|
|
463
|
+
removed_keys = catalog1_resources.keys - catalog2_resources.keys
|
|
464
|
+
|
|
465
|
+
added_keys.each do |key|
|
|
466
|
+
key_for_map = key.split(/\f/, 3)[0..1].join("\f") # Keep first two values separated by \f
|
|
467
|
+
result << ['+', key, catalog2_resources[key], catalog2_result[:catalog_map][key_for_map]]
|
|
468
|
+
catalog2_resources.delete(key)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
removed_keys.each do |key|
|
|
472
|
+
key_for_map = key.split(/\f/, 3)[0..1].join("\f") # Keep first two values separated by \f
|
|
473
|
+
result << ['-', key, catalog1_resources[key], catalog1_result[:catalog_map][key_for_map]]
|
|
474
|
+
catalog1_resources.delete(key)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
@logger.debug "Exiting preprocess_diff; added #{added_keys.size}, removed #{removed_keys.size}"
|
|
478
|
+
[result, catalog1_result, catalog2_result]
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# This runs the remaining resources in the catalogs through hashdiff.
|
|
482
|
+
# @param catalog1_resources [<Hash<Catalog Resources, Catalog Map>] Hash of catalog1's resources, tokenized
|
|
483
|
+
# @param catalog2_resources [<Hash<Catalog Resources, Catalog Map>] Hash of catalog2's resources, tokenized
|
|
484
|
+
# @return [Array<Differences>, Array<(Token, Old, New)>] Input to next step
|
|
485
|
+
def hashdiff_initial(catalog1_in, catalog2_in)
|
|
486
|
+
catalog1_resources = catalog1_in[:catalog]
|
|
487
|
+
catalog2_resources = catalog2_in[:catalog]
|
|
488
|
+
|
|
489
|
+
@logger.debug "Entering hashdiff_initial; catalog sizes: #{catalog1_resources.size}, #{catalog2_resources.size}"
|
|
490
|
+
result = []
|
|
491
|
+
hashdiff_add_remove = Set.new
|
|
492
|
+
hashdiff_result = HashDiff.diff(catalog1_resources, catalog2_resources, delimiter: "\f")
|
|
493
|
+
hashdiff_result.each do |obj|
|
|
494
|
+
# Regular change
|
|
495
|
+
if obj[0] == '~'
|
|
496
|
+
key_for_map = obj[1].split(/\f/, 3)[0..1].join("\f") # Keep first two values separated by \f
|
|
497
|
+
obj << catalog1_in[:catalog_map][key_for_map]
|
|
498
|
+
obj << catalog2_in[:catalog_map][key_for_map]
|
|
499
|
+
result << obj
|
|
500
|
+
next
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Added/removed element to/from array
|
|
504
|
+
if obj[1] =~ /^(.+)\[\d+\]/
|
|
505
|
+
hashdiff_add_remove.add(Regexp.last_match(1))
|
|
506
|
+
next
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Added a new key that points to some kind of data structure that we know how
|
|
510
|
+
# to handle.
|
|
511
|
+
if obj[1] =~ /^(.+)\f([^\f]+)$/ && [String, Fixnum, Float, TrueClass, FalseClass, Array, Hash].include?(obj[2].class)
|
|
512
|
+
hashdiff_add_remove.add(obj[1])
|
|
513
|
+
next
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Any other weird edge cases need to be added and handled here. For now just error out.
|
|
517
|
+
# :nocov:
|
|
518
|
+
raise "Bug (please report): Unexpected data structure in hashdiff_result: #{obj.inspect}"
|
|
519
|
+
# :nocov:
|
|
520
|
+
end
|
|
521
|
+
@logger.debug "Exiting hashdiff_initial; changes: #{result.size}, nested changes: #{hashdiff_add_remove.size}"
|
|
522
|
+
[result, hashdiff_add_remove.to_a]
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# This diffs nested changes deep in the data structure. Each item in hashdiff_add_remove
|
|
526
|
+
# has been previously identified as being an addition or removal from a deeply nested element
|
|
527
|
+
# that exists in both old and new. This code compares that deeply nested element in both the
|
|
528
|
+
# old and new, and uses status '!' (rather than '+', '-', or '~') to indicate that the change
|
|
529
|
+
# occurred in a deeply nested element.
|
|
530
|
+
# @param hashdiff_add_remove [Array<Serialized Tokens>] Adds/removes from hashdiff
|
|
531
|
+
# @param remaining1 [Hash<Catalog1 Resources>] Serialized key / value pairs for catalog1 resources
|
|
532
|
+
# @param remaining2 [Hash<Catalog1 Resources>] Serialized key / value pairs for catalog2 resources
|
|
533
|
+
# @return [Array<'!', key, old, new>] Change set
|
|
534
|
+
def hashdiff_nested_changes(hashdiff_add_remove, remaining1, remaining2)
|
|
535
|
+
return [] if hashdiff_add_remove.empty?
|
|
536
|
+
|
|
537
|
+
catalog1 = remaining1[:catalog]
|
|
538
|
+
catalog2 = remaining2[:catalog]
|
|
539
|
+
catmap1 = remaining1[:catalog_map]
|
|
540
|
+
catmap2 = remaining2[:catalog_map]
|
|
541
|
+
result = []
|
|
542
|
+
|
|
543
|
+
hashdiff_add_remove.each do |key|
|
|
544
|
+
key_split = key.split(/\f/)
|
|
545
|
+
first_part_of_key = [key_split.shift, key_split.shift].join("\f")
|
|
546
|
+
key_split.unshift first_part_of_key
|
|
547
|
+
if catalog1[first_part_of_key].is_a?(Hash) && catalog2[first_part_of_key].is_a?(Hash)
|
|
548
|
+
# At this point catalog1[first_part_of_key] might look like this:
|
|
549
|
+
# {
|
|
550
|
+
# "type"=>"Class",
|
|
551
|
+
# "title"=>"Openssl::Package",
|
|
552
|
+
# "exported"=>false,
|
|
553
|
+
# "parameters"=>{"openssl_version"=>"1.0.1-4", "common-array"=>[1, 3, 5]}
|
|
554
|
+
# }
|
|
555
|
+
# and key_split looks like this:
|
|
556
|
+
# [ "Class\fOpenssl::Package", 'parameters', 'common-array' ]
|
|
557
|
+
#
|
|
558
|
+
# We have to dig out remaining1["Class\fOpenssl::Package"]['parameters']['common-array']
|
|
559
|
+
# to do the comparison.
|
|
560
|
+
obj0 = dig_out_key(catalog1, key_split.dup)
|
|
561
|
+
obj1 = dig_out_key(catalog2, key_split.dup)
|
|
562
|
+
result << ['!', key, obj0, obj1, catmap1[first_part_of_key], catmap2[first_part_of_key]]
|
|
563
|
+
else
|
|
564
|
+
# Bug condition
|
|
565
|
+
# :nocov:
|
|
566
|
+
raise "BUG (Please report): Unexpected resource: #{first_part_of_key.inspect} not a catalog resource"
|
|
567
|
+
# :nocov:
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
result
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# From an array of keys [key1, key2, key3, ...] dig out the value of hash[key1][key2][key3]...
|
|
574
|
+
# @param hash_in [Hash] Starting hash (or value passed in by recursion)
|
|
575
|
+
# @param key_array [Array<String>] Names of keys in order
|
|
576
|
+
# @return [?] Value of hash_in[key1][key2][key3]..., or nil if any keys along the way don't exist
|
|
577
|
+
def dig_out_key(hash_in, key_array)
|
|
578
|
+
return hash_in if key_array.empty?
|
|
579
|
+
return hash_in unless hash_in.is_a?(Hash)
|
|
580
|
+
return nil unless hash_in.key?(key_array[0])
|
|
581
|
+
next_key = key_array.shift
|
|
582
|
+
dig_out_key(hash_in[next_key], key_array)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# This is a helper for the constructor, verifying that the incoming catalog is an expected
|
|
586
|
+
# object.
|
|
587
|
+
# @param catalog [OctocatalogDiff::Catalog] Incoming catalog
|
|
588
|
+
# @return [Hash] Internal simplified hash object
|
|
589
|
+
def catalog_resources(catalog_in, name = 'Passed catalog')
|
|
590
|
+
return catalog_in.resources if catalog_in.is_a?(OctocatalogDiff::Catalog)
|
|
591
|
+
raise DifferError, "#{name} is not a valid catalog (input datatype: #{catalog_in.class})"
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Turn array of resources into a hash by serialized keys. For consistency with 'hashdiff'
|
|
595
|
+
# the serialized key is the resource type and all components of the title (split on '::'),
|
|
596
|
+
# joined with \f.
|
|
597
|
+
# @param catalog Array<Hash> Resource array from catalog
|
|
598
|
+
# @return [Hash] See description above
|
|
599
|
+
def resources_as_hashes_with_serialized_keys(catalog)
|
|
600
|
+
result = {
|
|
601
|
+
catalog: {},
|
|
602
|
+
catalog_map: {}
|
|
603
|
+
}
|
|
604
|
+
catalog.each do |item|
|
|
605
|
+
i = item.dup
|
|
606
|
+
result[:catalog_map]["#{item['type']}\f#{item['title']}"] = { 'file' => item['file'], 'line' => item['line'] }
|
|
607
|
+
i.delete('file')
|
|
608
|
+
i.delete('line')
|
|
609
|
+
result[:catalog]["#{item['type']}\f#{item['title']}"] = i
|
|
610
|
+
end
|
|
611
|
+
result
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|