octocatalog-diff 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/LICENSE +20 -0
- data/README.md +82 -0
- data/bin/octocatalog-diff +75 -0
- data/doc/advanced-bootstrap.md +33 -0
- data/doc/advanced-cache-dir.md +24 -0
- data/doc/advanced-catalog-only.md +37 -0
- data/doc/advanced-ci.md +13 -0
- data/doc/advanced-dynamic-ignores.md +123 -0
- data/doc/advanced-future-parser.md +11 -0
- data/doc/advanced-ignores.md +224 -0
- data/doc/advanced-output-formats.md +96 -0
- data/doc/advanced-output-hacks.md +45 -0
- data/doc/advanced-override-facts.md +67 -0
- data/doc/advanced-pe-enc.md +52 -0
- data/doc/advanced-puppet-master.md +50 -0
- data/doc/advanced-puppet-versions.md +9 -0
- data/doc/advanced-storeconfigs.md +72 -0
- data/doc/advanced-using-without-git.md +15 -0
- data/doc/advanced.md +43 -0
- data/doc/basic.md +70 -0
- data/doc/configuration-enc.md +69 -0
- data/doc/configuration-hiera.md +103 -0
- data/doc/configuration-puppetdb.md +49 -0
- data/doc/configuration.md +51 -0
- data/doc/dev/README.md +1 -0
- data/doc/dev/coverage.md +34 -0
- data/doc/dev/how-to-add-options.md +83 -0
- data/doc/dev/integration-tests.md +63 -0
- data/doc/dev/releasing.md +19 -0
- data/doc/installation.md +49 -0
- data/doc/limitations.md +34 -0
- data/doc/optionsref.md +947 -0
- data/doc/requirements.md +16 -0
- data/doc/roadmap.md +26 -0
- data/doc/similar.md +17 -0
- data/doc/troubleshooting.md +54 -0
- data/lib/octocatalog-diff.rb +12 -0
- data/lib/octocatalog-diff/bootstrap.rb +53 -0
- data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
- data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
- data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
- data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
- data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
- data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
- data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
- data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
- data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
- data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
- data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
- data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
- data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
- data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
- data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
- data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
- data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
- data/lib/octocatalog-diff/catalog.rb +209 -0
- data/lib/octocatalog-diff/catalog/computed.rb +205 -0
- data/lib/octocatalog-diff/catalog/json.rb +30 -0
- data/lib/octocatalog-diff/catalog/noop.rb +19 -0
- data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
- data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
- data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
- data/lib/octocatalog-diff/external/pson/README.md +20 -0
- data/lib/octocatalog-diff/external/pson/common.rb +370 -0
- data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
- data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
- data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
- data/lib/octocatalog-diff/external/pson/version.rb +8 -0
- data/lib/octocatalog-diff/facts.rb +125 -0
- data/lib/octocatalog-diff/facts/json.rb +20 -0
- data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
- data/lib/octocatalog-diff/facts/yaml.rb +29 -0
- data/lib/octocatalog-diff/puppetdb.rb +163 -0
- data/lib/octocatalog-diff/util/colored.rb +20 -0
- data/lib/octocatalog-diff/util/httparty.rb +158 -0
- data/lib/octocatalog-diff/util/parallel.rb +170 -0
- data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
- data/lib/octocatalog-diff/version.rb +7 -0
- metadata +386 -0
@@ -0,0 +1,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
|