octocatalog-diff 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/.version +1 -0
  3. data/LICENSE +20 -0
  4. data/README.md +82 -0
  5. data/bin/octocatalog-diff +75 -0
  6. data/doc/advanced-bootstrap.md +33 -0
  7. data/doc/advanced-cache-dir.md +24 -0
  8. data/doc/advanced-catalog-only.md +37 -0
  9. data/doc/advanced-ci.md +13 -0
  10. data/doc/advanced-dynamic-ignores.md +123 -0
  11. data/doc/advanced-future-parser.md +11 -0
  12. data/doc/advanced-ignores.md +224 -0
  13. data/doc/advanced-output-formats.md +96 -0
  14. data/doc/advanced-output-hacks.md +45 -0
  15. data/doc/advanced-override-facts.md +67 -0
  16. data/doc/advanced-pe-enc.md +52 -0
  17. data/doc/advanced-puppet-master.md +50 -0
  18. data/doc/advanced-puppet-versions.md +9 -0
  19. data/doc/advanced-storeconfigs.md +72 -0
  20. data/doc/advanced-using-without-git.md +15 -0
  21. data/doc/advanced.md +43 -0
  22. data/doc/basic.md +70 -0
  23. data/doc/configuration-enc.md +69 -0
  24. data/doc/configuration-hiera.md +103 -0
  25. data/doc/configuration-puppetdb.md +49 -0
  26. data/doc/configuration.md +51 -0
  27. data/doc/dev/README.md +1 -0
  28. data/doc/dev/coverage.md +34 -0
  29. data/doc/dev/how-to-add-options.md +83 -0
  30. data/doc/dev/integration-tests.md +63 -0
  31. data/doc/dev/releasing.md +19 -0
  32. data/doc/installation.md +49 -0
  33. data/doc/limitations.md +34 -0
  34. data/doc/optionsref.md +947 -0
  35. data/doc/requirements.md +16 -0
  36. data/doc/roadmap.md +26 -0
  37. data/doc/similar.md +17 -0
  38. data/doc/troubleshooting.md +54 -0
  39. data/lib/octocatalog-diff.rb +12 -0
  40. data/lib/octocatalog-diff/bootstrap.rb +53 -0
  41. data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
  42. data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
  43. data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
  44. data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
  45. data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
  46. data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
  47. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
  48. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
  49. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
  50. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
  51. data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
  52. data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
  53. data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
  54. data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
  55. data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
  56. data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
  57. data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
  58. data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
  59. data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
  60. data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
  61. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
  62. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
  63. data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
  64. data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
  65. data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
  66. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
  67. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
  68. data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
  69. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
  70. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
  71. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
  72. data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
  73. data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
  74. data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
  75. data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
  76. data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
  77. data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
  78. data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
  79. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
  80. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
  81. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
  82. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
  83. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
  84. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
  85. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
  86. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
  87. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
  88. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
  89. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
  90. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
  91. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
  92. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
  93. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
  94. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
  95. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
  96. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
  97. data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
  98. data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
  99. data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
  100. data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
  101. data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
  102. data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
  103. data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
  104. data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
  105. data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
  106. data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
  107. data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
  108. data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
  109. data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
  110. data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
  111. data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
  112. data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
  113. data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
  114. data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
  115. data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
  116. data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
  117. data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
  118. data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
  119. data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
  120. data/lib/octocatalog-diff/catalog.rb +209 -0
  121. data/lib/octocatalog-diff/catalog/computed.rb +205 -0
  122. data/lib/octocatalog-diff/catalog/json.rb +30 -0
  123. data/lib/octocatalog-diff/catalog/noop.rb +19 -0
  124. data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
  125. data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
  126. data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
  127. data/lib/octocatalog-diff/external/pson/README.md +20 -0
  128. data/lib/octocatalog-diff/external/pson/common.rb +370 -0
  129. data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
  130. data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
  131. data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
  132. data/lib/octocatalog-diff/external/pson/version.rb +8 -0
  133. data/lib/octocatalog-diff/facts.rb +125 -0
  134. data/lib/octocatalog-diff/facts/json.rb +20 -0
  135. data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
  136. data/lib/octocatalog-diff/facts/yaml.rb +29 -0
  137. data/lib/octocatalog-diff/puppetdb.rb +163 -0
  138. data/lib/octocatalog-diff/util/colored.rb +20 -0
  139. data/lib/octocatalog-diff/util/httparty.rb +158 -0
  140. data/lib/octocatalog-diff/util/parallel.rb +170 -0
  141. data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
  142. data/lib/octocatalog-diff/version.rb +7 -0
  143. metadata +386 -0
@@ -0,0 +1,16 @@
1
+ # Requirements
2
+
3
+ To run `octocatalog-diff` you will need these basics:
4
+
5
+ - Ruby 2.0 or higher
6
+ - Mac OS, Linux, or other Unix-line operating system (Windows is not supported)
7
+ - 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
9
+
10
+ We recommend that you also have the following to get the most out of `octocatalog-diff`, but these are not absolute requirements:
11
+
12
+ - If your Puppet code stored in a git repository, `octocatalog-diff` can check out branches for you as it does its comparisons. Your git repository can be stored on [GitHub.com](https://github.com/), [GitHub Enterprise](https://enterprise.github.com/home), or similar. If your Puppet code is not stored in a git repository, you can still point the tool at "from" and "to" directories, but you'll have to check them out yourself.
13
+
14
+ - If you have API access (HTTPS) to PuppetDB, `octocatalog-diff` can retrieve facts automatically and also support [exported resources](https://docs.puppet.com/puppet/latest/reference/lang_exported.html) if you use them. If you are not using PuppetDB or don't have access, the tool can still read facts from YAML files.
15
+
16
+ - If your site uses an [external node classifier](https://docs.puppet.com/guides/external_nodes.html), `octocatalog-diff` can execute the ENC script as part of its catalog compiles. Depending on how your ENC is designed, this may require network access or credentials to some service. If you are not using an ENC, that's fine. If you have an ENC but don't have the requisite access, depending on your setup the tool could produce unexpected results.
@@ -0,0 +1,26 @@
1
+ # Roadmap
2
+
3
+ This document outlines our philosophy and goals for the continued development of `octocatalog-diff`.
4
+
5
+ ## Goals
6
+
7
+ - Work on a system without a full Puppet installation
8
+ - Cause no added load on production Puppet masters (unless you specifically choose to do so with non-default options)
9
+ - Offer a command line tool to help make developers more efficient
10
+ - Offer the ability to in a Continuous Integration (CI) environment
11
+ - Be compatible with Puppet 3.8.7, 4.5, and later versions
12
+ - Provide flexibility to build and compare catalogs even in esoteric Puppet codebases
13
+
14
+ ## Areas for future development
15
+
16
+ We are considering these areas for possible future development:
17
+
18
+ - Improved display of diffs, perhaps a web interface
19
+ - Additional CI use cases
20
+ - CI output display to summarize a change and a list of affected hosts, rather than listing all changes host-by-host
21
+
22
+ ## Antipatterns
23
+
24
+ These are ideas we've evaluated and decided not to pursue. (If you are considering a [contribution](/.github/CONTRIBUTING.md) along these lines, please [open an issue](https://github.com/github/octocatalog-diff/issues/new) before start working on it. We would feel badly if you did a bunch of work that we could not accept.)
25
+
26
+ - Making this into a Puppet module with a "face" so that it can be run with `puppet octocatalog-diff ...` or similar. (We have specifically designed this tool to run without a full Puppet installation. There are [similar projects](/doc/similar.md) that are distributed as Puppet modules.)
@@ -0,0 +1,17 @@
1
+ # Similar Projects
2
+
3
+ We are aware of the following projects that do similar things to octocatalog-diff:
4
+
5
+ - [Puppet's catalog_preview Puppet module](https://forge.puppet.com/puppetlabs/catalog_preview)
6
+
7
+ Installs as a module into your Puppet codebase and helps with migration from older Puppet versions to newer ones, or from open source Puppet to Puppet Enterprise. Also provides the ability to compare environments (branches). Requires a full working Puppet installation.
8
+
9
+ - [Zack Smith's catalog_diff Puppet module](https://forge.puppet.com/zack/catalog_diff)
10
+
11
+ Installs as a module into your Puppet codebase, allowing you to diff catalogs created by different versions of Puppet. Requires a full working Puppet installation.
12
+
13
+ - [camptocamp's puppet-catalog-diff-viewer](https://github.com/camptocamp/puppet-catalog-diff-viewer)
14
+
15
+ A viewer for JSON reports produced by the catalog_diff Puppet module.
16
+
17
+ `octocatalog-diff` differs from the above projects by running on a system without a fully configured Puppet installation (such as a developer workstation or CI server). This approach allows developers to run it without having access to the production Puppet servers, and it does not put any load on production Puppet masters when it compiles and compares catalogs.
@@ -0,0 +1,54 @@
1
+ # Troubleshooting
2
+
3
+ Things not quite working as expected? This section will contain hints to help you get up and running.
4
+
5
+ ### Make sure the tests pass
6
+
7
+ If you are getting errors from ruby, we'd really like to know if the tests are passing on your platform. Please follow the [installation instructions](/doc/installation.md#installing-from-source) to install octocatalog-diff from source, if you have not already done so. Once the repository is checked out, change into the directory run `rake` to perform the tests.
8
+
9
+ If you get test failures from a clean checkout of the master branch, please [open an issue](https://github.com/github/octocatalog-diff/issues/new) to let us know.
10
+
11
+ ### Make sure your configuration file is found and error-free
12
+
13
+ Run the following command to test for the existence and integrity of your configuration file.
14
+
15
+ ```
16
+ octocatalog-diff --config-test
17
+ ```
18
+
19
+ If you get an error indicating that the file can't be found, or you get errors arising from the content of the file, please review the [configuration instructions](/doc/configuration.md) to make sure you've set things up correctly.
20
+
21
+ ### Run the command in debug mode
22
+
23
+ Supplying `-d` on the command line, in addition to the node name and any other arguments, will provide a substantial amount of debugging information to the terminal window. If you ultimately end up requesting our help, we will need this debugging output.
24
+
25
+ Example:
26
+
27
+ ```
28
+ octocatalog-diff -d -n SomeNodeName.yourdomain.com
29
+ ```
30
+
31
+ ### Run only certain components of the command
32
+
33
+ To perform the bootstrapping and catalog compilation in separate steps, you can run octocatalog-diff with arguments asking it to do only one or the other. This will help you narrow down whether the problem is in the bootstrapping (first command) or catalog compilation (second command).
34
+
35
+ Be sure you are in the directory where your Puppet code is checked out when you run these commands.
36
+
37
+ To run just the bootstrapping code (do this within a checkout of your Puppet repository):
38
+
39
+ ```
40
+ mkdir /tmp/octo-test
41
+ octocatalog-diff -d --bootstrap-then-exit --bootstrapped-from-dir=/tmp/octo-test
42
+ ```
43
+
44
+ To run just the catalog compilation code (do this within a checkout of your Puppet repository):
45
+
46
+ ```
47
+ octocatalog-diff -d -n SomeNodeName.yourdomain.com -o /tmp/catalog.json --bootstrapped-to-dir=$PWD --catalog-only
48
+ ```
49
+
50
+ ### Contact us
51
+
52
+ Still having trouble? Please [open an issue](https://github.com/github/octocatalog-diff/issues/new) and we will do our best to help.
53
+
54
+ Please follow the provided issue template, which will ask you for certain output that we need to diagnose the problem.
@@ -0,0 +1,12 @@
1
+ # These are all the classes we believe people might want to call directly, so load
2
+ # them in response to a 'require octocatalog-diff'.
3
+
4
+ loads = [
5
+ 'bootstrap',
6
+ 'catalog',
7
+ 'facts',
8
+ 'puppetdb',
9
+ 'version',
10
+ 'catalog-diff/cli'
11
+ ]
12
+ loads.each { |f| require_relative "octocatalog-diff/#{f}" }
@@ -0,0 +1,53 @@
1
+ require 'open3'
2
+ require 'shellwords'
3
+
4
+ module OctocatalogDiff
5
+ # Contains bootstrap function to bootstrap a checked-out Puppet environment.
6
+ class Bootstrap
7
+ # Bootstrap a checked-out Puppet environment
8
+ # @param options [Hash] Options hash:
9
+ # :path [String] => Directory to bootstrap
10
+ # :bootstrap_script [String] => Bootstrap script, relative to directory
11
+ # @return [Hash] => [Fixnum] :status_code, [String] :output
12
+ def self.bootstrap(options = {})
13
+ # Options validation
14
+ unless options[:path].is_a?(String)
15
+ raise ArgumentError, 'Directory to bootstrap (:path) undefined or wrong data type'
16
+ end
17
+ unless File.directory?(options[:path])
18
+ raise Errno::ENOENT, "Non-existent directory '#{options[:path]}' in bootstrap"
19
+ end
20
+ unless options[:bootstrap_script].is_a?(String)
21
+ raise ArgumentError, 'Bootstrap script (:bootstrap_script) undefined or wrong data type'
22
+ end
23
+ bootstrap_script = File.join(options[:path], options[:bootstrap_script])
24
+ unless File.file?(bootstrap_script)
25
+ raise Errno::ENOENT, "Non-existent bootstrap script '#{options[:bootstrap_script]}'"
26
+ end
27
+
28
+ # 'env' sets up the environment variables that will be passed to the script.
29
+ # This is a clean environment.
30
+ env = {
31
+ 'PWD' => options[:path],
32
+ 'HOME' => ENV['HOME'],
33
+ 'PATH' => ENV['PATH'],
34
+ 'BASEDIR' => options[:basedir]
35
+ }
36
+ env.merge!(options[:bootstrap_environment]) if options[:bootstrap_environment].is_a?(Hash)
37
+
38
+ # 'opts' are options passed to the Open3.capture2e command which are described
39
+ # here: http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-capture2e
40
+ # Setting { :chdir => dir } means the shelled-out script will execute in the specified directory
41
+ # This natively avoids the need to shell out to 'cd $dir && script/bootstrap'
42
+ opts = { chdir: options[:path], unsetenv_others: true }
43
+
44
+ # Actually execute the command and capture the output (combined stdout and stderr).
45
+ cmd = [bootstrap_script, options[:bootstrap_args]].compact.map { |x| Shellwords.escape(x) }.join(' ')
46
+ output, status = Open3.capture2e(env, cmd, opts)
47
+ {
48
+ status_code: status.exitstatus,
49
+ output: output
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,205 @@
1
+ require_relative 'cli/catalogs'
2
+ require_relative 'cli/diffs'
3
+ require_relative 'cli/options'
4
+ require_relative 'cli/printer'
5
+ require_relative '../catalog-util/cached_master_directory'
6
+ require_relative 'cli/helpers/fact_override'
7
+ require_relative '../version'
8
+
9
+ require 'logger'
10
+ require 'socket'
11
+
12
+ module OctocatalogDiff
13
+ module CatalogDiff
14
+ # This is the CLI for catalog-diff. It's responsible for parsing the command line
15
+ # arguments and then handing off to appropriate methods to perform the catalog-diff.
16
+ class Cli
17
+ # Version number
18
+ VERSION = OctocatalogDiff::Version::VERSION
19
+
20
+ # Exit codes
21
+ EXITCODE_SUCCESS_NO_DIFFS = 0
22
+ EXITCODE_FAILURE = 1
23
+ EXITCODE_SUCCESS_WITH_DIFFS = 2
24
+
25
+ # The default type+title+attribute to ignore in catalog-diff.
26
+ DEFAULT_IGNORES = [
27
+ { type: 'Class' } # Don't care about classes themselves, only what they actually do!
28
+ ].freeze
29
+
30
+ # The default options.
31
+ DEFAULT_OPTIONS = {
32
+ from_env: 'origin/master',
33
+ to_env: '.',
34
+ colors: true,
35
+ debug: false,
36
+ quiet: false,
37
+ format: :color_text,
38
+ display_source_file_line: false,
39
+ compare_file_text: true,
40
+ display_datatype_changes: true,
41
+ parallel: true,
42
+ suppress_absent_file_details: true
43
+ }.freeze
44
+
45
+ # This method is the one to call externally. It is possible to specify alternate
46
+ # command line arguments, for testing.
47
+ # @param argv [Array] Use specified arguments (defaults to ARGV)
48
+ # @param logger [Logger] Logger object
49
+ # @param opts [Hash] Additional options
50
+ # @return [Fixnum] Exit code: 0=no diffs, 1=something went wrong, 2=worked but there are diffs
51
+ def self.cli(argv = ARGV, logger = Logger.new(STDERR), opts = {})
52
+ # Save a copy of argv to print out later in debugging
53
+ argv_save = argv.dup
54
+
55
+ # Are there additional ARGV to munge, e.g. that have been supplied in the options from a
56
+ # configuration file?
57
+ if opts.key?(:additional_argv)
58
+ raise ArgumentError, ':additional_argv must be array!' unless opts[:additional_argv].is_a?(Array)
59
+ argv.concat opts[:additional_argv]
60
+ end
61
+
62
+ # Parse command line
63
+ options = parse_opts(argv)
64
+
65
+ # Additional options from hard-coded specified options. These are only processed if
66
+ # there are not already values defined from command line options.
67
+ # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false)
68
+ # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist.
69
+ opts.each { |k, v| options[k] = v unless options.key?(k) }
70
+ veto_options = %w(enc header hiera_config include_tags)
71
+ veto_options.each { |x| options.delete(x.to_sym) if options["no_#{x}".to_sym] }
72
+ options[:ignore].concat opts.fetch(:additional_ignores, [])
73
+
74
+ # Incorporate default options where needed.
75
+ # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false)
76
+ # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist.
77
+ DEFAULT_OPTIONS.each { |k, v| options[k] = v unless options.key?(k) }
78
+
79
+ # Fact overrides come in here - 'options' is modified
80
+ setup_fact_overrides(options)
81
+
82
+ # Configure the logger and logger.debug initial information
83
+ # 'logger' is modified and used
84
+ setup_logger(logger, options, argv_save)
85
+
86
+ # --catalog-only is a special case that compiles the catalog for the "to" branch
87
+ # and then exits, without doing any 'diff' whatsoever. Support that option.
88
+ return catalog_only(logger, options) if options[:catalog_only]
89
+
90
+ # Set up the cached master directory - maintain it, adjust options if needed. However, if we
91
+ # are getting the 'from' catalog from PuppetDB, then don't do this.
92
+ unless options[:cached_master_dir].nil? || options[:from_puppetdb]
93
+ OctocatalogDiff::CatalogUtil::CachedMasterDirectory.run(options, logger)
94
+ end
95
+
96
+ # bootstrap_then_exit is a special case that only prepares directories and does not
97
+ # depend on facts. This happens within the 'catalogs' object, since bootstrapping and
98
+ # preparing catalogs are tightly coupled operations. However this does not actually
99
+ # build catalogs.
100
+ catalogs_obj = OctocatalogDiff::CatalogDiff::Cli::Catalogs.new(options, logger)
101
+ return bootstrap_then_exit(logger, catalogs_obj) if options[:bootstrap_then_exit]
102
+
103
+ # Compile catalogs
104
+ catalogs = catalogs_obj.catalogs
105
+ logger.info "Catalogs compiled for #{options[:node]}"
106
+
107
+ # Cache catalogs if master caching is enabled. If a catalog is being read from the cached master
108
+ # directory, set the compilation directory attribute, so that the "compilation directory dependent"
109
+ # suppressor will still work.
110
+ %w(from to).each do |x|
111
+ next unless options["#{x}_env".to_sym] == options.fetch(:master_cache_branch, 'origin/master')
112
+ next if options[:cached_master_dir].nil?
113
+ catalogs[x.to_sym].compilation_dir = options["#{x}_catalog_compilation_dir".to_sym] || options[:cached_master_dir]
114
+ rc = OctocatalogDiff::CatalogUtil::CachedMasterDirectory.save_catalog_in_cache_dir(
115
+ options[:node],
116
+ options[:cached_master_dir],
117
+ catalogs[x.to_sym]
118
+ )
119
+ logger.debug "Cached master catalog for #{options[:node]}" if rc
120
+ end
121
+
122
+ # Compute diffs
123
+ diffs_obj = OctocatalogDiff::CatalogDiff::Cli::Diffs.new(options, logger)
124
+ diffs = diffs_obj.diffs(catalogs)
125
+ logger.info "Diffs computed for #{options[:node]}"
126
+
127
+ # Display diffs
128
+ logger.info 'No differences' if diffs.empty?
129
+ printer_obj = OctocatalogDiff::CatalogDiff::Cli::Printer.new(options, logger)
130
+ printer_obj.printer(diffs, catalogs[:from].compilation_dir, catalogs[:to].compilation_dir)
131
+
132
+ # Return the diff object if requested (generally for testing) or otherwise return exit code
133
+ return diffs if opts[:RETURN_DIFFS]
134
+ diffs.any? ? EXITCODE_SUCCESS_WITH_DIFFS : EXITCODE_SUCCESS_NO_DIFFS
135
+ end
136
+
137
+ # Parse command line options with 'optparse'. Returns a hash with the parsed arguments.
138
+ # @param argv [Array] Command line arguments (MUST be specified)
139
+ # @return [Hash] Options
140
+ def self.parse_opts(argv)
141
+ options = { ignore: DEFAULT_IGNORES.dup }
142
+ Options.parse_options(argv, options)
143
+ end
144
+
145
+ # Fact overrides come in here
146
+ def self.setup_fact_overrides(options)
147
+ [:from_fact_override, :to_fact_override].each do |key|
148
+ o = options["#{key}_in".to_sym]
149
+ next unless o.is_a?(Array)
150
+ next unless o.any?
151
+ options[key] ||= []
152
+ options[key].concat o.map { |x| OctocatalogDiff::CatalogDiff::Cli::Helpers::FactOverride.new(x) }
153
+ end
154
+ end
155
+
156
+ # Helper method: Configure and setup logger
157
+ def self.setup_logger(logger, options, argv_save)
158
+ # Configure the logger
159
+ logger.level = Logger::INFO
160
+ logger.level = Logger::DEBUG if options[:debug]
161
+ logger.level = Logger::ERROR if options[:quiet]
162
+
163
+ # Some debugging information up front
164
+ logger.debug "Running octocatalog-diff #{VERSION} with ruby #{RUBY_VERSION}"
165
+ logger.debug "Command line arguments: #{argv_save.inspect}"
166
+ logger.debug "Running on host #{Socket.gethostname} (#{RUBY_PLATFORM})"
167
+ end
168
+
169
+ # Compile the catalog only
170
+ def self.catalog_only(logger, options)
171
+ # Indicate where we are
172
+ logger.debug "Compiling catalog --catalog-only for #{options[:node]}"
173
+
174
+ # Compile catalog
175
+ catalog_opts = options.merge(
176
+ from_catalog: '-', # Prevents a compile
177
+ to_catalog: nil, # Forces a compile
178
+ )
179
+ cat_obj = OctocatalogDiff::CatalogDiff::Cli::Catalogs.new(catalog_opts, logger)
180
+ catalogs = cat_obj.catalogs
181
+
182
+ # If the catalog compilation failed, an exception would have been thrown. So if
183
+ # we get here, the catalog succeeded. Dump the catalog to the appropriate place
184
+ # and exit successfully.
185
+ if options[:output_file]
186
+ File.open(options[:output_file], 'w') { |f| f.write(catalogs[:to].catalog_json) }
187
+ logger.info "Wrote catalog to #{options[:output_file]}"
188
+ else
189
+ puts catalogs[:to].catalog_json
190
+ end
191
+ return [catalogs[:from], catalogs[:to]] if options[:RETURN_DIFFS] # For integration testing
192
+ EXITCODE_SUCCESS_NO_DIFFS
193
+ end
194
+
195
+ # --bootstrap-then-exit command
196
+ def self.bootstrap_then_exit(logger, catalogs_obj)
197
+ catalogs_obj.bootstrap_then_exit
198
+ return EXITCODE_SUCCESS_NO_DIFFS
199
+ rescue OctocatalogDiff::CatalogDiff::Cli::Catalogs::BootstrapError => exc
200
+ logger.fatal("--bootstrap-then-exit error: bootstrap failed (#{exc})")
201
+ return EXITCODE_FAILURE
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,240 @@
1
+ require 'json'
2
+ require 'open3'
3
+ require 'yaml'
4
+ require_relative '../../catalog-util/bootstrap' # For BootstrapError
5
+ require_relative '../../catalog'
6
+ require_relative '../../util/parallel'
7
+
8
+ module OctocatalogDiff
9
+ module CatalogDiff
10
+ class Cli
11
+ # Helper class to construct catalogs, performing all necessary steps such as
12
+ # bootstrapping directories, installing facts, and running puppet.
13
+ class Catalogs
14
+ # Exceptions that are anticipated can be caught in the calling class and tested for explicitly in spec tests.
15
+ class BootstrapError < RuntimeError; end
16
+ class CatalogError < RuntimeError; end
17
+
18
+ # Constructor
19
+ # @param options [Hash] Options
20
+ # @param logger [Logger] Logger object
21
+ def initialize(options, logger)
22
+ @options = options
23
+ @logger = logger
24
+ @catalogs = nil
25
+ raise '@logger must not be nil' if @logger.nil?
26
+ end
27
+
28
+ # Compile catalogs. This handles building both the old and new catalog (in parallel) and returns
29
+ # only when both catalogs have been built.
30
+ # @return [Hash] { :from => [OctocatalogDiff::Catalog], :to => [OctocatalogDiff::Catalog] }
31
+ def catalogs
32
+ @catalogs ||= build_catalog_parallelizer
33
+ end
34
+
35
+ # Handles the "bootstrap then exit" option, which bootstraps directories but
36
+ # exits without compiling catalogs.
37
+ def bootstrap_then_exit
38
+ @logger.debug('Begin bootstrap_then_exit')
39
+ OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory_parallelizer(@options, @logger)
40
+ @logger.debug('Success bootstrap_then_exit')
41
+ @logger.info('Successfully completed --bootstrap-then-exit action')
42
+ rescue OctocatalogDiff::CatalogUtil::Bootstrap::BootstrapError => exc
43
+ @logger.error("Bootstrap exception: #{exc}")
44
+ raise BootstrapError, "Bootstrap exception: #{exc}"
45
+ end
46
+
47
+ private
48
+
49
+ # Parallelizes bootstrapping of directories and building catalogs.
50
+ # @return [Hash] { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
51
+ def build_catalog_parallelizer
52
+ # Construct parallel tasks. The array supplied to OctocatalogDiff::Util::Parallel is the task portion
53
+ # of each of the tuples in catalog_tasks.
54
+ catalog_tasks = build_catalog_tasks
55
+
56
+ # Update any tasks for catalogs that do not need to be compiled. This is the case when --catalog-only
57
+ # is specified and only one catalog is to be built. This will change matching catalog tasks to the 'noop' type.
58
+ catalog_tasks.map! do |x|
59
+ if @options["#{x[0]}_catalog".to_sym] == '-'
60
+ x[1].args[:backend] = :noop
61
+ elsif @options["#{x[0]}_catalog".to_sym].is_a?(String)
62
+ x[1].args[:json] = File.read(@options["#{x[0]}_catalog".to_sym])
63
+ x[1].args[:backend] = :json
64
+ end
65
+ x
66
+ end
67
+
68
+ # Initialize the objects for each parallel task. Initializing the object is very fast and does not actually
69
+ # build the catalog.
70
+ result = {}
71
+ catalog_tasks.each do |x|
72
+ result[x[0]] = OctocatalogDiff::Catalog.new(x[1].args)
73
+ @logger.debug "Initialized #{result[x[0]].builder} for #{x[0]}-catalog"
74
+ end
75
+
76
+ # Disable --compare-file-text if either (or both) of the chosen backends do not support it
77
+ if @options.fetch(:compare_file_text, false)
78
+ result.each do |_key, val|
79
+ next unless val.convert_file_resources == false
80
+ @logger.debug "Disabling --compare-file-text; not supported by #{val.builder}"
81
+ @options[:compare_file_text] = false
82
+ catalog_tasks.map! do |x|
83
+ x[1].args[:compare_file_text] = false
84
+ x
85
+ end
86
+ break
87
+ end
88
+ end
89
+
90
+ # Inject the starting object into the catalog tasks
91
+ catalog_tasks.map! do |x|
92
+ x[1].args[:object] = result[x[0]]
93
+ x
94
+ end
95
+
96
+ # Execute the parallelized catalog builds
97
+ passed_catalog_tasks = catalog_tasks.map { |x| x[1] }
98
+ parallel_catalogs = OctocatalogDiff::Util::Parallel.run_tasks(passed_catalog_tasks, @logger, @options[:parallel])
99
+
100
+ # If the catalogs array is empty at this point, there is an unexpected size mismatch. This should
101
+ # never happen, but test for it anyway.
102
+ unless parallel_catalogs.size == catalog_tasks.size
103
+ # :nocov:
104
+ raise "BUG: mismatch catalog_result (#{parallel_catalogs.size} vs #{catalog_tasks.size})"
105
+ # :nocov:
106
+ end
107
+
108
+ # Construct result hash. Will eventually be in the format
109
+ # { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
110
+
111
+ # Analyze the results from parallel run.
112
+ catalog_tasks.each do |x|
113
+ # The `parallel_catalog_obj` is a OctocatalogDiff::Util::Parallel::Result. Get the first element from
114
+ # the parallel_catalogs output.
115
+ parallel_catalog_obj = parallel_catalogs.shift
116
+
117
+ # Add the result to the 'result' hash
118
+ add_parallel_result(result, parallel_catalog_obj, x)
119
+ end
120
+
121
+ # Things have succeeded if the :to and :from catalogs exist at this point. If not, things have
122
+ # failed, and an exception should be thrown.
123
+ raise CatalogError, 'One or more catalogs failed to compile.' unless result.key?(:to) && result.key?(:from)
124
+ result
125
+ end
126
+
127
+ # Get catalog compilation tasks.
128
+ # @return [Array<[key, task]>] Catalog tasks
129
+ def build_catalog_tasks
130
+ [:from, :to].map do |key|
131
+ # These are arguments to OctocatalogDiff::Util::Parallel::Task. In most cases the arguments
132
+ # of OctocatalogDiff::Util::Parallel::Task are taken directly from options, but there are
133
+ # some defaults or otherwise-named options that must be set here.
134
+ args = @options.merge(
135
+ tag: key.to_s,
136
+ branch: @options["#{key}_env".to_sym],
137
+ bootstrapped_dir: @options["bootstrapped_#{key}_dir".to_sym],
138
+ basedir: @options[:basedir],
139
+ compare_file_text: @options.fetch(:compare_file_text, true),
140
+ retry_failed_catalog: @options.fetch(:retry_failed_catalog, 0),
141
+ parser: @options["parser_#{key}".to_sym]
142
+ )
143
+
144
+ # If any options are in the form of 'to_SOMETHING' or 'from_SOMETHING', this sets the option to
145
+ # 'SOMETHING' for the catalog if it matches this key. For example, when compiling the 'to' catalog
146
+ # when an option of :to_some_arg => 'foo', this sets :some_arg => foo, and deletes :to_some_arg and
147
+ # :from_some_arg.
148
+ @options.keys.select { |x| x.to_s =~ /^(to|from)_/ }.each do |opt_key|
149
+ args[opt_key.to_s.sub(/^(to|from)_/, '').to_sym] = @options[opt_key] if opt_key.to_s.start_with?(key.to_s)
150
+ args.delete(opt_key)
151
+ end
152
+
153
+ # The task is a OctocatalogDiff::Util::Parallel::Task object that contains the method to execute,
154
+ # validator method, text description, and arguments to provide when calling the method.
155
+ task = OctocatalogDiff::Util::Parallel::Task.new(
156
+ method: method(:build_catalog),
157
+ validator: method(:catalog_validator),
158
+ description: "build_catalog for #{@options["#{key}_env".to_sym]}",
159
+ args: args
160
+ )
161
+
162
+ # The format of `catalog_tasks` will be a tuple, where the first element is the key
163
+ # (e.g. :to or :from) and the second element is the OctocatalogDiff::Util::Parallel::Task object.
164
+ [key, task]
165
+ end.compact
166
+ end
167
+
168
+ # Given a result from the 'parallel' run and a corresponding (key,task) tuple, add valid
169
+ # catalogs to the 'result' hash and throw errors for invalid catalogs.
170
+ # @param result [Hash] Result hash for build_catalog_parallelizer (may be modified)
171
+ # @param parallel_catalog_obj [OctocatalogDiff::Util::Parallel::Result] Parallel catalog result
172
+ # @param key_task_tuple [Array<key, task>] Key, task tuple
173
+ def add_parallel_result(result, parallel_catalog_obj, key_task_tuple)
174
+ # Expand the tuple into variables
175
+ key, task = key_task_tuple
176
+
177
+ # For reporting purposes, get the branch name.
178
+ branch = task.args[:branch]
179
+
180
+ # Check the result of the parallel run on this object.
181
+ if parallel_catalog_obj.status.nil?
182
+ # The compile was killed because another task failed.
183
+ @logger.warn "Catalog compile for #{branch} was aborted due to another failure"
184
+
185
+ elsif parallel_catalog_obj.output.is_a?(OctocatalogDiff::Catalog)
186
+ # The result is a catalog, but we do not know if it was successfully compiled
187
+ # until we test the validity.
188
+ catalog = parallel_catalog_obj.output
189
+ if catalog.valid?
190
+ # The catalog was successfully compiled.
191
+ result[key] = parallel_catalog_obj.output
192
+ else
193
+ # The catalog failed, but a catalog object was returned so that better error reporting
194
+ # can take place. In this error reporting, we will replace 'Error:' with '[Puppet Error]'
195
+ # and remove the compilation directory (which is a tmpdir) to reveal only the relative
196
+ # path to the files involved.
197
+ dir = catalog.compilation_dir || ''
198
+ dir_regex = Regexp.new(Regexp.escape(dir) + '/environments/production/')
199
+ error_display = catalog.error_message.split("\n").map do |line|
200
+ line.sub(/^Error:/, '[Puppet Error]').gsub(dir_regex, '')
201
+ end.join("\n")
202
+ raise CatalogError, "Catalog for #{branch} failed to compile due to errors:\n#{error_display}"
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 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
+ # @return [Boolean] true if catalog is valid, false otherwise
233
+ def catalog_validator(catalog = nil, _logger = @logger)
234
+ return false unless catalog.is_a?(OctocatalogDiff::Catalog)
235
+ catalog.valid?
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end