octocatalog-diff 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/.version +1 -0
  3. data/LICENSE +20 -0
  4. data/README.md +82 -0
  5. data/bin/octocatalog-diff +75 -0
  6. data/doc/advanced-bootstrap.md +33 -0
  7. data/doc/advanced-cache-dir.md +24 -0
  8. data/doc/advanced-catalog-only.md +37 -0
  9. data/doc/advanced-ci.md +13 -0
  10. data/doc/advanced-dynamic-ignores.md +123 -0
  11. data/doc/advanced-future-parser.md +11 -0
  12. data/doc/advanced-ignores.md +224 -0
  13. data/doc/advanced-output-formats.md +96 -0
  14. data/doc/advanced-output-hacks.md +45 -0
  15. data/doc/advanced-override-facts.md +67 -0
  16. data/doc/advanced-pe-enc.md +52 -0
  17. data/doc/advanced-puppet-master.md +50 -0
  18. data/doc/advanced-puppet-versions.md +9 -0
  19. data/doc/advanced-storeconfigs.md +72 -0
  20. data/doc/advanced-using-without-git.md +15 -0
  21. data/doc/advanced.md +43 -0
  22. data/doc/basic.md +70 -0
  23. data/doc/configuration-enc.md +69 -0
  24. data/doc/configuration-hiera.md +103 -0
  25. data/doc/configuration-puppetdb.md +49 -0
  26. data/doc/configuration.md +51 -0
  27. data/doc/dev/README.md +1 -0
  28. data/doc/dev/coverage.md +34 -0
  29. data/doc/dev/how-to-add-options.md +83 -0
  30. data/doc/dev/integration-tests.md +63 -0
  31. data/doc/dev/releasing.md +19 -0
  32. data/doc/installation.md +49 -0
  33. data/doc/limitations.md +34 -0
  34. data/doc/optionsref.md +947 -0
  35. data/doc/requirements.md +16 -0
  36. data/doc/roadmap.md +26 -0
  37. data/doc/similar.md +17 -0
  38. data/doc/troubleshooting.md +54 -0
  39. data/lib/octocatalog-diff.rb +12 -0
  40. data/lib/octocatalog-diff/bootstrap.rb +53 -0
  41. data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
  42. data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
  43. data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
  44. data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
  45. data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
  46. data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
  47. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
  48. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
  49. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
  50. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
  51. data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
  52. data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
  53. data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
  54. data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
  55. data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
  56. data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
  57. data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
  58. data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
  59. data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
  60. data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
  61. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
  62. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
  63. data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
  64. data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
  65. data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
  66. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
  67. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
  68. data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
  69. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
  70. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
  71. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
  72. data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
  73. data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
  74. data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
  75. data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
  76. data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
  77. data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
  78. data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
  79. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
  80. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
  81. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
  82. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
  83. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
  84. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
  85. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
  86. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
  87. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
  88. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
  89. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
  90. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
  91. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
  92. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
  93. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
  94. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
  95. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
  96. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
  97. data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
  98. data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
  99. data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
  100. data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
  101. data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
  102. data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
  103. data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
  104. data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
  105. data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
  106. data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
  107. data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
  108. data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
  109. data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
  110. data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
  111. data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
  112. data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
  113. data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
  114. data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
  115. data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
  116. data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
  117. data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
  118. data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
  119. data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
  120. data/lib/octocatalog-diff/catalog.rb +209 -0
  121. data/lib/octocatalog-diff/catalog/computed.rb +205 -0
  122. data/lib/octocatalog-diff/catalog/json.rb +30 -0
  123. data/lib/octocatalog-diff/catalog/noop.rb +19 -0
  124. data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
  125. data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
  126. data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
  127. data/lib/octocatalog-diff/external/pson/README.md +20 -0
  128. data/lib/octocatalog-diff/external/pson/common.rb +370 -0
  129. data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
  130. data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
  131. data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
  132. data/lib/octocatalog-diff/external/pson/version.rb +8 -0
  133. data/lib/octocatalog-diff/facts.rb +125 -0
  134. data/lib/octocatalog-diff/facts/json.rb +20 -0
  135. data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
  136. data/lib/octocatalog-diff/facts/yaml.rb +29 -0
  137. data/lib/octocatalog-diff/puppetdb.rb +163 -0
  138. data/lib/octocatalog-diff/util/colored.rb +20 -0
  139. data/lib/octocatalog-diff/util/httparty.rb +158 -0
  140. data/lib/octocatalog-diff/util/parallel.rb +170 -0
  141. data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
  142. data/lib/octocatalog-diff/version.rb +7 -0
  143. metadata +386 -0
@@ -0,0 +1,145 @@
1
+ require_relative '../differ'
2
+
3
+ module OctocatalogDiff
4
+ module CatalogDiff
5
+ class Cli
6
+ # Wrapper around OctocatalogDiff::CatalogDiff::Differ to provide the logger object, set up
7
+ # ignores, and add additional ignores for items dependent upon the compilation directory.
8
+ class Diffs
9
+ # Constructor
10
+ # @param options [Hash] Options from cli/options
11
+ # @param logger [Logger] Logger object
12
+ def initialize(options, logger)
13
+ @options = options
14
+ @logger = logger
15
+ end
16
+
17
+ # The method to call externally, passing in the catalogs as a hash (see parameter). This
18
+ # sets up options and ignores and then actually performs the diffs. The result is the array
19
+ # of diffs.
20
+ # @param catalogs [Hash] { :to => OctocatalogDiff::Catalog, :from => OctocatalogDiff::Catalog }
21
+ # @return [Array<diffs>] Array of diffs
22
+ def diffs(catalogs)
23
+ @logger.debug 'Begin compute diffs between catalogs'
24
+ diff_opts = @options.merge(logger: @logger)
25
+
26
+ # Construct the actual differ object that the present one wraps
27
+ differ = OctocatalogDiff::CatalogDiff::Differ.new(diff_opts, catalogs[:from], catalogs[:to])
28
+ differ.ignore(attr: 'tags') unless @options.fetch(:include_tags, false)
29
+ differ.ignore(@options.fetch(:ignore, []))
30
+
31
+ # Handle --ignore-tags option, the ability to tag resources within modules/manifests and
32
+ # have catalog-diff ignore them.
33
+ if @options[:ignore_tags].is_a?(Array) && @options[:ignore_tags].any?
34
+ # Go through the "to" catalog and identify any resources that have been tagged with one or more
35
+ # specified "ignore tags." Add any such items to the ignore list. The 'to' catalog has the authoritative
36
+ # list of dynamic ignores.
37
+ catalogs[:to].resources.each do |resource|
38
+ next unless tagged_for_ignore?(resource)
39
+ differ.ignore(type: resource['type'], title: resource['title'])
40
+ @logger.debug "Ignoring type='#{resource['type']}', title='#{resource['title']}' based on tag in to-catalog"
41
+ end
42
+
43
+ # Go through the "from" catalog and identify any resources that have been tagged with one or more
44
+ # specified "ignore tags." Only mark the resources for ignoring if they do not appear in the 'to'
45
+ # catalog, thereby allowing the 'to' catalog to be the authoritative ignore list. This allows deleted
46
+ # items that were previously ignored to continue to be ignored.
47
+ catalogs[:from].resources.each do |resource|
48
+ next if catalogs[:to].resource(type: resource['type'], title: resource['title'])
49
+ next unless tagged_for_ignore?(resource)
50
+ differ.ignore(type: resource['type'], title: resource['title'])
51
+ @logger.debug "Ignoring type='#{resource['type']}', title='#{resource['title']}' based on tag in from-catalog"
52
+ end
53
+ end
54
+
55
+ # Actually perform the diff
56
+ diff_result = differ.diff
57
+ diff_result.delete_if { |element| change_due_to_compilation_dir?(element, catalogs) }
58
+ @logger.debug 'Success compute diffs between catalogs'
59
+ diff_result
60
+ end
61
+
62
+ # Catch anything that explictly changed as a result of different compilation directories and
63
+ # warn about it. These are probably things that should be refactored. For now we're going to pull
64
+ # these out after the fact so we can warn about them if they do show up.
65
+ # @param change [Array(diff format)] Change in diff format
66
+ # @param catalogs [Hash] { :to => OctocatalogDiff::Catalog, :from => OctocatalogDiff::Catalog }
67
+ # @return [Boolean] True if change includes compilation directory, false otherwise
68
+ def change_due_to_compilation_dir?(change, catalogs)
69
+ dir1 = catalogs.fetch(:to).compilation_dir
70
+ dir2 = catalogs.fetch(:from).compilation_dir
71
+ return false if dir1.nil? || dir2.nil?
72
+
73
+ dir1_rexp = Regexp.escape(dir1)
74
+ dir2_rexp = Regexp.escape(dir2)
75
+ dir = "(?:#{dir1_rexp}|#{dir2_rexp})"
76
+
77
+ # Check for added/removed resources where the title of the resource includes the compilation directory
78
+ if change[0] == '+' || change[0] == '-'
79
+ if change[1] =~ /^([^\f]+)\f([^\f]*#{dir}[^\f]*)/
80
+ message = "Resource #{Regexp.last_match(1)}[#{Regexp.last_match(2)}]"
81
+ message += ' appears to depend on catalog compilation directory. Suppressed from results.'
82
+ @logger.warn message
83
+ return true
84
+ end
85
+ end
86
+
87
+ # Check for a change where the difference in a parameter exactly corresponds to the difference in the
88
+ # compilation directory.
89
+ if change[0] == '~' || change[0] == '!'
90
+ from_before = nil
91
+ from_after = nil
92
+ from_match = false
93
+ to_before = nil
94
+ to_after = nil
95
+ to_match = false
96
+
97
+ if change[2] =~ /^(.*)#{dir2}(.*)$/m
98
+ from_before = Regexp.last_match(1) || ''
99
+ from_after = Regexp.last_match(2) || ''
100
+ from_match = true
101
+ end
102
+
103
+ if change[3] =~ /^(.*)#{dir1}(.*)$/m
104
+ to_before = Regexp.last_match(1) || ''
105
+ to_after = Regexp.last_match(2) || ''
106
+ to_match = true
107
+ end
108
+
109
+ if from_match && to_match && to_before == from_before && to_after == from_after
110
+ message = "Resource key #{change[1].gsub(/\f/, ' => ')}"
111
+ message += ' appears to depend on catalog compilation directory. Suppressed from results.'
112
+ @logger.warn message
113
+ return true
114
+ end
115
+
116
+ if from_match || to_match
117
+ message = "Resource key #{change[1].gsub(/\f/, ' => ')}"
118
+ message += ' may depend on catalog compilation directory, but there may be differences.'
119
+ message += ' This is included in results for now, but please verify.'
120
+ @logger.warn message
121
+ end
122
+ end
123
+
124
+ false
125
+ end
126
+
127
+ private
128
+
129
+ # Determine if a resource is tagged with any ignore-tag.
130
+ # @param resource [Hash] The resource
131
+ # @return [Boolean] true if tagged for ignore, false if not
132
+ def tagged_for_ignore?(resource)
133
+ return false unless @options[:ignore_tags].is_a?(Array)
134
+ return false unless resource.key?('tags') && resource['tags'].is_a?(Array)
135
+ @options[:ignore_tags].each do |tag|
136
+ # tag_with_type will be like: 'ignored_catalog_diff__mymodule__mytype'
137
+ tag_with_type = [tag, resource['type'].downcase.gsub(/\W/, '_')].join('__')
138
+ return true if resource['tags'].include?(tag) || resource['tags'].include?(tag_with_type)
139
+ end
140
+ false
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,99 @@
1
+ require 'json'
2
+
3
+ module OctocatalogDiff
4
+ module CatalogDiff
5
+ class Cli
6
+ class Helpers
7
+ # Since input from the command line is in the format of a string, this class helps to guess
8
+ # at the data type.
9
+ class FactOverride
10
+ # Accessors
11
+ attr_reader :key, :value
12
+
13
+ # Constructor: Input will be a string (since it comes from command line).
14
+ # This code will make a best guess at the data type (or use a supplied data type if any).
15
+ # @param input [String] Input in the format: key=(data type)value
16
+ def initialize(input, key = nil)
17
+ # Normally the input will be a string in the format key=(data type)value where the data
18
+ # type is optional and the parentheses are literal. Example:
19
+ # foo=1 (auto-determine data type - in this case it would be a fixnum)
20
+ # foo=(fixnum)1 (will be a fixnum)
21
+ # foo=(string)1 (will be '1' the string)
22
+ # If input is not a string, we can still construct the object if the key is given.
23
+ # That input would come directly from code and not from the command line, since inputs
24
+ # from the command line are always strings.
25
+ if input.is_a?(String)
26
+ unless input.include?('=')
27
+ raise ArgumentError, "Fact override '#{input}' is not in 'key=(data type)value' format"
28
+ end
29
+ input.strip!
30
+ @key, raw_value = input.split('=', 2)
31
+ @value = parsed_value(raw_value)
32
+ elsif key.nil?
33
+ message = "Define a key when the input is not a string (#{input.class} => #{input.inspect})"
34
+ raise ArgumentError, message
35
+ else
36
+ @key = key
37
+ @value = input
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Guess the datatype from a particular input
44
+ # @param input [String] Input in string format
45
+ # @return [?] Output in appropriate format
46
+ def parsed_value(input)
47
+ # If data type is explicitly given
48
+ if input =~ /^\((\w+)\)(.*)$/m
49
+ datatype = Regexp.last_match(1)
50
+ value = Regexp.last_match(2)
51
+ return convert_to_data_type(datatype.downcase, value)
52
+ end
53
+
54
+ # Guess data type
55
+ return input.to_i if input =~ /^-?\d+$/
56
+ return input.to_f if input =~ /^-?\d*\.\d+$/
57
+ return true if input.casecmp('true').zero?
58
+ return false if input.casecmp('false').zero?
59
+ input
60
+ end
61
+
62
+ # Handle data type that's explicitly given
63
+ # @param datatype [String] Data type (as a string)
64
+ # @param value [String] Value given
65
+ # @return [?] Value converted to specified data type
66
+ def convert_to_data_type(datatype, value)
67
+ return value if datatype == 'string'
68
+ return parse_json(value) if datatype == 'json'
69
+ return nil if datatype == 'nil'
70
+ if datatype == 'fixnum'
71
+ return Regexp.last_match(1).to_i if value =~ /^(-?\d+)$/
72
+ raise ArgumentError, "Illegal fixnum '#{value}'"
73
+ end
74
+ if datatype == 'float'
75
+ return Regexp.last_match(1).to_f if value =~ /^(-?\d*\.\d+)$/
76
+ return Regexp.last_match(1).to_f if value =~ /^(-?\d+)$/
77
+ raise ArgumentError, "Illegal float '#{value}'"
78
+ end
79
+ if datatype == 'boolean'
80
+ return true if value.casecmp('true').zero?
81
+ return false if value.casecmp('false').zero?
82
+ raise ArgumentError, "Illegal boolean '#{value}'"
83
+ end
84
+ raise ArgumentError, "Unknown data type '#{datatype}'"
85
+ end
86
+
87
+ # Parse JSON value
88
+ # @param input [String] Input, hopefully in JSON format
89
+ # @return [?] Output data structure
90
+ def parse_json(input)
91
+ JSON.parse(input)
92
+ rescue JSON::ParserError => exc
93
+ raise JSON::ParserError, "Failed to parse JSON: input=#{input} error=#{exc}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,173 @@
1
+ require_relative '../cli'
2
+ require_relative '../../facts'
3
+ require_relative '../../version'
4
+
5
+ require 'optparse'
6
+
7
+ module OctocatalogDiff
8
+ module CatalogDiff
9
+ class Cli
10
+ # This class contains the option parser. 'parse_options' is the external entry point.
11
+ class Options
12
+ # The usage banner.
13
+ BANNER = 'Usage: catalog-diff -n <hostname> [-f <from environment>] [-t <to environment>]'.freeze
14
+
15
+ # List of classes
16
+ def self.classes
17
+ @classes ||= []
18
+ end
19
+
20
+ # Define the Option class and newoption() method for use by catalog-diff/cli/options/*.rb files
21
+ class Option
22
+ DEFAULT_WEIGHT = 999
23
+ def self.has_weight(w) # rubocop:disable Style/PredicateName
24
+ @weight = w
25
+ end
26
+
27
+ def self.weight
28
+ @weight || DEFAULT_WEIGHT
29
+ end
30
+
31
+ def self.newoption(name, &block)
32
+ klass = Class.new(OctocatalogDiff::CatalogDiff::Cli::Options::Option)
33
+ klass.const_set('NAME', name)
34
+ klass.class_exec(&block)
35
+ Options.classes.push(klass)
36
+ end
37
+ end
38
+
39
+ # Method to call all of the other methods in this class. Except in very specific circumstances,
40
+ # this should be the method called from outside of this class.
41
+ # @param argv [Array] Array of command line arguments
42
+ # @param defaults [Hash] Default values
43
+ # @return [Hash] Parsed options
44
+ def self.parse_options(argv, defaults = {})
45
+ options = defaults.dup
46
+ Options.classes.clear
47
+ ::OptionParser.new do |parser|
48
+ parser.banner = "#{BANNER}\n\n"
49
+ option_classes.each do |klass|
50
+ obj = klass.new
51
+ obj.parse(parser, options)
52
+ end
53
+ parser.on_tail('-v', '--version', 'Show version information about this program and quit.') do
54
+ puts "octocatalog-diff #{OctocatalogDiff::Version::VERSION}"
55
+ exit
56
+ end
57
+ end.parse! argv
58
+ options
59
+ end
60
+
61
+ # Read in *.rb files in the 'options' directory and create classes from them.
62
+ # Sort the classes according to weight and return the list of sorted classes.
63
+ # @return [Array<Class>] Sorted classes
64
+ def self.option_classes
65
+ files = Dir.glob(File.join(File.dirname(__FILE__), 'options', '*.rb'))
66
+ files.each { |file| load file } # Populates self.classes
67
+ classes.sort { |a, b| a.weight <=> b.weight }
68
+ end
69
+
70
+ # Sets up options that can be defined globally or for just one branch. For example, with a
71
+ # CLI name of 'puppet-binary' this will acknowledge 3 options: --puppet-binary (global),
72
+ # --from-puppet-binary (for the from branch only), and --to-puppet-binary (for the to branch
73
+ # only). The only options that will be created are the 'to' and 'from' variants, but the global
74
+ # option will populate any of the 'to' and 'from' variants that are missing.
75
+ # @param :datatype [?] Expected data type
76
+ def self.option_globally_or_per_branch(opts = {})
77
+ datatype = opts.fetch(:datatype, '')
78
+ return option_globally_or_per_branch_string(opts) if datatype.is_a?(String)
79
+ return option_globally_or_per_branch_array(opts) if datatype.is_a?(Array)
80
+ raise ArgumentError, "option_globally_or_per_branch not equipped to handle #{datatype.class}"
81
+ end
82
+
83
+ # See description of `option_globally_or_per_branch`. This implements the logic for a string value.
84
+ # @param :parser [OptionParser object] The OptionParser argument
85
+ # @param :options [Hash] Options hash being constructed; this is modified in this method.
86
+ # @param :cli_name [String] Name of option on command line (e.g. puppet-binary)
87
+ # @param :option_name [Symbol] Name of option in the options hash (e.g. :puppet_binary)
88
+ # @param :desc [String] Description of option on the command line; will have "for the XX branch" appended
89
+ def self.option_globally_or_per_branch_string(opts)
90
+ parser = opts.fetch(:parser)
91
+ options = opts.fetch(:options)
92
+ cli_name = opts.fetch(:cli_name)
93
+ option_name = opts.fetch(:option_name)
94
+ desc = opts.fetch(:desc)
95
+
96
+ flag = "#{cli_name} STRING"
97
+ from_option = "from_#{option_name}".to_sym
98
+ to_option = "to_#{option_name}".to_sym
99
+ parser.on("--#{flag}", "#{desc} globally") do |x|
100
+ validate_option(opts[:validator], x) if opts[:validator]
101
+ translated = translate_option(opts[:translator], x)
102
+ options[to_option] ||= translated
103
+ options[from_option] ||= translated
104
+ end
105
+ parser.on("--to-#{flag}", "#{desc} for the to branch") do |x|
106
+ validate_option(opts[:validator], x) if opts[:validator]
107
+ options[to_option] = translate_option(opts[:translator], x)
108
+ end
109
+ parser.on("--from-#{flag}", "#{desc} for the from branch") do |x|
110
+ validate_option(opts[:validator], x) if opts[:validator]
111
+ options[from_option] = translate_option(opts[:translator], x)
112
+ end
113
+ end
114
+
115
+ # See description of `option_globally_or_per_branch`. This implements the logic for an array.
116
+ # @param :parser [OptionParser object] The OptionParser argument
117
+ # @param :options [Hash] Options hash being constructed; this is modified in this method.
118
+ # @param :cli_name [String] Name of option on command line (e.g. puppet-binary)
119
+ # @param :option_name [Symbol] Name of option in the options hash (e.g. :puppet_binary)
120
+ # @param :desc [String] Description of option on the command line; will have "for the XX branch" appended
121
+ def self.option_globally_or_per_branch_array(opts = {})
122
+ parser = opts.fetch(:parser)
123
+ options = opts.fetch(:options)
124
+ cli_name = opts.fetch(:cli_name)
125
+ option_name = opts.fetch(:option_name)
126
+ desc = opts.fetch(:desc)
127
+
128
+ flag = "#{cli_name} STRING1[,STRING2[,...]]"
129
+ from_option = "from_#{option_name}".to_sym
130
+ to_option = "to_#{option_name}".to_sym
131
+ parser.on("--#{flag}", Array, "#{desc} globally") do |x|
132
+ validate_option(opts[:validator], x) if opts[:validator]
133
+ translated = translate_option(opts[:translator], x)
134
+ options[to_option] ||= []
135
+ options[to_option].concat translated
136
+ options[from_option] ||= []
137
+ options[from_option].concat translated
138
+ end
139
+ parser.on("--to-#{flag}", Array, "#{desc} for the to branch") do |x|
140
+ validate_option(opts[:validator], x) if opts[:validator]
141
+ options[to_option] ||= []
142
+ options[to_option].concat translate_option(opts[:translator], x)
143
+ end
144
+ parser.on("--from-#{flag}", Array, "#{desc} for the from branch") do |x|
145
+ validate_option(opts[:validator], x) if opts[:validator]
146
+ options[from_option] ||= []
147
+ options[from_option].concat translate_option(opts[:translator], x)
148
+ end
149
+ end
150
+
151
+ # If a validator was provided, run the validator on the supplied value. The validator is expected to
152
+ # throw an error if there is a problem. Note that the validator runs *before* the translator if both
153
+ # a validator and translator are supplied.
154
+ # @param validator [Code] Validation function
155
+ # @param value [?] Value to validate (typically a String but can really be anything)
156
+ def self.validate_option(validator, value)
157
+ validator.call(value)
158
+ end
159
+
160
+ # If a translator was provided, run the translator on the supplied value. The translator is expected
161
+ # to return the data type needed for the option (typically a String but can really be anything). Note
162
+ # that the translator runs *after* the validator if both a validator and translator are supplied.
163
+ # @param translator [Code] Translator function
164
+ # @param value [?] Original input value
165
+ # @return [?] Translated value
166
+ def self.translate_option(translator, value)
167
+ return value if translator.nil?
168
+ translator.call(value)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,14 @@
1
+ # Option to set the base checkout directory of puppet repository
2
+ # @param parser [OptionParser object] The OptionParser argument
3
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
4
+ OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:basedir) do
5
+ has_weight 10
6
+
7
+ def parse(parser, options)
8
+ parser.on('--basedir DIRNAME', 'Use an alternate base directory (git checkout of puppet repository)') do |dir|
9
+ path = File.absolute_path(dir)
10
+ raise Errno::ENOENT, 'Invalid basedir provided' unless Dir.exist?(path)
11
+ options[:basedir] = path
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # Allow the bootstrap environment to be set up via the command line.
2
+ # @param parser [OptionParser object] The OptionParser argument
3
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
4
+ OctocatalogDiff::CatalogDiff::Cli::Options::Option.newoption(:bootstrap_environment) do
5
+ has_weight 50
6
+
7
+ def parse(parser, options)
8
+ descriptive_text = 'Bootstrap script environment variables in key=value format'
9
+ parser.on('--bootstrap-environment "key1=val1,key2=val2,..."', Array, descriptive_text) do |res|
10
+ options[:bootstrap_environment] ||= {}
11
+ res.each do |item|
12
+ raise ArgumentError, "Bootstrap environment #{item} must be in key=value format!" unless item =~ /=/
13
+ key, val = item.split(/=/, 2)
14
+ options[:bootstrap_environment][key] = Regexp.last_match(1) if val.strip =~ /^['"]?(.+?)['"]?$/
15
+ end
16
+ end
17
+ end
18
+ end