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,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