|
15 | 15 | require 'socket' |
16 | 16 |
|
17 | 17 | module OctocatalogDiff |
18 | | - module CatalogDiff |
19 | | - # This is the CLI for catalog-diff. It's responsible for parsing the command line |
20 | | - # arguments and then handing off to appropriate methods to perform the catalog-diff. |
21 | | - class Cli |
22 | | - # Version number |
23 | | - VERSION = OctocatalogDiff::Version::VERSION |
24 | | - |
25 | | - # Exit codes |
26 | | - EXITCODE_SUCCESS_NO_DIFFS = 0 |
27 | | - EXITCODE_FAILURE = 1 |
28 | | - EXITCODE_SUCCESS_WITH_DIFFS = 2 |
29 | | - |
30 | | - # The default type+title+attribute to ignore in catalog-diff. |
31 | | - DEFAULT_IGNORES = [ |
32 | | - { type: 'Class' } # Don't care about classes themselves, only what they actually do! |
33 | | - ].freeze |
34 | | - |
35 | | - # The default options. |
36 | | - DEFAULT_OPTIONS = { |
37 | | - from_env: 'origin/master', |
38 | | - to_env: '.', |
39 | | - colors: true, |
40 | | - debug: false, |
41 | | - quiet: false, |
42 | | - format: :color_text, |
43 | | - display_source_file_line: false, |
44 | | - compare_file_text: true, |
45 | | - display_datatype_changes: true, |
46 | | - parallel: true, |
47 | | - suppress_absent_file_details: true, |
48 | | - hiera_path: 'hieradata' |
49 | | - }.freeze |
50 | | - |
51 | | - # This method is the one to call externally. It is possible to specify alternate |
52 | | - # command line arguments, for testing. |
53 | | - # @param argv [Array] Use specified arguments (defaults to ARGV) |
54 | | - # @param logger [Logger] Logger object |
55 | | - # @param opts [Hash] Additional options |
56 | | - # @return [Fixnum] Exit code: 0=no diffs, 1=something went wrong, 2=worked but there are diffs |
57 | | - def self.cli(argv = ARGV, logger = Logger.new(STDERR), opts = {}) |
58 | | - # Save a copy of argv to print out later in debugging |
59 | | - argv_save = argv.dup |
60 | | - |
61 | | - # Are there additional ARGV to munge, e.g. that have been supplied in the options from a |
62 | | - # configuration file? |
63 | | - if opts.key?(:additional_argv) |
64 | | - raise ArgumentError, ':additional_argv must be array!' unless opts[:additional_argv].is_a?(Array) |
65 | | - argv.concat opts[:additional_argv] |
66 | | - end |
67 | | - |
68 | | - # Parse command line |
69 | | - options = parse_opts(argv) |
70 | | - |
71 | | - # Additional options from hard-coded specified options. These are only processed if |
72 | | - # there are not already values defined from command line options. |
73 | | - # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false) |
74 | | - # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist. |
75 | | - opts.each { |k, v| options[k] = v unless options.key?(k) } |
76 | | - veto_options = %w(enc header hiera_config include_tags) |
77 | | - veto_options.each { |x| options.delete(x.to_sym) if options["no_#{x}".to_sym] } |
78 | | - options[:ignore].concat opts.fetch(:additional_ignores, []) |
79 | | - |
80 | | - # Incorporate default options where needed. |
81 | | - # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false) |
82 | | - # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist. |
83 | | - DEFAULT_OPTIONS.each { |k, v| options[k] = v unless options.key?(k) } |
84 | | - veto_with_none_options = %w(hiera_path hiera_path_strip) |
85 | | - veto_with_none_options.each { |x| options.delete(x.to_sym) if options[x.to_sym] == :none } |
86 | | - |
87 | | - # Fact overrides come in here - 'options' is modified |
88 | | - setup_fact_overrides(options) |
89 | | - |
90 | | - # Configure the logger and logger.debug initial information |
91 | | - # 'logger' is modified and used |
92 | | - setup_logger(logger, options, argv_save) |
93 | | - |
94 | | - # --catalog-only is a special case that compiles the catalog for the "to" branch |
95 | | - # and then exits, without doing any 'diff' whatsoever. Support that option. |
96 | | - return catalog_only(logger, options) if options[:catalog_only] |
97 | | - |
98 | | - # Set up the cached master directory - maintain it, adjust options if needed. However, if we |
99 | | - # are getting the 'from' catalog from PuppetDB, then don't do this. |
100 | | - unless options[:cached_master_dir].nil? || options[:from_puppetdb] |
101 | | - OctocatalogDiff::CatalogUtil::CachedMasterDirectory.run(options, logger) |
102 | | - end |
103 | | - |
104 | | - # bootstrap_then_exit is a special case that only prepares directories and does not |
105 | | - # depend on facts. This happens within the 'catalogs' object, since bootstrapping and |
106 | | - # preparing catalogs are tightly coupled operations. However this does not actually |
107 | | - # build catalogs. |
108 | | - if options[:bootstrap_then_exit] |
109 | | - catalogs_obj = OctocatalogDiff::Util::Catalogs.new(options, logger) |
110 | | - return bootstrap_then_exit(logger, catalogs_obj) |
111 | | - end |
112 | | - |
113 | | - # Compile catalogs and do catalog-diff |
114 | | - catalog_diff = OctocatalogDiff::API::V1::CatalogDiff.catalog_diff(options.merge(logger: logger)) |
115 | | - |
116 | | - # Display diffs |
117 | | - printer_obj = OctocatalogDiff::Cli::Printer.new(options, logger) |
118 | | - printer_obj.printer(catalog_diff.diffs, catalog_diff.from.compilation_dir, catalog_diff.to.compilation_dir) |
119 | | - |
120 | | - # Return the diff object if requested (generally for testing) or otherwise return exit code |
121 | | - return catalog_diff.diffs if opts[:RETURN_DIFFS] |
122 | | - catalog_diff.diffs.any? ? EXITCODE_SUCCESS_WITH_DIFFS : EXITCODE_SUCCESS_NO_DIFFS |
| 18 | + # This is the CLI for catalog-diff. It's responsible for parsing the command line |
| 19 | + # arguments and then handing off to appropriate methods to perform the catalog-diff. |
| 20 | + class Cli |
| 21 | + # Version number |
| 22 | + VERSION = OctocatalogDiff::Version::VERSION |
| 23 | + |
| 24 | + # Exit codes |
| 25 | + EXITCODE_SUCCESS_NO_DIFFS = 0 |
| 26 | + EXITCODE_FAILURE = 1 |
| 27 | + EXITCODE_SUCCESS_WITH_DIFFS = 2 |
| 28 | + |
| 29 | + # The default type+title+attribute to ignore in catalog-diff. |
| 30 | + DEFAULT_IGNORES = [ |
| 31 | + { type: 'Class' } # Don't care about classes themselves, only what they actually do! |
| 32 | + ].freeze |
| 33 | + |
| 34 | + # The default options. |
| 35 | + DEFAULT_OPTIONS = { |
| 36 | + from_env: 'origin/master', |
| 37 | + to_env: '.', |
| 38 | + colors: true, |
| 39 | + debug: false, |
| 40 | + quiet: false, |
| 41 | + format: :color_text, |
| 42 | + display_source_file_line: false, |
| 43 | + compare_file_text: true, |
| 44 | + display_datatype_changes: true, |
| 45 | + parallel: true, |
| 46 | + suppress_absent_file_details: true, |
| 47 | + hiera_path: 'hieradata' |
| 48 | + }.freeze |
| 49 | + |
| 50 | + # This method is the one to call externally. It is possible to specify alternate |
| 51 | + # command line arguments, for testing. |
| 52 | + # @param argv [Array] Use specified arguments (defaults to ARGV) |
| 53 | + # @param logger [Logger] Logger object |
| 54 | + # @param opts [Hash] Additional options |
| 55 | + # @return [Fixnum] Exit code: 0=no diffs, 1=something went wrong, 2=worked but there are diffs |
| 56 | + def self.cli(argv = ARGV, logger = Logger.new(STDERR), opts = {}) |
| 57 | + # Save a copy of argv to print out later in debugging |
| 58 | + argv_save = argv.dup |
| 59 | + |
| 60 | + # Are there additional ARGV to munge, e.g. that have been supplied in the options from a |
| 61 | + # configuration file? |
| 62 | + if opts.key?(:additional_argv) |
| 63 | + raise ArgumentError, ':additional_argv must be array!' unless opts[:additional_argv].is_a?(Array) |
| 64 | + argv.concat opts[:additional_argv] |
123 | 65 | end |
124 | 66 |
|
125 | | - # Parse command line options with 'optparse'. Returns a hash with the parsed arguments. |
126 | | - # @param argv [Array] Command line arguments (MUST be specified) |
127 | | - # @return [Hash] Options |
128 | | - def self.parse_opts(argv) |
129 | | - options = { ignore: DEFAULT_IGNORES.dup } |
130 | | - Options.parse_options(argv, options) |
| 67 | + # Parse command line |
| 68 | + options = parse_opts(argv) |
| 69 | + |
| 70 | + # Additional options from hard-coded specified options. These are only processed if |
| 71 | + # there are not already values defined from command line options. |
| 72 | + # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false) |
| 73 | + # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist. |
| 74 | + opts.each { |k, v| options[k] = v unless options.key?(k) } |
| 75 | + veto_options = %w(enc header hiera_config include_tags) |
| 76 | + veto_options.each { |x| options.delete(x.to_sym) if options["no_#{x}".to_sym] } |
| 77 | + options[:ignore].concat opts.fetch(:additional_ignores, []) |
| 78 | + |
| 79 | + # Incorporate default options where needed. |
| 80 | + # Note: do NOT use 'options[k] ||= v' here because if the value of options[k] is boolean(false) |
| 81 | + # it will then be overridden. Whereas the intent is to define values only for those keys that don't exist. |
| 82 | + DEFAULT_OPTIONS.each { |k, v| options[k] = v unless options.key?(k) } |
| 83 | + veto_with_none_options = %w(hiera_path hiera_path_strip) |
| 84 | + veto_with_none_options.each { |x| options.delete(x.to_sym) if options[x.to_sym] == :none } |
| 85 | + |
| 86 | + # Fact overrides come in here - 'options' is modified |
| 87 | + setup_fact_overrides(options) |
| 88 | + |
| 89 | + # Configure the logger and logger.debug initial information |
| 90 | + # 'logger' is modified and used |
| 91 | + setup_logger(logger, options, argv_save) |
| 92 | + |
| 93 | + # --catalog-only is a special case that compiles the catalog for the "to" branch |
| 94 | + # and then exits, without doing any 'diff' whatsoever. Support that option. |
| 95 | + return catalog_only(logger, options) if options[:catalog_only] |
| 96 | + |
| 97 | + # Set up the cached master directory - maintain it, adjust options if needed. However, if we |
| 98 | + # are getting the 'from' catalog from PuppetDB, then don't do this. |
| 99 | + unless options[:cached_master_dir].nil? || options[:from_puppetdb] |
| 100 | + OctocatalogDiff::CatalogUtil::CachedMasterDirectory.run(options, logger) |
131 | 101 | end |
132 | 102 |
|
133 | | - # Fact overrides come in here |
134 | | - def self.setup_fact_overrides(options) |
135 | | - [:from_fact_override, :to_fact_override].each do |key| |
136 | | - o = options["#{key}_in".to_sym] |
137 | | - next unless o.is_a?(Array) |
138 | | - next unless o.any? |
139 | | - options[key] ||= [] |
140 | | - options[key].concat o.map { |x| OctocatalogDiff::Cli::Helpers::FactOverride.new(x) } |
141 | | - end |
| 103 | + # bootstrap_then_exit is a special case that only prepares directories and does not |
| 104 | + # depend on facts. This happens within the 'catalogs' object, since bootstrapping and |
| 105 | + # preparing catalogs are tightly coupled operations. However this does not actually |
| 106 | + # build catalogs. |
| 107 | + if options[:bootstrap_then_exit] |
| 108 | + catalogs_obj = OctocatalogDiff::Util::Catalogs.new(options, logger) |
| 109 | + return bootstrap_then_exit(logger, catalogs_obj) |
142 | 110 | end |
143 | 111 |
|
144 | | - # Helper method: Configure and setup logger |
145 | | - def self.setup_logger(logger, options, argv_save) |
146 | | - # Configure the logger |
147 | | - logger.level = Logger::INFO |
148 | | - logger.level = Logger::DEBUG if options[:debug] |
149 | | - logger.level = Logger::ERROR if options[:quiet] |
150 | | - |
151 | | - # Some debugging information up front |
152 | | - version_display = ENV['OCTOCATALOG_DIFF_CUSTOM_VERSION'] || VERSION |
153 | | - logger.debug "Running octocatalog-diff #{version_display} with ruby #{RUBY_VERSION}" |
154 | | - logger.debug "Command line arguments: #{argv_save.inspect}" |
155 | | - logger.debug "Running on host #{Socket.gethostname} (#{RUBY_PLATFORM})" |
156 | | - end |
| 112 | + # Compile catalogs and do catalog-diff |
| 113 | + catalog_diff = OctocatalogDiff::API::V1::CatalogDiff.catalog_diff(options.merge(logger: logger)) |
| 114 | + |
| 115 | + # Display diffs |
| 116 | + printer_obj = OctocatalogDiff::Cli::Printer.new(options, logger) |
| 117 | + printer_obj.printer(catalog_diff.diffs, catalog_diff.from.compilation_dir, catalog_diff.to.compilation_dir) |
| 118 | + |
| 119 | + # Return the diff object if requested (generally for testing) or otherwise return exit code |
| 120 | + return catalog_diff.diffs if opts[:RETURN_DIFFS] |
| 121 | + catalog_diff.diffs.any? ? EXITCODE_SUCCESS_WITH_DIFFS : EXITCODE_SUCCESS_NO_DIFFS |
| 122 | + end |
| 123 | + |
| 124 | + # Parse command line options with 'optparse'. Returns a hash with the parsed arguments. |
| 125 | + # @param argv [Array] Command line arguments (MUST be specified) |
| 126 | + # @return [Hash] Options |
| 127 | + def self.parse_opts(argv) |
| 128 | + options = { ignore: DEFAULT_IGNORES.dup } |
| 129 | + Options.parse_options(argv, options) |
| 130 | + end |
157 | 131 |
|
158 | | - # Compile the catalog only |
159 | | - def self.catalog_only(logger, options) |
160 | | - opts = options.merge(logger: logger) |
161 | | - to_catalog = OctocatalogDiff::API::V1::CatalogCompile.catalog(opts) |
162 | | - |
163 | | - # If the catalog compilation failed, an exception would have been thrown. So if |
164 | | - # we get here, the catalog succeeded. Dump the catalog to the appropriate place |
165 | | - # and exit successfully. |
166 | | - if options[:output_file] |
167 | | - File.open(options[:output_file], 'w') { |f| f.write(to_catalog.catalog_json) } |
168 | | - logger.info "Wrote catalog to #{options[:output_file]}" |
169 | | - else |
170 | | - puts to_catalog.catalog_json |
171 | | - end |
172 | | - return [OctocatalogDiff::Catalog.new(backend: :noop), to_catalog] if options[:RETURN_DIFFS] # For integration testing |
173 | | - EXITCODE_SUCCESS_NO_DIFFS |
| 132 | + # Fact overrides come in here |
| 133 | + def self.setup_fact_overrides(options) |
| 134 | + [:from_fact_override, :to_fact_override].each do |key| |
| 135 | + o = options["#{key}_in".to_sym] |
| 136 | + next unless o.is_a?(Array) |
| 137 | + next unless o.any? |
| 138 | + options[key] ||= [] |
| 139 | + options[key].concat o.map { |x| OctocatalogDiff::Cli::Helpers::FactOverride.new(x) } |
174 | 140 | end |
| 141 | + end |
| 142 | + |
| 143 | + # Helper method: Configure and setup logger |
| 144 | + def self.setup_logger(logger, options, argv_save) |
| 145 | + # Configure the logger |
| 146 | + logger.level = Logger::INFO |
| 147 | + logger.level = Logger::DEBUG if options[:debug] |
| 148 | + logger.level = Logger::ERROR if options[:quiet] |
| 149 | + |
| 150 | + # Some debugging information up front |
| 151 | + version_display = ENV['OCTOCATALOG_DIFF_CUSTOM_VERSION'] || VERSION |
| 152 | + logger.debug "Running octocatalog-diff #{version_display} with ruby #{RUBY_VERSION}" |
| 153 | + logger.debug "Command line arguments: #{argv_save.inspect}" |
| 154 | + logger.debug "Running on host #{Socket.gethostname} (#{RUBY_PLATFORM})" |
| 155 | + end |
175 | 156 |
|
176 | | - # --bootstrap-then-exit command |
177 | | - def self.bootstrap_then_exit(logger, catalogs_obj) |
178 | | - catalogs_obj.bootstrap_then_exit |
179 | | - return EXITCODE_SUCCESS_NO_DIFFS |
180 | | - rescue OctocatalogDiff::Util::Catalogs::BootstrapError => exc |
181 | | - logger.fatal("--bootstrap-then-exit error: bootstrap failed (#{exc})") |
182 | | - return EXITCODE_FAILURE |
| 157 | + # Compile the catalog only |
| 158 | + def self.catalog_only(logger, options) |
| 159 | + opts = options.merge(logger: logger) |
| 160 | + to_catalog = OctocatalogDiff::API::V1::CatalogCompile.catalog(opts) |
| 161 | + |
| 162 | + # If the catalog compilation failed, an exception would have been thrown. So if |
| 163 | + # we get here, the catalog succeeded. Dump the catalog to the appropriate place |
| 164 | + # and exit successfully. |
| 165 | + if options[:output_file] |
| 166 | + File.open(options[:output_file], 'w') { |f| f.write(to_catalog.catalog_json) } |
| 167 | + logger.info "Wrote catalog to #{options[:output_file]}" |
| 168 | + else |
| 169 | + puts to_catalog.catalog_json |
183 | 170 | end |
| 171 | + return [OctocatalogDiff::Catalog.new(backend: :noop), to_catalog] if options[:RETURN_DIFFS] # For integration testing |
| 172 | + EXITCODE_SUCCESS_NO_DIFFS |
| 173 | + end |
| 174 | + |
| 175 | + # --bootstrap-then-exit command |
| 176 | + def self.bootstrap_then_exit(logger, catalogs_obj) |
| 177 | + catalogs_obj.bootstrap_then_exit |
| 178 | + return EXITCODE_SUCCESS_NO_DIFFS |
| 179 | + rescue OctocatalogDiff::Util::Catalogs::BootstrapError => exc |
| 180 | + logger.fatal("--bootstrap-then-exit error: bootstrap failed (#{exc})") |
| 181 | + return EXITCODE_FAILURE |
184 | 182 | end |
185 | 183 | end |
186 | 184 | end |
0 commit comments