octocatalog-diff 0.6.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/README.md +5 -2
  4. data/bin/octocatalog-diff +9 -49
  5. data/doc/CHANGELOG.md +14 -0
  6. data/doc/advanced-filter.md +59 -1
  7. data/doc/advanced-override-enc.md +54 -0
  8. data/doc/advanced-pe-enc.md +1 -1
  9. data/doc/advanced.md +2 -1
  10. data/doc/dev/api.md +5 -0
  11. data/doc/dev/api/v1.md +41 -0
  12. data/doc/dev/api/v1/calls/catalog-diff.md +209 -0
  13. data/doc/dev/api/v1/calls/catalog.md +115 -0
  14. data/doc/dev/api/v1/calls/config.md +37 -0
  15. data/doc/dev/api/v1/objects/catalog.md +127 -0
  16. data/doc/dev/api/v1/objects/diff.md +261 -0
  17. data/doc/dev/api/v1/objects/override.md +30 -0
  18. data/doc/dev/how-to-add-options.md +12 -12
  19. data/doc/optionsref.md +91 -74
  20. data/doc/versions/v1.md +22 -0
  21. data/lib/octocatalog-diff.rb +1 -8
  22. data/lib/octocatalog-diff/api/v1.rb +27 -0
  23. data/lib/octocatalog-diff/api/v1/catalog-compile.rb +40 -0
  24. data/lib/octocatalog-diff/api/v1/catalog-diff.rb +68 -0
  25. data/lib/octocatalog-diff/api/v1/catalog.rb +84 -0
  26. data/lib/octocatalog-diff/api/v1/common.rb +24 -0
  27. data/lib/octocatalog-diff/api/v1/config.rb +125 -0
  28. data/lib/octocatalog-diff/api/v1/diff.rb +194 -0
  29. data/lib/octocatalog-diff/api/v1/override.rb +103 -0
  30. data/lib/octocatalog-diff/catalog-diff/differ.rb +66 -47
  31. data/lib/octocatalog-diff/catalog-diff/display.rb +8 -2
  32. data/lib/octocatalog-diff/catalog-diff/display/json.rb +3 -2
  33. data/lib/octocatalog-diff/catalog-diff/display/legacy_json.rb +28 -0
  34. data/lib/octocatalog-diff/catalog-diff/display/text.rb +64 -9
  35. data/lib/octocatalog-diff/catalog-diff/filter.rb +45 -6
  36. data/lib/octocatalog-diff/catalog-diff/filter/absent_file.rb +65 -0
  37. data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +78 -0
  38. data/lib/octocatalog-diff/catalog-diff/filter/yaml.rb +10 -7
  39. data/lib/octocatalog-diff/catalog-util/bootstrap.rb +13 -14
  40. data/lib/octocatalog-diff/catalog-util/builddir.rb +1 -0
  41. data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +2 -2
  42. data/lib/octocatalog-diff/catalog-util/enc.rb +49 -14
  43. data/lib/octocatalog-diff/catalog-util/enc/pe.rb +3 -5
  44. data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +3 -1
  45. data/lib/octocatalog-diff/catalog-util/git.rb +36 -24
  46. data/lib/octocatalog-diff/catalog.rb +5 -9
  47. data/lib/octocatalog-diff/catalog/computed.rb +9 -1
  48. data/lib/octocatalog-diff/catalog/puppetdb.rb +4 -3
  49. data/lib/octocatalog-diff/cli.rb +195 -0
  50. data/lib/octocatalog-diff/cli/diffs.rb +40 -0
  51. data/lib/octocatalog-diff/cli/options.rb +183 -0
  52. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/basedir.rb +1 -1
  53. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_current.rb +1 -1
  54. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_environment.rb +1 -1
  55. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_script.rb +1 -1
  56. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_then_exit.rb +1 -1
  57. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrapped_dirs.rb +1 -1
  58. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/cached_master_dir.rb +1 -1
  59. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/catalog_only.rb +1 -1
  60. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/color.rb +1 -1
  61. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/command_line.rb +2 -2
  62. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/compare_file_text.rb +1 -1
  63. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/create_symlinks.rb +2 -2
  64. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/debug.rb +1 -1
  65. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/debug_bootstrap.rb +1 -1
  66. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_datatype_changes.rb +1 -1
  67. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_detail_add.rb +1 -1
  68. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_source_file_line.rb +1 -1
  69. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/enc.rb +1 -1
  70. data/lib/octocatalog-diff/cli/options/enc_override.rb +21 -0
  71. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/environment.rb +2 -2
  72. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/existing_catalogs.rb +1 -1
  73. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/fact_file.rb +1 -1
  74. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/fact_override.rb +2 -2
  75. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/facts_terminus.rb +1 -1
  76. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/filters.rb +5 -2
  77. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/from_puppetdb.rb +1 -1
  78. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/header.rb +1 -1
  79. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_config.rb +1 -1
  80. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_path.rb +1 -1
  81. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_path_strip.rb +1 -1
  82. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hostname.rb +1 -1
  83. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore.rb +1 -1
  84. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore_attr.rb +1 -1
  85. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore_tags.rb +1 -1
  86. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/include_tags.rb +1 -1
  87. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/master_cache_branch.rb +1 -1
  88. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/output_file.rb +1 -1
  89. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/output_format.rb +5 -3
  90. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/parallel.rb +1 -1
  91. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/parser.rb +1 -1
  92. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pass_env_vars.rb +1 -1
  93. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_ca.rb +1 -1
  94. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_client_cert.rb +1 -1
  95. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_client_key.rb +1 -1
  96. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_token.rb +1 -1
  97. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_token_file.rb +1 -1
  98. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_url.rb +1 -1
  99. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/preserve_environments.rb +1 -1
  100. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_binary.rb +2 -2
  101. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master.rb +2 -2
  102. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_api_version.rb +2 -2
  103. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_ca.rb +2 -2
  104. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_client_cert.rb +2 -2
  105. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_client_key.rb +2 -2
  106. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_api_version.rb +1 -1
  107. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_ca.rb +1 -1
  108. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_cert.rb +1 -1
  109. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_key.rb +1 -1
  110. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_password.rb +1 -1
  111. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_password_file.rb +1 -1
  112. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_url.rb +1 -1
  113. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/quiet.rb +1 -1
  114. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/retry_failed_catalog.rb +1 -1
  115. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/safe_to_delete_cached_master_dir.rb +1 -1
  116. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/storeconfigs.rb +1 -1
  117. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/suppress_absent_file_details.rb +2 -1
  118. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/to_from_branch.rb +1 -1
  119. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/validate_references.rb +1 -1
  120. data/lib/octocatalog-diff/cli/printer.rb +52 -0
  121. data/lib/octocatalog-diff/errors.rb +33 -0
  122. data/lib/octocatalog-diff/facts.rb +1 -4
  123. data/lib/octocatalog-diff/facts/puppetdb.rb +8 -7
  124. data/lib/octocatalog-diff/puppetdb.rb +5 -9
  125. data/lib/octocatalog-diff/util/catalogs.rb +242 -0
  126. metadata +97 -75
  127. data/lib/octocatalog-diff/catalog-diff/cli.rb +0 -211
  128. data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +0 -246
  129. data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +0 -147
  130. data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +0 -100
  131. data/lib/octocatalog-diff/catalog-diff/cli/options.rb +0 -185
  132. data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +0 -54
@@ -3,7 +3,7 @@
3
3
  # Set storeconfigs (integration with PuppetDB for collected resources)
4
4
  # @param parser [OptionParser object] The OptionParser argument
5
5
  # @param options [Hash] Options hash being constructed; this is modified in this method.
6
- OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:storeconfigs) do
6
+ OctocatalogDiff::Cli::Options::Option.newoption(:storeconfigs) do
7
7
  has_weight 220
8
8
 
9
9
  def parse(parser, options)
@@ -3,9 +3,10 @@
3
3
  # If enabled, this option will suppress changes to certain attributes of a file, if the
4
4
  # file is specified to be 'absent' in the target catalog. Suppressed changes in this case
5
5
  # include user, group, mode, and content, because a removed file has none of those.
6
+ # <i>This option is DEPRECATED; please use <code>--filters AbsentFile</code> instead.</i>
6
7
  # @param parser [OptionParser object] The OptionParser argument
7
8
  # @param options [Hash] Options hash being constructed; this is modified in this method.
8
- OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:suppress_absent_file_details) do
9
+ OctocatalogDiff::Cli::Options::Option.newoption(:suppress_absent_file_details) do
9
10
  has_weight 600
10
11
 
11
12
  def parse(parser, options)
@@ -4,7 +4,7 @@
4
4
  # the current contents of the base code directory without any git checkouts.
5
5
  # @param parser [OptionParser object] The OptionParser argument
6
6
  # @param options [Hash] Options hash being constructed; this is modified in this method.
7
- OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:to_from_branch) do
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:to_from_branch) do
8
8
  has_weight 20
9
9
 
10
10
  def parse(parser, options)
@@ -5,7 +5,7 @@
5
5
  # parameters are to be checked.
6
6
  # @param parser [OptionParser object] The OptionParser argument
7
7
  # @param options [Hash] Options hash being constructed; this is modified in this method.
8
- OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:validate_references) do
8
+ OctocatalogDiff::Cli::Options::Option.newoption(:validate_references) do
9
9
  has_weight 205
10
10
 
11
11
  def parse(parser, options)
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../catalog-diff/display'
4
+ require_relative '../errors'
5
+
6
+ module OctocatalogDiff
7
+ class Cli
8
+ # Wrapper around OctocatalogDiff::CatalogDiff::Display to set the options and
9
+ # output to a file or the screen depending on selection.
10
+ class Printer
11
+ # Constructor
12
+ # @param options [Hash] Options from cli/options
13
+ # @param logger [Logger] Logger object
14
+ def initialize(options, logger)
15
+ @options = options
16
+ @logger = logger
17
+ end
18
+
19
+ # The method to call externally, passing in diffs. This takes the appropriate action
20
+ # based on options, which is either to write the result into an output file, or print
21
+ # the result on STDOUT. Does not return anything.
22
+ # @param diffs [Array<Diffs>] Array of differences
23
+ # @param from_dir [String] Directory in which "from" catalog was compiled
24
+ # @param to_dir [String] Directory in which "to" catalog was compiled
25
+ def printer(diffs, from_dir = nil, to_dir = nil)
26
+ unless diffs.is_a?(Array)
27
+ raise ArgumentError, "printer() expects an array, not #{diffs.class}"
28
+ end
29
+ display_opts = @options.merge(compilation_from_dir: from_dir, compilation_to_dir: to_dir)
30
+ diff_text = OctocatalogDiff::CatalogDiff::Display.output(diffs, display_opts, @logger)
31
+ if @options[:output_file].nil?
32
+ puts diff_text unless diff_text.empty?
33
+ else
34
+ output_to_file(diff_text)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Output to a file, handling errors related to writing files.
41
+ # @param diff_in [String|Array] Text to write to file
42
+ def output_to_file(diff_in)
43
+ diff_text = diff_in.is_a?(Array) ? diff_in.join("\n") : diff_in
44
+ File.open(@options[:output_file], 'w') { |f| f.write(diff_text) }
45
+ @logger.info "Wrote diff to #{@options[:output_file]}"
46
+ rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR => exc
47
+ @logger.error "Cannot write to #{@options[:output_file]}: #{exc}"
48
+ raise OctocatalogDiff::Errors::PrinterError, "Cannot write to #{@options[:output_file]}: #{exc}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OctocatalogDiff
4
+ # Contains error classes raised by this gem
5
+ class Errors
6
+ # Error classes for handled configuration file errors
7
+ class ConfigurationFileNotFoundError < RuntimeError; end
8
+ class ConfigurationFileContentError < RuntimeError; end
9
+
10
+ # Error classes for building catalogs
11
+ class BootstrapError < RuntimeError; end
12
+ class CatalogError < RuntimeError; end
13
+ class PuppetVersionError < RuntimeError; end
14
+ class ReferenceValidationError < RuntimeError; end
15
+ class GitCheckoutError < RuntimeError; end
16
+
17
+ # Error classes for retrieving facts
18
+ class FactSourceError < RuntimeError; end
19
+ class FactRetrievalError < RuntimeError; end
20
+
21
+ # Errors for PuppetDB
22
+ class PuppetDBNodeNotFoundError < RuntimeError; end
23
+ class PuppetDBGenericError < RuntimeError; end
24
+ class PuppetDBConnectionError < RuntimeError; end
25
+
26
+ # Errors for Puppet Enterprise
27
+ class PEClassificationError < RuntimeError; end
28
+
29
+ # Miscellanous catalog-diff errors
30
+ class DifferError < RuntimeError; end
31
+ class PrinterError < RuntimeError; end
32
+ end
33
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'errors'
3
4
  require_relative 'facts/json'
4
5
  require_relative 'facts/yaml'
5
6
  require_relative 'facts/puppetdb'
@@ -119,9 +120,5 @@ module OctocatalogDiff
119
120
  @facts['values'][key] = value
120
121
  end
121
122
  end
122
-
123
- # Separate classes to handle errors we throw explicitly
124
- class FactSourceError < RuntimeError; end
125
- class FactRetrievalError < RuntimeError; end
126
123
  end
127
124
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../errors'
3
4
  require_relative '../facts'
4
5
  require_relative '../puppetdb'
5
6
  require 'yaml'
@@ -42,21 +43,21 @@ module OctocatalogDiff
42
43
  result.map { |x| facts[x['name']] = x['value'] }
43
44
  if facts.empty?
44
45
  message = "Unable to retrieve facts for node #{node} from PuppetDB (empty or nil)!"
45
- raise OctocatalogDiff::Facts::FactRetrievalError, message
46
+ raise OctocatalogDiff::Errors::FactRetrievalError, message
46
47
  end
47
48
 
48
49
  # Create a structure compatible with YAML fact files.
49
50
  obj_to_return = { 'name' => node, 'values' => {} }
50
51
  facts.each { |k, v| obj_to_return['values'][k.sub(/^::/, '')] = v }
51
52
  break # Not return, to avoid LocalJumpError in Ruby 2.2
52
- rescue OctocatalogDiff::PuppetDB::ConnectionError => exc
53
- exception_class = OctocatalogDiff::Facts::FactSourceError
53
+ rescue OctocatalogDiff::Errors::PuppetDBConnectionError => exc
54
+ exception_class = OctocatalogDiff::Errors::FactSourceError
54
55
  exception_message = "Fact retrieval failed (#{exc.class}) (#{exc.message})"
55
- rescue OctocatalogDiff::PuppetDB::NotFoundError => exc
56
- exception_class = OctocatalogDiff::Facts::FactRetrievalError
56
+ rescue OctocatalogDiff::Errors::PuppetDBNodeNotFoundError => exc
57
+ exception_class = OctocatalogDiff::Errors::FactRetrievalError
57
58
  exception_message = "Node #{node} not found in PuppetDB (#{exc.message})"
58
- rescue OctocatalogDiff::PuppetDB::PuppetDBError => exc
59
- exception_class = OctocatalogDiff::Facts::FactRetrievalError
59
+ rescue OctocatalogDiff::Errors::PuppetDBGenericError => exc
60
+ exception_class = OctocatalogDiff::Errors::FactRetrievalError
60
61
  exception_message = "Fact retrieval failed for node #{node} from PuppetDB (#{exc.message})"
61
62
  end
62
63
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'errors'
3
4
  require_relative 'util/httparty'
4
5
 
5
6
  require 'uri'
@@ -14,11 +15,6 @@ URI::HTTPS.const_set(:DEFAULT_PORT, 8081)
14
15
  module OctocatalogDiff
15
16
  # A standard way to connect to PuppetDB from the various scripts in this repository.
16
17
  class PuppetDB
17
- # This class raises errors for certain handled problems
18
- class NotFoundError < RuntimeError; end
19
- class PuppetDBError < RuntimeError; end
20
- class ConnectionError < RuntimeError; end
21
-
22
18
  # Allow connections to be read (used in tests for now)
23
19
  attr_reader :connections
24
20
 
@@ -87,7 +83,7 @@ module OctocatalogDiff
87
83
  def get(path)
88
84
  _get(path)
89
85
  rescue Net::OpenTimeout, Errno::ECONNREFUSED => exc
90
- raise ConnectionError, "#{exc.class} connecting to PuppetDB (need VPN on?): #{exc.message}"
86
+ raise OctocatalogDiff::Errors::PuppetDBConnectionError, "#{exc.class} connecting to PuppetDB (need VPN on?): #{exc.message}"
91
87
  end
92
88
 
93
89
  private
@@ -120,8 +116,8 @@ module OctocatalogDiff
120
116
 
121
117
  # Handle all non-200's from PuppetDB
122
118
  unless response[:code] == 200
123
- raise NotFoundError, "404 - #{response[:error]}" if response[:code] == 404
124
- raise PuppetDBError, "#{response[:code]} - #{response[:error]}"
119
+ raise OctocatalogDiff::Errors::PuppetDBNodeNotFoundError, "404 - #{response[:error]}" if response[:code] == 404
120
+ raise OctocatalogDiff::Errors::PuppetDBGenericError, "#{response[:code]} - #{response[:error]}"
125
121
  end
126
122
 
127
123
  # PuppetDB can return 'Not Found' as a string with a 200 response code
@@ -129,7 +125,7 @@ module OctocatalogDiff
129
125
 
130
126
  # PuppetDB can also return an error message in a 200; we'll call this a 500
131
127
  if response.key?(:error)
132
- raise PuppetDBError, "500 - #{response[:error]}"
128
+ raise OctocatalogDiff::Errors::PuppetDBGenericError, "500 - #{response[:error]}"
133
129
  end
134
130
 
135
131
  # If we get here without raising an error, it will fall out of the begin/rescue
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'yaml'
6
+ require_relative '../catalog'
7
+ require_relative '../errors'
8
+ require_relative 'parallel'
9
+
10
+ module OctocatalogDiff
11
+ module Util
12
+ # Helper class to construct catalogs, performing all necessary steps such as
13
+ # bootstrapping directories, installing facts, and running puppet.
14
+ class Catalogs
15
+ # Constructor
16
+ # @param options [Hash] Options
17
+ # @param logger [Logger] Logger object
18
+ def initialize(options, logger)
19
+ @options = options
20
+ @logger = logger
21
+ @catalogs = nil
22
+ raise '@logger must not be nil' if @logger.nil?
23
+ end
24
+
25
+ # Compile catalogs. This handles building both the old and new catalog (in parallel) and returns
26
+ # only when both catalogs have been built.
27
+ # @return [Hash] { :from => [OctocatalogDiff::Catalog], :to => [OctocatalogDiff::Catalog] }
28
+ def catalogs
29
+ @catalogs ||= build_catalog_parallelizer
30
+ end
31
+
32
+ # Handles the "bootstrap then exit" option, which bootstraps directories but
33
+ # exits without compiling catalogs.
34
+ def bootstrap_then_exit
35
+ @logger.debug('Begin bootstrap_then_exit')
36
+ OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory_parallelizer(@options, @logger)
37
+ @logger.debug('Success bootstrap_then_exit')
38
+ @logger.info('Successfully completed --bootstrap-then-exit action')
39
+ end
40
+
41
+ private
42
+
43
+ # Parallelizes bootstrapping of directories and building catalogs.
44
+ # @return [Hash] { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
45
+ def build_catalog_parallelizer
46
+ # Construct parallel tasks. The array supplied to OctocatalogDiff::Util::Parallel is the task portion
47
+ # of each of the tuples in catalog_tasks.
48
+ catalog_tasks = build_catalog_tasks
49
+
50
+ # Update any tasks for catalogs that do not need to be compiled. This is the case when --catalog-only
51
+ # is specified and only one catalog is to be built. This will change matching catalog tasks to the 'noop' type.
52
+ catalog_tasks.map! do |x|
53
+ if @options["#{x[0]}_catalog".to_sym] == '-'
54
+ x[1].args[:backend] = :noop
55
+ elsif @options["#{x[0]}_catalog".to_sym].is_a?(String)
56
+ x[1].args[:json] = File.read(@options["#{x[0]}_catalog".to_sym])
57
+ x[1].args[:backend] = :json
58
+ end
59
+ x
60
+ end
61
+
62
+ # Initialize the objects for each parallel task. Initializing the object is very fast and does not actually
63
+ # build the catalog.
64
+ result = {}
65
+ catalog_tasks.each do |x|
66
+ result[x[0]] = OctocatalogDiff::Catalog.new(x[1].args)
67
+ @logger.debug "Initialized #{result[x[0]].builder} for #{x[0]}-catalog"
68
+ end
69
+
70
+ # Disable --compare-file-text if either (or both) of the chosen backends do not support it
71
+ if @options.fetch(:compare_file_text, false)
72
+ result.each do |_key, val|
73
+ next unless val.convert_file_resources == false
74
+ @logger.debug "Disabling --compare-file-text; not supported by #{val.builder}"
75
+ @options[:compare_file_text] = false
76
+ catalog_tasks.map! do |x|
77
+ x[1].args[:compare_file_text] = false
78
+ x
79
+ end
80
+ break
81
+ end
82
+ end
83
+
84
+ # Inject the starting object into the catalog tasks
85
+ catalog_tasks.map! do |x|
86
+ x[1].args[:object] = result[x[0]]
87
+ x
88
+ end
89
+
90
+ # Execute the parallelized catalog builds
91
+ passed_catalog_tasks = catalog_tasks.map { |x| x[1] }
92
+ parallel_catalogs = OctocatalogDiff::Util::Parallel.run_tasks(passed_catalog_tasks, @logger, @options[:parallel])
93
+
94
+ # If the catalogs array is empty at this point, there is an unexpected size mismatch. This should
95
+ # never happen, but test for it anyway.
96
+ unless parallel_catalogs.size == catalog_tasks.size
97
+ # :nocov:
98
+ raise "BUG: mismatch catalog_result (#{parallel_catalogs.size} vs #{catalog_tasks.size})"
99
+ # :nocov:
100
+ end
101
+
102
+ # Construct result hash. Will eventually be in the format
103
+ # { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
104
+
105
+ # Analyze the results from parallel run.
106
+ catalog_tasks.each do |x|
107
+ # The `parallel_catalog_obj` is a OctocatalogDiff::Util::Parallel::Result. Get the first element from
108
+ # the parallel_catalogs output.
109
+ parallel_catalog_obj = parallel_catalogs.shift
110
+
111
+ # Add the result to the 'result' hash
112
+ add_parallel_result(result, parallel_catalog_obj, x)
113
+ end
114
+
115
+ # Things have succeeded if the :to and :from catalogs exist at this point. If not, things have
116
+ # failed, and an exception should be thrown.
117
+ return result if result.key?(:to) && result.key?(:from)
118
+
119
+ # This is believed to be a bug condition.
120
+ # :nocov:
121
+ raise OctocatalogDiff::Errors::CatalogError, 'One or more catalogs failed to compile.'
122
+ # :nocov:
123
+ end
124
+
125
+ # Get catalog compilation tasks.
126
+ # @return [Array<[key, task]>] Catalog tasks
127
+ def build_catalog_tasks
128
+ [:from, :to].map do |key|
129
+ # These are arguments to OctocatalogDiff::Util::Parallel::Task. In most cases the arguments
130
+ # of OctocatalogDiff::Util::Parallel::Task are taken directly from options, but there are
131
+ # some defaults or otherwise-named options that must be set here.
132
+ args = @options.merge(
133
+ tag: key.to_s,
134
+ branch: @options["#{key}_env".to_sym] || '-',
135
+ bootstrapped_dir: @options["bootstrapped_#{key}_dir".to_sym],
136
+ basedir: @options[:basedir],
137
+ compare_file_text: @options.fetch(:compare_file_text, true),
138
+ retry_failed_catalog: @options.fetch(:retry_failed_catalog, 0),
139
+ parser: @options["parser_#{key}".to_sym]
140
+ )
141
+
142
+ # If any options are in the form of 'to_SOMETHING' or 'from_SOMETHING', this sets the option to
143
+ # 'SOMETHING' for the catalog if it matches this key. For example, when compiling the 'to' catalog
144
+ # when an option of :to_some_arg => 'foo', this sets :some_arg => foo, and deletes :to_some_arg and
145
+ # :from_some_arg.
146
+ @options.keys.select { |x| x.to_s =~ /^(to|from)_/ }.each do |opt_key|
147
+ args[opt_key.to_s.sub(/^(to|from)_/, '').to_sym] = @options[opt_key] if opt_key.to_s.start_with?(key.to_s)
148
+ args.delete(opt_key)
149
+ end
150
+
151
+ # The task is a OctocatalogDiff::Util::Parallel::Task object that contains the method to execute,
152
+ # validator method, text description, and arguments to provide when calling the method.
153
+ task = OctocatalogDiff::Util::Parallel::Task.new(
154
+ method: method(:build_catalog),
155
+ validator: method(:catalog_validator),
156
+ validator_args: { task: key },
157
+ description: "build_catalog for #{@options["#{key}_env".to_sym]}",
158
+ args: args
159
+ )
160
+
161
+ # The format of `catalog_tasks` will be a tuple, where the first element is the key
162
+ # (e.g. :to or :from) and the second element is the OctocatalogDiff::Util::Parallel::Task object.
163
+ [key, task]
164
+ end.compact
165
+ end
166
+
167
+ # Given a result from the 'parallel' run and a corresponding (key,task) tuple, add valid
168
+ # catalogs to the 'result' hash and throw errors for invalid catalogs.
169
+ # @param result [Hash] Result hash for build_catalog_parallelizer (may be modified)
170
+ # @param parallel_catalog_obj [OctocatalogDiff::Util::Parallel::Result] Parallel catalog result
171
+ # @param key_task_tuple [Array<key, task>] Key, task tuple
172
+ def add_parallel_result(result, parallel_catalog_obj, key_task_tuple)
173
+ # Expand the tuple into variables
174
+ key, task = key_task_tuple
175
+
176
+ # For reporting purposes, get the branch name.
177
+ branch = task.args[:branch]
178
+
179
+ # Check the result of the parallel run on this object.
180
+ if parallel_catalog_obj.status.nil?
181
+ # The compile was killed because another task failed.
182
+ @logger.warn "Catalog compile for #{branch} was aborted due to another failure"
183
+
184
+ elsif parallel_catalog_obj.output.is_a?(OctocatalogDiff::Catalog)
185
+ # The result is a catalog, but we do not know if it was successfully compiled
186
+ # until we test the validity.
187
+ catalog = parallel_catalog_obj.output
188
+ if catalog.valid?
189
+ # The catalog was successfully compiled.
190
+ result[key] = parallel_catalog_obj.output
191
+ else
192
+ # The catalog failed, but a catalog object was returned so that better error reporting
193
+ # can take place. In this error reporting, we will replace 'Error:' with '[Puppet Error]'
194
+ # and remove the compilation directory (which is a tmpdir) to reveal only the relative
195
+ # path to the files involved.
196
+ dir = catalog.compilation_dir || ''
197
+ dir_regex = Regexp.new(Regexp.escape(dir) + '/environments/[^/]+/')
198
+ error_display = catalog.error_message.split("\n").map do |line|
199
+ line.sub(/^Error:/, '[Puppet Error]').gsub(dir_regex, '')
200
+ end.join("\n")
201
+ message = "Catalog for #{branch} failed to compile due to errors:\n#{error_display}"
202
+ raise OctocatalogDiff::Errors::CatalogError, message
203
+ end
204
+ else
205
+ # Something unhandled went wrong, and an exception was thrown. Reveal a generic message.
206
+ msg = parallel_catalog_obj.exception.message
207
+ message = "Catalog for '#{key}' (#{branch}) failed to compile with #{parallel_catalog_obj.exception.class}: #{msg}"
208
+ message += "\n" + parallel_catalog_obj.exception.backtrace.map { |x| " #{x}" }.join("\n") if @options[:debug]
209
+ raise OctocatalogDiff::Errors::CatalogError, message
210
+ end
211
+ end
212
+
213
+ # Performs the steps necessary to build a catalog.
214
+ # @param opts [Hash] Options hash
215
+ # @return [Hash] { :rc => exit code, :catalog => Catalog as JSON string }
216
+ def build_catalog(opts, logger = @logger)
217
+ logger.debug("Setting up Puppet catalog build for #{opts[:branch]}")
218
+ catalog = opts[:object]
219
+ logger.debug("Catalog for #{opts[:branch]} will be built with #{catalog.builder}")
220
+ time_start = Time.now
221
+ catalog.build(logger)
222
+ time_it_took = Time.now - time_start
223
+ retries_str = " retries = #{catalog.retries}" if catalog.retries.is_a?(Fixnum)
224
+ time_str = "in #{time_it_took} seconds#{retries_str}"
225
+ status_str = catalog.valid? ? 'successfully built' : 'failed'
226
+ logger.debug "Catalog for #{opts[:branch]} #{status_str} with #{catalog.builder} #{time_str}"
227
+ catalog
228
+ end
229
+
230
+ # Validate a catalog in the parallel execution
231
+ # @param catalog [OctocatalogDiff::Catalog] Catalog object
232
+ # @param logger [Logger] Logger object (presently unused)
233
+ # @param args [Hash] Additional arguments set specifically for validator
234
+ # @return [Boolean] true if catalog is valid, false otherwise
235
+ def catalog_validator(catalog = nil, _logger = @logger, args = {})
236
+ return false unless catalog.is_a?(OctocatalogDiff::Catalog)
237
+ catalog.validate_references if args[:task] == :to
238
+ catalog.valid?
239
+ end
240
+ end
241
+ end
242
+ end