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.
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,145 @@
1
+ require_relative '../bootstrap'
2
+ require_relative '../util/parallel'
3
+ require_relative 'git'
4
+
5
+ require 'fileutils'
6
+
7
+ module OctocatalogDiff
8
+ module CatalogUtil
9
+ # Methods to bootstrap a directory. Intended to be called from catalog-diff/cli. This handles
10
+ # parallelization of bootstrap, and formats arguments as expected by the higher level bootstrap
11
+ # script.
12
+ class Bootstrap
13
+ # Exceptions that are anticipated can be caught in the calling class and tested
14
+ # for explicitly in spec tests.
15
+ class BootstrapError < RuntimeError; end
16
+
17
+ # Bootstrap directories specified by --bootstrapped-from-dir and --bootstrapped-to-dir
18
+ # command line options. Bootstrapping occurs in parallel. This takes no parameters (options come
19
+ # from options) and returns nothing (it raises an exception if something fails).
20
+ def self.bootstrap_directory_parallelizer(options, logger)
21
+ # What directories do we have to bootstrap?
22
+ dirs = []
23
+
24
+ unless options[:bootstrapped_from_dir].nil?
25
+ if options[:from_env] == '.'
26
+ message = 'Must specify a from-branch other than . when using --bootstrapped-from-dir!' \
27
+ ' Please use "-f <from_branch>" argument.'
28
+ logger.error(message)
29
+ raise BootstrapError, message
30
+ end
31
+
32
+ opts = options.merge(branch: options[:from_env],
33
+ path: options[:bootstrapped_from_dir],
34
+ tag: 'from_dir',
35
+ dir: options[:basedir])
36
+ dirs << opts
37
+ end
38
+
39
+ unless options[:bootstrapped_to_dir].nil?
40
+ if options[:to_env] == '.'
41
+ message = 'Must specify a to-branch other than . when using --bootstrapped-to-dir!' \
42
+ ' Please use "-t <to_branch>" argument.'
43
+ logger.error(message)
44
+ raise BootstrapError, message
45
+ end
46
+
47
+ opts = options.merge(branch: options[:to_env],
48
+ path: options[:bootstrapped_to_dir],
49
+ tag: 'to_dir')
50
+ dirs << opts
51
+ end
52
+
53
+ # If there are no directories given, advise the user to supply the necessary options
54
+ if dirs.empty?
55
+ return unless options[:cached_master_dir].nil?
56
+ message = 'Specify one or more of --bootstrapped-from-dir / --bootstrapped-to-dir / --cached-master-dir' \
57
+ ' when using --bootstrap_then_exit'
58
+ logger.error(message)
59
+ raise BootstrapError, message
60
+ end
61
+
62
+ # Bootstrap the directories in parallel. Since there are no results here that we
63
+ # care about, increment the success counter for each run that did not throw an exception.
64
+ tasks = dirs.map do |x|
65
+ OctocatalogDiff::Util::Parallel::Task.new(
66
+ method: method(:bootstrap_directory),
67
+ description: "bootstrap #{x[:tag]} #{x[:path]} for #{x[:branch]}",
68
+ args: x
69
+ )
70
+ end
71
+
72
+ logger.debug("Begin #{dirs.size} bootstrap(s)")
73
+ parallel_tasks = OctocatalogDiff::Util::Parallel.run_tasks(tasks, logger, options[:parallel])
74
+ parallel_tasks.each do |result|
75
+ if result.status
76
+ logger.debug("Success bootstrap_directory for #{result.args[:tag]}")
77
+ else
78
+ errmsg = "Failed bootstrap_directory for #{result.args[:tag]}: #{result.exception.class} #{result.exception.message}"
79
+ raise BootstrapError, errmsg
80
+ end
81
+ end
82
+ end
83
+
84
+ # Performs the actual bootstrap of a directory. Intended to be called by bootstrap_directory_parallelizer
85
+ # above, or as part of the parallelized catalog build process from catalog-diff/cli/catalogs.
86
+ # @param logger [Logger] Logger object
87
+ # @param dir_opts [Hash] Directory options: branch, path, tag
88
+ def self.bootstrap_directory(options, logger)
89
+ raise ArgumentError, ':path must be supplied' unless options[:path]
90
+ FileUtils.mkdir_p(options[:path]) unless Dir.exist?(options[:path])
91
+ git_checkout(logger, options) if options[:branch]
92
+ unless options[:bootstrap_script].nil?
93
+ install_bootstrap_script(logger, options)
94
+ run_bootstrap(logger, options)
95
+ end
96
+ end
97
+
98
+ # Perform git checkout
99
+ # @param logger [Logger] Logger object
100
+ # @param dir_opts [Hash] Directory options: branch, path, tag
101
+ def self.git_checkout(logger, dir_opts)
102
+ logger.debug("Begin git checkout #{dir_opts[:basedir]}:#{dir_opts[:branch]} -> #{dir_opts[:path]}")
103
+ OctocatalogDiff::CatalogUtil::Git.check_out_git_archive(dir_opts.merge(logger: logger))
104
+ logger.debug("Success git checkout #{dir_opts[:basedir]}:#{dir_opts[:branch]} -> #{dir_opts[:path]}")
105
+ rescue OctocatalogDiff::CatalogUtil::Git::GitCheckoutError => exc
106
+ logger.error("Git checkout error: #{exc}")
107
+ raise BootstrapError, exc
108
+ end
109
+
110
+ # Install bootstrap script in the target directory. This allows proper bootstrapping from the
111
+ # latest version of the script, not the script that was in place at the time that directory's branch
112
+ # was committed.
113
+ # @param logger [Logger] Logger object
114
+ # @param opts [Hash] Directory options
115
+ def self.install_bootstrap_script(logger, opts)
116
+ # Verify that bootstrap file exists
117
+ src = File.join(opts[:basedir], opts[:bootstrap_script])
118
+ raise BootstrapError, "Bootstrap script #{src} does not exist" unless File.file?(src)
119
+
120
+ logger.debug('Begin install bootstrap script in target directory')
121
+
122
+ # Create destination directory if needed
123
+ dest = File.join(opts[:path], opts[:bootstrap_script])
124
+ dest_dir = File.dirname(dest)
125
+ FileUtils.mkdir_p(dest_dir) unless File.directory?(dest_dir)
126
+
127
+ # Copy file and make executable
128
+ FileUtils.cp src, dest
129
+ FileUtils.chmod 0o755, dest
130
+ logger.debug("Success: copied #{src} to #{dest}")
131
+ end
132
+
133
+ # Execute the bootstrap.
134
+ # @param logger [Logger] Logger object
135
+ # @param opts [Hash] Directory options
136
+ def self.run_bootstrap(logger, opts)
137
+ logger.debug("Begin bootstrap with '#{opts[:bootstrap_script]}' in #{opts[:path]}")
138
+ result = OctocatalogDiff::Bootstrap.bootstrap(opts)
139
+ raise BootstrapError, "bootstrap failed for #{opts[:path]}: #{result[:output]}" unless (result[:status_code]).zero?
140
+ logger.debug("Success bootstrap in #{opts[:path]}")
141
+ result[:output]
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,289 @@
1
+ require 'yaml'
2
+
3
+ require_relative '../facts'
4
+ require_relative 'enc'
5
+
6
+ module OctocatalogDiff
7
+ module CatalogUtil
8
+ # Represents a directory that is created such that a catalog can be compiled
9
+ # in it. This has the following major functions:
10
+ # - Create the temporary directory that will serve as the puppet configuration directory
11
+ # - Register a handler to remove the temporary directory upon exit
12
+ # - Install needed configuration files within the directory (e.g. puppetdb.conf)
13
+ # - Install the facts into the directory
14
+ # - Install 'environments/production' which is a symlink to the checkout of the puppet code
15
+ class BuildDir
16
+ # Allow the path to the temporary directory to be read
17
+ attr_reader :tempdir, :enc, :fact_file
18
+
19
+ # Constructor
20
+ # Options for constructor:
21
+ # :puppetdb_url [String] PuppetDB Server URLs
22
+ # :puppetdb_server_url_timeout [Fixnum] Timeout (seconds) for puppetdb.conf
23
+ # :facts [OctocatalogDiff::Facts] Facts object
24
+ # :fact_file [String] File from which to read facts
25
+ # :node [String] Node name
26
+ # :basedir [String] Directory containing puppet code
27
+ # :enc [String] ENC script file (can be relative or absolute path)
28
+ # :pe_enc_url [String] ENC URL (for Puppet Enterprise node classification service)
29
+ # :hiera_config [String] hiera configuration file (relative to base directory)
30
+ # :hiera_path_strip [String] string to strip off the beginning of :datadir
31
+ # :puppetdb_ssl_ca [String] Path to SSL CA certificate
32
+ # :puppetdb_ssl_client_key [String] String representation of SSL client key
33
+ # :puppetdb_ssl_client_cert [String] String representation of SSL client certificate
34
+ # :puppetdb_ssl_client_password [String] Password to unlock SSL private key
35
+ # @param options [Hash] Options for class; see above description
36
+ def initialize(options = {}, logger = nil)
37
+ @options = options.dup
38
+ @tempdir = Dir.mktmpdir
39
+ at_exit { FileUtils.rm_rf(@tempdir) if File.directory?(@tempdir) }
40
+
41
+ @factdir = nil
42
+ @enc = nil
43
+ @fact_file = nil
44
+ @node = options[:node]
45
+
46
+ create_structure
47
+ install_directory_symlink(logger, options[:basedir])
48
+
49
+ # These configurations are optional. Don't call the methods if parameters are nil.
50
+ unless options[:puppetdb_url].nil?
51
+ install_puppetdb_conf(logger, options[:puppetdb_url], options[:puppetdb_server_url_timeout])
52
+ install_routes_yaml(logger)
53
+ end
54
+ unless options[:hiera_config].nil?
55
+ install_hiera_config(logger, options[:hiera_config], options[:hiera_path_strip])
56
+ end
57
+ @fact_file = install_fact_file(logger, options) unless options.fetch(:facts_terminus, 'yaml') != 'yaml'
58
+ @enc = install_enc(logger) unless options[:enc].nil? && options[:pe_enc_url].nil?
59
+ install_ssl(logger, options) if options[:puppetdb_ssl_ca] || options[:puppetdb_ssl_client_cert]
60
+ end
61
+
62
+ # Create common structure
63
+ def create_structure
64
+ %w(environments facts var var/ssl var/yaml var/yaml/facts).each do |dir|
65
+ Dir.mkdir(File.join(@tempdir, dir))
66
+ FileUtils.chmod 0o755, File.join(@tempdir, dir)
67
+ end
68
+ end
69
+
70
+ # Install puppetdb.conf file in temporary directory
71
+ # @param server_urls [String] String for server_urls in puppetdb.conf
72
+ # @param server_url_timeout [Fixnum] Value for server_url_timeout in puppetdb.conf
73
+ def install_puppetdb_conf(logger, server_urls, server_url_timeout = 30)
74
+ unless server_urls.is_a?(String)
75
+ raise ArgumentError, "server_urls must be a string, got a: #{server_urls.class}"
76
+ end
77
+
78
+ server_url_timeout ||= 30 # If called with nil argument, supply default
79
+ unless server_url_timeout.is_a?(Fixnum)
80
+ raise ArgumentError, "server_url_timeout must be a fixnum, got a: #{server_url_timeout.class}"
81
+ end
82
+
83
+ puppetdb_conf = File.join(@tempdir, 'puppetdb.conf')
84
+ File.open(puppetdb_conf, 'w') do |f|
85
+ f.write "[main]\n"
86
+ f.write "server_urls = #{server_urls}\n"
87
+ f.write "server_url_timeout = #{server_url_timeout}\n"
88
+ end
89
+ logger.debug("Installed puppetdb.conf file at #{puppetdb_conf}")
90
+ end
91
+
92
+ # Install routes.yaml file in temporary directory
93
+ # No parameters or return - thus just writes a file (and notes it to debugging log)
94
+ # Note: catalog cache => json avoids sending the compiled catalog to PuppetDB
95
+ # even if storeconfigs is enabled.
96
+ def install_routes_yaml(logger)
97
+ routes_yaml = File.join(@tempdir, 'routes.yaml')
98
+ routes_hash = {
99
+ 'master' => {
100
+ 'facts' => {
101
+ 'terminus' => 'puppetdb',
102
+ 'cache' => 'yaml'
103
+ },
104
+ 'catalog' => {
105
+ 'cache' => 'json'
106
+ }
107
+ }
108
+ }
109
+ File.open(routes_yaml, 'w') { |f| f.write(routes_hash.to_yaml) }
110
+ logger.debug("Installed routes.yaml file at #{routes_yaml}")
111
+ end
112
+
113
+ # Install the fact file in temporary directory
114
+ # @param options [Hash] Options
115
+ def install_fact_file(logger, options)
116
+ unless options[:facts_terminus].nil? || options[:facts_terminus] == 'yaml'
117
+ raise ArgumentError, "Called install_fact_file but :facts_terminus = #{options[:facts_terminus]}"
118
+ end
119
+ unless options[:node].is_a?(String) && !options[:node].empty?
120
+ raise ArgumentError, 'Called install_fact_file without node, or with an empty node'
121
+ end
122
+
123
+ facts = if options[:fact_file]
124
+ raise Errno::ENOENT, "Fact file #{options[:fact_file]} does not exist" unless File.file?(options[:fact_file])
125
+ fact_file_opts = { fact_file_string: File.read(options[:fact_file]) }
126
+ fact_file_opts[:backend] = Regexp.last_match(1).to_sym if options[:fact_file] =~ /.*\.(\w+)$/
127
+ OctocatalogDiff::Facts.new(fact_file_opts)
128
+ elsif options[:facts].is_a?(OctocatalogDiff::Facts)
129
+ options[:facts].dup
130
+ else
131
+ raise ArgumentError, 'No facts passed to "install_fact_file" method'
132
+ end
133
+
134
+ if options[:fact_override].is_a?(Array)
135
+ options[:fact_override].each do |override|
136
+ old_value = facts.fact(override.key)
137
+ facts.override(override.key, override.value)
138
+ logger.debug("Override #{override.key} from #{old_value.inspect} to #{override.value.inspect}")
139
+ end
140
+ end
141
+
142
+ fact_file_out = File.join(@tempdir, 'var', 'yaml', 'facts', "#{options[:node]}.yaml")
143
+ File.open(fact_file_out, 'w') { |f| f.write(facts.facts_to_yaml(options[:node])) }
144
+ logger.debug("Installed fact file at #{fact_file_out}")
145
+ fact_file_out
146
+ end
147
+
148
+ # Install symbolic link to puppet environment
149
+ # @param dir [String] Directory to link to
150
+ def install_directory_symlink(logger, dir)
151
+ raise ArgumentError, "Called install_directory_symlink with #{dir.class} argument" unless dir.is_a?(String)
152
+ raise Errno::ENOENT, "Specified directory #{dir} doesn't exist" unless File.directory?(dir)
153
+
154
+ environment_symlink = File.join(@tempdir, 'environments', 'production')
155
+ FileUtils.rm_f environment_symlink if File.exist?(environment_symlink)
156
+ FileUtils.symlink dir, environment_symlink
157
+ logger.debug("Symlinked #{environment_symlink} -> #{dir}")
158
+ end
159
+
160
+ # Install ENC
161
+ # @param enc [String] Path to ENC script, relative to checkout
162
+ def install_enc(logger)
163
+ raise ArgumentError, 'A node must be specified when using an ENC' unless @node.is_a?(String)
164
+ enc_obj = OctocatalogDiff::CatalogUtil::ENC.new(@options.merge(tempdir: @tempdir))
165
+ raise "Failed ENC: #{enc_obj.error_message}" if enc_obj.error_message
166
+
167
+ enc_path = File.join(@tempdir, 'enc.sh')
168
+ File.open(enc_path, 'w') do |f|
169
+ f.write "#!/bin/sh\n"
170
+ f.write "cat <<-EOF\n"
171
+ f.write enc_obj.content
172
+ f.write "\nEOF\n"
173
+ end
174
+ FileUtils.chmod 0o755, enc_path
175
+
176
+ logger.debug("Installed ENC to echo content, #{enc_obj.content.length} bytes")
177
+ enc_path
178
+ end
179
+
180
+ # Install hiera config file
181
+ # @param hiera_config [String] Path to file, relative to checkout
182
+ # @param hiera_path_strip [String] Prefix to strip off when munging file
183
+ def install_hiera_config(logger, hiera_config, hiera_path_strip)
184
+ # Validate hiera config file
185
+ unless hiera_config.is_a?(String)
186
+ raise ArgumentError, "Called install_hiera_config with a #{hiera_config.class} argument"
187
+ end
188
+ file_src = if hiera_config.start_with? '/'
189
+ hiera_config
190
+ elsif hiera_config =~ %r{^environments/production/}
191
+ File.join(@tempdir, hiera_config)
192
+ else
193
+ File.join(@tempdir, 'environments', 'production', hiera_config)
194
+ end
195
+ raise Errno::ENOENT, "hiera.yaml (#{file_src}) wasn't found" unless File.file?(file_src)
196
+
197
+ # Munge datadir in hiera config file
198
+ obj = YAML.load_file(file_src)
199
+ %w(yaml json).each do |key|
200
+ next unless obj.key?(key.to_sym)
201
+ next if obj[key.to_sym][:datadir].nil?
202
+ unless hiera_path_strip.nil?
203
+ rexp1 = Regexp.new('^' + hiera_path_strip)
204
+ obj[key.to_sym][:datadir].sub!(rexp1, @tempdir)
205
+ end
206
+ rexp2 = Regexp.new('%{(::)?environment}')
207
+ obj[key.to_sym][:datadir].sub!(rexp2, 'production')
208
+ end
209
+
210
+ # Write properly formatted hiera config file into temporary directory
211
+ File.open(File.join(@tempdir, 'hiera.yaml'), 'w') { |f| f.write(obj.to_yaml.gsub('!ruby/sym ', ':')) }
212
+ logger.debug("Installed hiera.yaml from #{file_src} to #{File.join(@tempdir, 'hiera.yaml')}")
213
+ end
214
+
215
+ # Install SSL certificate authority certificate, client key, and client certificate into the
216
+ # expected locations within Puppet's SSL directory. Note that if the client key has a password,
217
+ # this will write the key (without password) onto disk, because Puppet doesn't support unlocking
218
+ # the private key.
219
+ # @param logger [Logger] Logger object
220
+ # @param options [Hash] Options hash
221
+ def install_ssl(logger, options)
222
+ return unless options[:puppetdb_ssl_client_cert] || options[:puppetdb_ssl_client_key] || options[:puppetdb_ssl_ca]
223
+
224
+ # Create directory structure expected by Puppet
225
+ %w(var/ssl/certs var/ssl/private var/ssl/private_keys).each do |dir|
226
+ Dir.mkdir(File.join(@tempdir, dir))
227
+ FileUtils.chmod 0o700, File.join(@tempdir, dir)
228
+ end
229
+
230
+ # SSL client auth requested?
231
+ if options[:puppetdb_ssl_client_cert] || options[:puppetdb_ssl_client_key]
232
+ raise ArgumentError, '--puppetdb-ssl-ca must be provided for client auth' unless options[:puppetdb_ssl_ca]
233
+ raise ArgumentError, '--puppetdb-ssl-client-cert must be provided' unless options[:puppetdb_ssl_client_cert]
234
+ raise ArgumentError, '--puppetdb-ssl-client-key must be provided' unless options[:puppetdb_ssl_client_key]
235
+ install_ssl_client(logger, options)
236
+ end
237
+
238
+ # SSL CA provided?
239
+ install_ssl_ca(logger, options) if options[:puppetdb_ssl_ca]
240
+ end
241
+
242
+ private
243
+
244
+ # Install SSL certificate authority certificate
245
+ # @param logger [Logger] Logger object
246
+ # @param options [Hash] Options hash
247
+ def install_ssl_ca(logger, options)
248
+ ca_file = options[:puppetdb_ssl_ca]
249
+ raise Errno::ENOENT, 'SSL CA file does not exist' unless File.file?(ca_file)
250
+ ca_content = File.read(ca_file)
251
+ ca_outfile = File.join(@tempdir, 'var', 'ssl', 'certs', 'ca.pem')
252
+ File.open(ca_outfile, 'w') { |f| f.write(ca_content) }
253
+ logger.debug "Installed CA certificate in #{ca_outfile}"
254
+ end
255
+
256
+ # Install SSL keypair for client certificate authentication
257
+ # @param logger [Logger] Logger object
258
+ # @param options [Hash] Options hash
259
+ def install_ssl_client(logger, options)
260
+ # Since Puppet always looks for the key and cert in a file named after the hostname, determine the
261
+ # hostname here for the purposes of naming the files.
262
+ require 'socket'
263
+ host = Socket.gethostname
264
+ install_ssl_client_cert(logger, host, options[:puppetdb_ssl_client_cert])
265
+ install_ssl_client_key(logger, host, options[:puppetdb_ssl_client_key])
266
+ install_ssl_client_password(logger, options[:puppetdb_ssl_client_password])
267
+ end
268
+
269
+ def install_ssl_client_cert(logger, host, content)
270
+ cert_outfile = File.join(@tempdir, 'var', 'ssl', 'certs', "#{host}.pem")
271
+ File.open(cert_outfile, 'w') { |f| f.write(content) }
272
+ logger.debug "Installed SSL client certificate in #{cert_outfile}"
273
+ end
274
+
275
+ def install_ssl_client_key(logger, host, content)
276
+ key_outfile = File.join(@tempdir, 'var', 'ssl', 'private_keys', "#{host}.pem")
277
+ File.open(key_outfile, 'w') { |f| f.write(content) }
278
+ logger.debug "Installed SSL client key in #{key_outfile}"
279
+ end
280
+
281
+ def install_ssl_client_password(logger, password)
282
+ return unless password
283
+ password_outfile = File.join(@tempdir, 'var', 'ssl', 'private', 'password')
284
+ File.open(password_outfile, 'w') { |f| f.write(password) }
285
+ logger.debug "Installed SSL client key password in #{password_outfile}"
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,169 @@
1
+ require_relative 'bootstrap'
2
+ require_relative 'git'
3
+ require_relative '../catalog-diff/cli/catalogs'
4
+
5
+ require 'fileutils'
6
+
7
+ module OctocatalogDiff
8
+ module CatalogUtil
9
+ # Handle the bootstrapped and cached checkout of [master branch]. This is an optimization
10
+ # targeted at local development environments, since a frequent pattern is "run a catalog-diff
11
+ # between what I have here, and master."
12
+ #
13
+ # Please note that there could be a race condition here if this code was run in parallel (i.e.,
14
+ # the cached master directory is blown away and re-created when a Puppet catalog compile is in
15
+ # progress). Do not introduce this code to an environment where catalog-diff may be running in
16
+ # parallel unless you have accounted for this (or are willing to tolerate any errors).
17
+ class CachedMasterDirectory
18
+ # Set default branch. Can be overridden by options[:master_cache_branch].
19
+ DEFAULT_MASTER_BRANCH = 'origin/master'.freeze
20
+
21
+ # Get the master branch based on supplied options.
22
+ # @param options [Hash] Options hash
23
+ # @return [String] Master branch configured (defaults to DEFAULT_MASTER_BRANCH)
24
+ def self.master_branch(options = {})
25
+ options.fetch(:master_cache_branch, DEFAULT_MASTER_BRANCH)
26
+ end
27
+
28
+ # This is the entry point from the CLI (or anywhere else). Takes options hash and logger
29
+ # as arguments, sets up the cached master directory if required, and adjusts options hash
30
+ # accordingly. Returns nothing; raises exceptions for failures.
31
+ # @param options [Hash] Options hash from CLI
32
+ # @param logger [Logger] Logger object
33
+ def self.run(options, logger)
34
+ # If nobody asked for this, don't do anything
35
+ return if options[:cached_master_dir].nil?
36
+
37
+ # Verify that parameters are set up correctly and that at least one of the to-branch and
38
+ # from-branch is [master branch]. If not, it's not worthwhile to do any of the remaining
39
+ # tasks in this section.
40
+ return unless cached_master_applicable_to_this_run?(options)
41
+
42
+ # This directory was supposed to be created as part of the option setup. Make sure it exists
43
+ # as a sanity check.
44
+ Dir.mkdir options[:cached_master_dir], 0o755 unless Dir.exist?(options[:cached_master_dir])
45
+
46
+ # Determine if it's necessary to check out the git repo to the directory in question.
47
+ git_repo_checkout_bootstrap(options, logger) unless git_repo_checkout_current?(options, logger)
48
+
49
+ # Under --bootstrap-then-exit, don't adjust the options. (Otherwise code runs twice.)
50
+ return if options[:bootstrap_then_exit]
51
+
52
+ # Re-point any options to the cached directory.
53
+ %w(from to).each do |x|
54
+ next unless options["#{x}_env".to_sym] == master_branch(options)
55
+ logger.debug "Setting --bootstrapped-#{x}-dir=#{options[:cached_master_dir]}"
56
+ options["bootstrapped_#{x}_dir".to_sym] = options[:cached_master_dir]
57
+ end
58
+
59
+ # If a catalog was already compiled for the requested node, point to it directly to avoid
60
+ # re-compiling said catalog.
61
+ unless options[:node].nil?
62
+ catalog_path = File.join(options[:cached_master_dir], '.catalogs', options[:node] + '.json')
63
+ if File.file?(catalog_path)
64
+ %w(from to).each do |x|
65
+ next unless options["#{x}_env".to_sym] == master_branch(options)
66
+ next unless options["#{x}_catalog".to_sym].nil?
67
+ logger.debug "Setting --#{x}-catalog=#{catalog_path}"
68
+ options["#{x}_catalog".to_sym] = catalog_path
69
+ options["#{x}_catalog_compilation_dir".to_sym] = options[:cached_master_dir]
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Determine if the cached master directory functionality is needed at all.
76
+ # @param options [Hash] Options hash from CLI
77
+ # @return [Boolean] whether to-branch and/or from-branch == [master branch]
78
+ def self.cached_master_applicable_to_this_run?(options)
79
+ return false if options[:cached_master_dir].nil?
80
+ target_branch = master_branch(options)
81
+ options.fetch(:from_env, '') == target_branch || options.fetch(:to_env, '') == target_branch
82
+ end
83
+
84
+ # Determine whether git repo checkout in the directory is current.
85
+ # To consider here: (a) is anything at all checked out; (b) is the correct SHA checked out?
86
+ # @param options [Hash] Options hash from CLI
87
+ # @param logger [Logger] Logger object
88
+ # @return [Boolean] whether git repo checkout in the directory is current
89
+ def self.git_repo_checkout_current?(options, logger)
90
+ shafile = File.join(options[:cached_master_dir], '.catalog-diff-master.sha')
91
+ return false unless File.file?(shafile)
92
+ bootstrapped_sha = File.read(shafile)
93
+ target_branch = master_branch(options)
94
+ branch_sha_opts = options.merge(branch: target_branch)
95
+ current_master_sha = OctocatalogDiff::CatalogUtil::Git.branch_sha(branch_sha_opts)
96
+ logger.debug "Cached master dir: bootstrapped=#{bootstrapped_sha}; current=#{current_master_sha}"
97
+ bootstrapped_sha.strip == current_master_sha.strip
98
+ end
99
+
100
+ # Check out [master branch] -> cached directory and bootstrap it
101
+ # @param options [Hash] Options hash from CLI
102
+ # @param logger [Logger] Logger object
103
+ def self.git_repo_checkout_bootstrap(options, logger)
104
+ # This directory isn't current so kill it
105
+ # Too dangerous if someone slips up on the command line:
106
+ # FileUtils.rm_rf options[:cached_master_dir] if Dir.exist?(options[:cached_master_dir])
107
+ shafile = File.join(options[:cached_master_dir], '.catalog-diff-master.sha')
108
+ target_branch = master_branch(options)
109
+ branch_sha_opts = options.merge(branch: target_branch)
110
+ current_master_sha = OctocatalogDiff::CatalogUtil::Git.branch_sha(branch_sha_opts)
111
+
112
+ if Dir.exist?(options[:cached_master_dir]) && File.exist?(shafile)
113
+ # If :cached_master_dir was set in a known-safe manner, safe_to_delete_cached_master_dir will
114
+ # allow the cleanup to take place automatically.
115
+ if options.fetch(:safe_to_delete_cached_master_dir, false) == options[:cached_master_dir]
116
+ FileUtils.rm_rf options[:cached_master_dir] if Dir.exist?(options[:cached_master_dir])
117
+ else
118
+ message = "To proceed, #{options[:cached_master_dir]} needs to be deleted, so it can be re-created."\
119
+ " I'm not yet deemed safe enough to do this for you though. Please jump out to a shell and run"\
120
+ " 'rm -rf #{options[:cached_master_dir]}' and then come back and try again. (Existing SHA:"\
121
+ " #{File.read(shafile).strip}; current master SHA: #{current_master_sha})"
122
+ raise Errno::EEXIST, message
123
+ end
124
+ end
125
+
126
+ # This logic is similar to 'bootstrap-then-exit' (without the exit part). The
127
+ # bootstrap_then_exit handles creating this directory.
128
+ fake_options = options.dup
129
+ fake_options[:bootstrap_then_exit] = true
130
+ fake_options[:bootstrapped_from_dir] = options[:cached_master_dir]
131
+ fake_options[:bootstrapped_to_dir] = nil
132
+ fake_options[:from_env] = master_branch(options)
133
+
134
+ logger.debug 'Begin bootstrap cached master directory'
135
+ catalogs_obj = OctocatalogDiff::CatalogDiff::Cli::Catalogs.new(fake_options, logger)
136
+ catalogs_obj.bootstrap_then_exit
137
+ logger.debug 'Success bootstrap cached master directory'
138
+
139
+ # Write the SHA of [master branch], so git_repo_checkout_current? works next time
140
+ File.open(shafile, 'w') { |f| f.write(current_master_sha) }
141
+ logger.debug "Cached master directory bootstrapped to #{current_master_sha}"
142
+
143
+ # Create <dir>/.catalogs, to save any catalogs compiled along the way
144
+ catalog_dir = File.join(options[:cached_master_dir], '.catalogs')
145
+ Dir.mkdir catalog_dir unless File.directory?(catalog_dir)
146
+ end
147
+
148
+ # Save a compiled catalog in the cached master directory. Does not die fatally if
149
+ # catalog is invalid or this isn't set up or whatever else.
150
+ # @param node [String] node name
151
+ # @param dir [String] cached master directory
152
+ # @param catalog [OctocatalogDiff::Catalog] Catalog object
153
+ # @return [Boolean] true if catalog was saved, false if not
154
+ def self.save_catalog_in_cache_dir(node, dir, catalog)
155
+ return false if dir.nil? || node.nil?
156
+ return false if catalog.nil? || !catalog.valid?
157
+
158
+ path = File.join(dir, '.catalogs')
159
+ return false unless Dir.exist?(path)
160
+
161
+ filepath = File.join(path, node + '.json')
162
+ return false if File.file?(filepath)
163
+
164
+ File.open(filepath, 'w') { |f| f.write(catalog.catalog_json) }
165
+ true
166
+ end
167
+ end
168
+ end
169
+ end