octocatalog-diff 1.1.0 → 1.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 86467134c489b13251760bd9ffd23caced1c5222
4
- data.tar.gz: 8260b43c4e844b2224dd491018feb8a1da65c4ed
3
+ metadata.gz: 75c0856d744b0c909315a69aceb6e2d5e416991b
4
+ data.tar.gz: 127ac1617cc437a73d716ff68a0eea0623e62c8b
5
5
  SHA512:
6
- metadata.gz: fedd421520a95d4147878d50510d7f2f149cfcba0933c75d7f88c5f3efbeb45f6105b9c5bab53cbc46cf7234db1987cba0b9bdc327cd0b418ce8f69365723282
7
- data.tar.gz: 1bc344f71b92ef5154253e4b115f7916e59a9ee49c21c7344adb80ad7dd97d2f164fb951016e8a22d95ba564eab1dbc27a9c1b0cff8a014d327bfde881fb5d21
6
+ metadata.gz: a4a873ed14e553d8362267e88fdac5eeaf46e5929275601871048959805f93515af9f8d8152115e49f1c15ddf17fc38fa70158b1e001be71d20a0646f05c422e
7
+ data.tar.gz: 7c3f9a881d1e2b9d8669c96d6ffbb9953a25b8ff2c63f2a9c3151201c09be9ee95c6182b040370b08a54720c830e98f701cbe3f903c7b876ae80ec140b08a3b0
data/.version CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0
data/doc/CHANGELOG.md CHANGED
@@ -8,6 +8,16 @@
8
8
  </tr>
9
9
  </thead><tbody>
10
10
  <tr valign=top>
11
+ <td>1.2.0</td>
12
+ <td>2017-05-18</td>
13
+ <td>
14
+ <li><a href="https://github.com/github/octocatalog-diff/pull/112">#112</a>: Split arguments added for ENC</li>
15
+ <li><a href="https://github.com/github/octocatalog-diff/pull/113">#113</a>: (Enhancement) Override facts and ENC parameters using regular expressions</li>
16
+ <li><a href="https://github.com/github/octocatalog-diff/pull/103">#111</a>: Simplify parallel processing to solve some intermittent failures</li>
17
+ <li><a href="https://github.com/github/octocatalog-diff/pull/110">#110</a>: Ruby 2.4 compatibility</li>
18
+ </td>
19
+ </tr>
20
+ <tr valign=top>
11
21
  <td>1.1.0</td>
12
22
  <td>2017-05-08</td>
13
23
  <td>
@@ -65,3 +65,16 @@ The following data types in parentheses are supported:
65
65
  | `(json)` | Treat the input as a JSON string (calls `JSON.parse` in ruby) |
66
66
  | `(boolean)` | Treat the input as a boolean -- it must be `true` or `false`, case-insensitive |
67
67
  | `(nil)` | Ignore any characters after `(nil)` and deletes the fact if the fact exists |
68
+
69
+ ## Regular expressions
70
+
71
+ If you wish to match multiple facts by pattern, specify the regular expression in place of the key name. For example:
72
+
73
+ ```
74
+ octocatalog-diff -n some-node.example.com -f master -t master \
75
+ --to-fact-override /^ipaddress/=10.11.12.13
76
+ ```
77
+
78
+ In this example, `$::ipaddress`, `$::ipaddress_eth0`, `$::ipaddress_bond0`, and any other facts starting with "ipaddress" would be overridden. However, a fact named `$::additional_ipaddress` would not be overridden, because it does not match the regular expression.
79
+
80
+ Please note that you cannot *add* a fact with a regular expression -- when using regular expressions you can only modify or delete facts.
@@ -22,7 +22,7 @@ describe 'whatever behavior' do
22
22
  # @result[:logs] is a String containing everything printed to STDERR (Logger)
23
23
  # @result[:output] is a String containing everything printed to STDOUT
24
24
  # @result[:diffs] is an Array of differences
25
- # @result[:exitcode] is a Fixnum representing the exit code: 0 = no changes, 1 = failure, 2 = success, with changes
25
+ # @result[:exitcode] is an Integer representing the exit code: 0 = no changes, 1 = failure, 2 = success, with changes
26
26
  # @result[:exception] contains any exception that was thrown
27
27
  end
28
28
 
@@ -46,7 +46,7 @@ describe 'whatever behavior' do
46
46
  # @result[:logs] is a String containing everything printed to STDERR (Logger)
47
47
  # @result[:output] is a String containing everything printed to STDOUT
48
48
  # @result[:diffs] is an Array of differences
49
- # @result[:exitcode] is a Fixnum representing the exit code: 0 = no changes, 1 = failure, 2 = success, with changes
49
+ # @result[:exitcode] is an Integer representing the exit code: 0 = no changes, 1 = failure, 2 = success, with changes
50
50
  # @result[:exception] contains any exception that was thrown
51
51
  end
52
52
 
data/doc/requirements.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  To run `octocatalog-diff` you will need these basics:
4
4
 
5
- - Ruby 2.0 or higher
5
+ - Ruby 2.0 through 2.4 (we test octocatalog-diff with Ruby 2.0, 2.1, 2.2, 2.3, and 2.4)
6
6
  - Mac OS, Linux, or other Unix-line operating system (Windows is not supported)
7
7
  - Ability to install gems, e.g. with [rbenv](https://github.com/rbenv/rbenv) or [rvm](https://rvm.io/), or root privileges to install into the system Ruby
8
- - Puppet agent for [Linux](https://docs.puppet.com/puppet/latest/reference/install_linux.html) or [Mac OS X](https://docs.puppet.com/puppet/latest/reference/install_osx.html), or installed as a gem
8
+ - Puppet agent for [Linux](https://docs.puppet.com/puppet/latest/reference/install_linux.html) or [Mac OS X](https://docs.puppet.com/puppet/latest/reference/install_osx.html), or installed as a gem (we support Puppet 3.8.7 and all versions of Puppet 4.x)
9
9
 
10
10
  We recommend that you also have the following to get the most out of `octocatalog-diff`, but these are not absolute requirements:
11
11
 
@@ -13,7 +13,8 @@ module OctocatalogDiff
13
13
  # Constructor: Accepts a key and value.
14
14
  # @param input [Hash] Must contain :key and :value
15
15
  def initialize(input)
16
- @key = input.fetch(:key)
16
+ key = input.fetch(:key)
17
+ @key = key =~ %r{\A/(.+)/\Z} ? Regexp.new(Regexp.last_match(1)) : key
17
18
  @value = parsed_value(input.fetch(:value))
18
19
  end
19
20
 
@@ -29,6 +30,7 @@ module OctocatalogDiff
29
30
  # If input is not a string, we can still construct the object if the key is given.
30
31
  # That input would come directly from code and not from the command line, since inputs
31
32
  # from the command line are always strings.
33
+ # Also support regular expressions for the key name, if delimited by //.
32
34
  if key.nil? && input.is_a?(String)
33
35
  unless input.include?('=')
34
36
  raise ArgumentError, "Fact override '#{input}' is not in 'key=(data type)value' format"
@@ -72,9 +74,9 @@ module OctocatalogDiff
72
74
  return value if datatype == 'string'
73
75
  return parse_json(value) if datatype == 'json'
74
76
  return nil if datatype == 'nil'
75
- if datatype == 'fixnum'
77
+ if datatype == 'fixnum' || datatype == 'integer'
76
78
  return Regexp.last_match(1).to_i if value =~ /^(-?\d+)$/
77
- raise ArgumentError, "Illegal fixnum '#{value}'"
79
+ raise ArgumentError, "Illegal integer '#{value}'"
78
80
  end
79
81
  if datatype == 'float'
80
82
  return Regexp.last_match(1).to_f if value =~ /^(-?\d*\.\d+)$/
@@ -10,7 +10,7 @@ module OctocatalogDiff
10
10
  # @param options [Hash] Options hash:
11
11
  # :path [String] => Directory to bootstrap
12
12
  # :bootstrap_script [String] => Bootstrap script, relative to directory
13
- # @return [Hash] => [Fixnum] :status_code, [String] :output
13
+ # @return [Hash] => [Integer] :status_code, [String] :output
14
14
  def self.bootstrap(options = {})
15
15
  # Options validation
16
16
  unless options[:path].is_a?(String)
@@ -530,7 +530,7 @@ module OctocatalogDiff
530
530
 
531
531
  # Added a new key that points to some kind of data structure that we know how
532
532
  # to handle.
533
- if obj[1] =~ /^(.+)\f([^\f]+)$/ && [String, Fixnum, Float, TrueClass, FalseClass, Array, Hash].include?(obj[2].class)
533
+ if obj[1] =~ /^(.+)\f([^\f]+)$/ && [String, Integer, Float, TrueClass, FalseClass, Array, Hash].include?(obj[2].class)
534
534
  hashdiff_add_remove.add(obj[1])
535
535
  next
536
536
  end
@@ -255,7 +255,7 @@ module OctocatalogDiff
255
255
  # Get the diff of two long strings. Call the 'diffy' gem for this.
256
256
  # @param string1 [String] First string (-)
257
257
  # @param string2 [String] Second string (+)
258
- # @param depth [Fixnum] Depth, for correct indentation
258
+ # @param depth [Integer] Depth, for correct indentation
259
259
  # @return Array<String> Displayable result
260
260
  def self.diff_two_strings_with_diffy(string1, string2, depth)
261
261
  # Single line strings?
@@ -324,8 +324,8 @@ module OctocatalogDiff
324
324
  # Get the diff of two hashes. Call the 'diffy' gem for this.
325
325
  # @param hash1 [Hash] First hash (-)
326
326
  # @param hash1 [Hash] Second hash (+)
327
- # @param depth [Fixnum] Depth, for correct indentation
328
- # @param limit [Fixnum] Maximum string length
327
+ # @param depth [Integer] Depth, for correct indentation
328
+ # @param limit [Integer] Maximum string length
329
329
  # @param strip_diff [Boolean] Strip leading +/-/" "
330
330
  # @return [Array<String>] Displayable result
331
331
  def self.diff_two_hashes_with_diffy(opts = {})
@@ -358,7 +358,7 @@ module OctocatalogDiff
358
358
  end
359
359
 
360
360
  # Special case: addition only, no truncation
361
- # @param depth [Fixnum] Depth, for correct indentation
361
+ # @param depth [Integer] Depth, for correct indentation
362
362
  # @param hash [Hash] Added object
363
363
  # @return [Array<String>] Displayable result
364
364
  def self.addition_only_no_truncation(depth, hash)
@@ -383,7 +383,7 @@ module OctocatalogDiff
383
383
 
384
384
  # Limit length of a string
385
385
  # @param str [String] String
386
- # @param limit [Fixnum] Limit (0=unlimited)
386
+ # @param limit [Integer] Limit (0=unlimited)
387
387
  # @return [String] Truncated string
388
388
  def self.truncate_string(str, limit)
389
389
  return str if limit.nil? || str.length <= limit
@@ -392,7 +392,7 @@ module OctocatalogDiff
392
392
 
393
393
  # Get the diff between two hashes. This is recursive-aware.
394
394
  # @param obj [diff object] diff object
395
- # @param depth [Fixnum] Depth of nesting, used for indentation
395
+ # @param depth [Integer] Depth of nesting, used for indentation
396
396
  # @return Array<String> Printable diff outputs
397
397
  def self.hash_diff(obj, depth, key_in, nested = false)
398
398
  result = []
@@ -417,7 +417,7 @@ module OctocatalogDiff
417
417
  end
418
418
 
419
419
  # Get the diff between two arbitrary objects
420
- # @param depth [Fixnum] Depth of nesting, used for indentation
420
+ # @param depth [Integer] Depth of nesting, used for indentation
421
421
  # @param old_obj [?] Old object
422
422
  # @param new_obj [?] New object
423
423
  # @return Array<String> Diff output
@@ -432,12 +432,31 @@ module OctocatalogDiff
432
432
 
433
433
  # Utility Method!
434
434
  # Indent a given text string with a certain number of spaces
435
- # @param spaces [Fixnum] Number of spaces
435
+ # @param spaces [Integer] Number of spaces
436
436
  # @param text [String] Text
437
437
  def self.left_pad(spaces, text = '')
438
438
  [' ' * spaces, text].join('')
439
439
  end
440
440
 
441
+ # Utility Method!
442
+ # Harmonize equivalent class names for comparison purposes.
443
+ # @param class_name [String] Class name as input
444
+ # @return [String] Class name as output
445
+ def self.class_name_for_diffy(class_name)
446
+ return 'Integer' if class_name == 'Fixnum'
447
+ class_name
448
+ end
449
+
450
+ # Utility Method!
451
+ # `is_a?(class)` only allows one method, but this uses an array
452
+ # @param object [?] Object to consider
453
+ # @param classes [Array] Classes to determine if object is a member of
454
+ # @return [Boolean] True if object is_a any of the classes, false otherwise
455
+ def self.object_is_any_of?(object, classes)
456
+ classes.each { |clazz| return true if object.is_a? clazz }
457
+ false
458
+ end
459
+
441
460
  # Utility Method!
442
461
  # Given an arbitrary object, convert it into a string for use by 'diffy'.
443
462
  # This basically exists so we can do something prettier than just calling .inspect or .to_s
@@ -445,10 +464,10 @@ module OctocatalogDiff
445
464
  # @param obj [?] Object to be stringified
446
465
  # @return [String] String representation of object for diffy
447
466
  def self.stringify_for_diffy(obj)
448
- return JSON.pretty_generate(obj) if [Hash, Array].include?(obj.class)
467
+ return JSON.pretty_generate(obj) if object_is_any_of?(obj, [Hash, Array])
449
468
  return '""' if obj.is_a?(String) && obj == ''
450
- return obj if [String, Fixnum, Float].include?(obj.class)
451
- "#{obj.class}: #{obj.inspect}"
469
+ return obj if object_is_any_of?(obj, [String, Fixnum, Integer, Float])
470
+ "#{class_name_for_diffy(obj.class)}: #{obj.inspect}"
452
471
  end
453
472
 
454
473
  # Utility Method!
@@ -512,8 +531,8 @@ module OctocatalogDiff
512
531
  return ['""', 'undef'] if obj2.nil?
513
532
 
514
533
  # If one is an integer and the other is a string
515
- return [obj1, "\"#{obj2}\""] if obj1.is_a?(Fixnum) && obj2.is_a?(String)
516
- return ["\"#{obj1}\"", obj2] if obj1.is_a?(String) && obj2.is_a?(Fixnum)
534
+ return [obj1, "\"#{obj2}\""] if obj1.is_a?(Integer) && obj2.is_a?(String)
535
+ return ["\"#{obj1}\"", obj2] if obj1.is_a?(String) && obj2.is_a?(Integer)
517
536
 
518
537
  # True and false
519
538
  return [obj1, "\"#{obj2}\""] if obj1.is_a?(TrueClass) && obj2.is_a?(String)
@@ -74,8 +74,11 @@ module OctocatalogDiff
74
74
  if result.status
75
75
  logger.debug("Success bootstrap_directory for #{result.args[:tag]}")
76
76
  else
77
+ # Believed to be a bug condition, since error should have already been raised if this happens.
78
+ # :nocov:
77
79
  errmsg = "Failed bootstrap_directory for #{result.args[:tag]}: #{result.exception.class} #{result.exception.message}"
78
80
  raise OctocatalogDiff::Errors::BootstrapError, errmsg
81
+ # :nocov:
79
82
  end
80
83
  end
81
84
  end
@@ -21,7 +21,7 @@ module OctocatalogDiff
21
21
  # Constructor
22
22
  # Options for constructor:
23
23
  # :puppetdb_url [String] PuppetDB Server URLs
24
- # :puppetdb_server_url_timeout [Fixnum] Timeout (seconds) for puppetdb.conf
24
+ # :puppetdb_server_url_timeout [Integer] Timeout (seconds) for puppetdb.conf
25
25
  # :facts [OctocatalogDiff::Facts] Facts object
26
26
  # :fact_file [String] File from which to read facts
27
27
  # :node [String] Node name
@@ -99,14 +99,14 @@ module OctocatalogDiff
99
99
 
100
100
  # Install puppetdb.conf file in temporary directory
101
101
  # @param server_urls [String] String for server_urls in puppetdb.conf
102
- # @param server_url_timeout [Fixnum] Value for server_url_timeout in puppetdb.conf
102
+ # @param server_url_timeout [Integer] Value for server_url_timeout in puppetdb.conf
103
103
  def install_puppetdb_conf(logger, server_urls, server_url_timeout = 30)
104
104
  unless server_urls.is_a?(String)
105
105
  raise ArgumentError, "server_urls must be a string, got a: #{server_urls.class}"
106
106
  end
107
107
 
108
108
  server_url_timeout ||= 30 # If called with nil argument, supply default
109
- unless server_url_timeout.is_a?(Fixnum)
109
+ unless server_url_timeout.is_a?(Integer)
110
110
  raise ArgumentError, "server_url_timeout must be a fixnum, got a: #{server_url_timeout.class}"
111
111
  end
112
112
 
@@ -163,9 +163,12 @@ module OctocatalogDiff
163
163
 
164
164
  if options[:fact_override].is_a?(Array)
165
165
  options[:fact_override].each do |override|
166
- old_value = facts.fact(override.key)
167
- facts.override(override.key, override.value)
168
- logger.debug("Override #{override.key} from #{old_value.inspect} to #{override.value.inspect}")
166
+ keys = override.key.is_a?(Regexp) ? facts.matching(override.key) : [override.key]
167
+ keys.each do |key|
168
+ old_value = facts.fact(key)
169
+ facts.override(key, override.value)
170
+ logger.debug("Override #{key} from #{old_value.inspect} to #{override.value.inspect}")
171
+ end
169
172
  end
170
173
  end
171
174
 
@@ -68,7 +68,8 @@ module OctocatalogDiff
68
68
  # enc?
69
69
  if @options[:enc]
70
70
  raise Errno::ENOENT, "Did not find ENC as expected at #{@options[:enc]}" unless File.file?(@options[:enc])
71
- cmdline << "--node_terminus=exec --external_nodes=#{Shellwords.escape(@options[:enc])}"
71
+ cmdline << '--node_terminus=exec'
72
+ cmdline << "--external_nodes=#{Shellwords.escape(@options[:enc])}"
72
73
  end
73
74
 
74
75
  # Future parser?
@@ -167,7 +168,7 @@ module OctocatalogDiff
167
168
  # the index.
168
169
  # @param cmdline [Array] Existing command line
169
170
  # @param key [String] Key to look up
170
- # @return [Fixnum] Index of where key is defined (nil if undefined)
171
+ # @return [Integer] Index of where key is defined (nil if undefined)
171
172
  def key_position(cmdline, key)
172
173
  cmdline.index { |x| x == "--#{key}" || x =~ /\A--#{key}=/ }
173
174
  end
@@ -65,8 +65,11 @@ module OctocatalogDiff
65
65
  return unless @options[:enc_override].is_a?(Array) && @options[:enc_override].any?
66
66
  content_structure = YAML.load(content)
67
67
  @options[:enc_override].each do |x|
68
- merge_enc_param(content_structure, x.key, x.value)
69
- logger.debug "ENC override: #{x.key} #{x.value.nil? ? 'DELETED' : '= ' + x.value.inspect}"
68
+ keys = x.key.is_a?(Regexp) ? content_structure.keys.select { |y| x.key.match(y) } : [x.key]
69
+ keys.each do |key|
70
+ merge_enc_param(content_structure, key, x.value)
71
+ logger.debug "ENC override: #{key} #{x.value.nil? ? 'DELETED' : '= ' + x.value.inspect}"
72
+ end
70
73
  end
71
74
  @content = content_structure.to_yaml
72
75
  end
@@ -86,7 +86,12 @@ module OctocatalogDiff
86
86
  resources.map! do |resource|
87
87
  if resource_convertible?(resource)
88
88
  path = file_path(resource['parameters']['source'], modulepaths)
89
- raise Errno::ENOENT, "Unable to resolve '#{resource['parameters']['source']}'!" if path.nil?
89
+ if path.nil?
90
+ # Pass this through as a wrapped exception, because it's more likely to be something wrong
91
+ # in the catalog itself than it is to be a broken setup of octocatalog-diff.
92
+ message = "Errno::ENOENT: Unable to resolve '#{resource['parameters']['source']}'!"
93
+ raise OctocatalogDiff::Errors::CatalogError, message
94
+ end
90
95
 
91
96
  if File.file?(path)
92
97
  # If the file is found, read its content. If the content is all ASCII, substitute it into
@@ -22,7 +22,7 @@ module OctocatalogDiff
22
22
  # @param :node [String] REQUIRED: Node name
23
23
  # @param :basedir [String] Directory in which to compile the catalog
24
24
  # @param :pass_env_vars [Array<String>] Environment variables to pass when compiling catalog
25
- # @param :retry_failed_catalog [Fixnum] Number of retries if a catalog compilation fails
25
+ # @param :retry_failed_catalog [Integer] Number of retries if a catalog compilation fails
26
26
  # @param :tag [String] For display purposes, the catalog being compiled
27
27
  # @param :puppet_binary [String] Full path to Puppet
28
28
  # @param :puppet_version [String] Puppet version (optional; if not supplied, it is calculated)
@@ -15,7 +15,7 @@ module OctocatalogDiff
15
15
 
16
16
  # Constructor - See OctocatalogDiff::PuppetDB for additional parameters
17
17
  # @param :node [String] Node name
18
- # @param :retry [Fixnum] Number of retries, if fetch fails
18
+ # @param :retry [Integer] Number of retries, if fetch fails
19
19
  def initialize(options)
20
20
  raise ArgumentError, 'Hash of options must be passed to OctocatalogDiff::Catalog::PuppetDB' unless options.is_a?(Hash)
21
21
  raise ArgumentError, 'node must be a non-empty string' unless options[:node].is_a?(String) && options[:node] != ''
@@ -22,17 +22,17 @@ module OctocatalogDiff
22
22
 
23
23
  # Constructor
24
24
  # @param :node [String] Node name
25
- # @param :retry_failed_catalog [Fixnum] Number of retries, if fetch fails
25
+ # @param :retry_failed_catalog [Integer] Number of retries, if fetch fails
26
26
  # @param :branch [String] Environment to fetch from Puppet Master
27
27
  # @param :puppet_master [String] Puppet server and port number (assumed to be DEFAULT_PUPPET_PORT_NUMBER if not given)
28
- # @param :puppet_master_api_version [Fixnum] Puppet server API (default DEFAULT_PUPPET_SERVER_API)
28
+ # @param :puppet_master_api_version [Integer] Puppet server API (default DEFAULT_PUPPET_SERVER_API)
29
29
  # @param :puppet_master_ssl_ca [String] Path to file used to sign puppet master's certificate
30
30
  # @param :puppet_master_ssl_verify [Boolean] Override the CA verification setting guessed from parameters
31
31
  # @param :puppet_master_ssl_client_pem [String] PEM-encoded client key and certificate
32
32
  # @param :puppet_master_ssl_client_p12 [String] pkcs12-encoded client key and certificate
33
33
  # @param :puppet_master_ssl_client_password [String] Path to file containing password for SSL client key (any format)
34
34
  # @param :puppet_master_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
35
- # @param :timeout [Fixnum] Connection timeout for Puppet master (default=PUPPET_MASTER_TIMEOUT seconds)
35
+ # @param :timeout [Integer] Connection timeout for Puppet master (default=PUPPET_MASTER_TIMEOUT seconds)
36
36
  def initialize(options)
37
37
  raise ArgumentError, 'Hash of options must be passed to OctocatalogDiff::Catalog::PuppetMaster' unless options.is_a?(Hash)
38
38
  raise ArgumentError, 'node must be a non-empty string' unless options[:node].is_a?(String) && options[:node] != ''
@@ -50,7 +50,7 @@ module OctocatalogDiff
50
50
  # @param argv [Array] Use specified arguments (defaults to ARGV)
51
51
  # @param logger [Logger] Logger object
52
52
  # @param opts [Hash] Additional options
53
- # @return [Fixnum] Exit code: 0=no diffs, 1=something went wrong, 2=worked but there are diffs
53
+ # @return [Integer] Exit code: 0=no diffs, 1=something went wrong, 2=worked but there are diffs
54
54
  def self.cli(argv = ARGV, logger = Logger.new(STDERR), opts = {})
55
55
  # Save a copy of argv to print out later in debugging
56
56
  argv_save = argv.dup
@@ -14,7 +14,7 @@ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_timeout) do
14
14
  cli_name: 'puppet-master-timeout',
15
15
  option_name: 'puppet_master_timeout',
16
16
  desc: 'Puppet Master catalog retrieval timeout in seconds',
17
- validator: ->(x) { x.to_i > 0 || raise(ArgumentError, 'Specify timeout as a integer greater than 0') },
17
+ validator: ->(x) { x.to_i > 0 || raise(ArgumentError, 'Specify timeout as an integer greater than 0') },
18
18
  translator: ->(x) { x.to_i }
19
19
  )
20
20
  end
@@ -120,5 +120,12 @@ module OctocatalogDiff
120
120
  @facts['values'][key] = value
121
121
  end
122
122
  end
123
+
124
+ # Find all facts matching a particular pattern
125
+ # @param regex [Regexp] Regular expression to match
126
+ # @return [Array<String>] Facts that match the regexp
127
+ def matching(regex)
128
+ @facts['values'].keys.select { |fact| regex.match(fact) }
129
+ end
123
130
  end
124
131
  end
@@ -17,7 +17,7 @@ module OctocatalogDiff
17
17
 
18
18
  # Retrieve facts from PuppetDB for a specified node.
19
19
  # @param :puppetdb_url [String|Array] => URL to PuppetDB
20
- # @param :retry [Fixnum] => Retry after timeout (default 0 retries, can be more)
20
+ # @param :retry [Integer] => Retry after timeout (default 0 retries, can be more)
21
21
  # @param node [String] Node name. (REQUIRED for PuppetDB fact source)
22
22
  # @return [Hash] Facts
23
23
  def self.fact_retriever(options = {}, node)
@@ -38,7 +38,7 @@ module OctocatalogDiff
38
38
  # Supported arguments:
39
39
  # @param :puppetdb_url [String or Array<String>] PuppetDB URL(s) to try in random order
40
40
  # @param :puppetdb_host [String] PuppetDB hostname, when constructing a URL
41
- # @param :puppetdb_port [Fixnum] Port number, defaults to 8080 (non-SSL) or 8081 (SSL)
41
+ # @param :puppetdb_port [Integer] Port number, defaults to 8080 (non-SSL) or 8081 (SSL)
42
42
  # @param :puppetdb_ssl [Boolean] defaults to true, because you should use SSL
43
43
  # @param :puppetdb_ssl_ca [String] Path to file containing CA certificate
44
44
  # @param :puppetdb_ssl_verify [Boolean] Override the CA verification setting guessed from parameters
@@ -46,7 +46,7 @@ module OctocatalogDiff
46
46
  # @param :puppetdb_ssl_client_p12 [String] pkcs12-encoded client key and certificate
47
47
  # @param :puppetdb_ssl_client_password [String] Path to file containing password for SSL client key (any format)
48
48
  # @param :puppetdb_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
49
- # @param :timeout [Fixnum] Connection timeout for PuppetDB (default=10)
49
+ # @param :timeout [Integer] Connection timeout for PuppetDB (default=10)
50
50
  def initialize(options = {})
51
51
  @connections =
52
52
  if options.key?(:puppetdb_url)
@@ -149,7 +149,7 @@ module OctocatalogDiff
149
149
 
150
150
  # Parse a URL to determine hostname, port number, and whether or not SSL is used.
151
151
  # @param url [String] URL to parse
152
- # @return [Hash] { ssl: true/false, host: <String>, port: <Fixnum> }
152
+ # @return [Hash] { ssl: true/false, host: <String>, port: <Integer> }
153
153
  def parse_url(url)
154
154
  uri = URI(url)
155
155
  raise ArgumentError, "URL #{url} has invalid scheme" unless uri.scheme =~ /^https?$/
@@ -99,6 +99,15 @@ module OctocatalogDiff
99
99
  # :nocov:
100
100
  end
101
101
 
102
+ # If catalogs failed to compile, report that. Prefer to display an actual failure message rather
103
+ # than a generic incomplete parallel task message if there is a more specific message present.
104
+ failures = parallel_catalogs.reject(&:status)
105
+ if failures.any?
106
+ f = failures.reject { |r| r.exception.is_a?(OctocatalogDiff::Util::Parallel::IncompleteTask) }.first
107
+ f ||= failures.first
108
+ raise f.exception
109
+ end
110
+
102
111
  # Construct result hash. Will eventually be in the format
103
112
  # { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
104
113
 
@@ -203,10 +212,12 @@ module OctocatalogDiff
203
212
  end
204
213
  else
205
214
  # Something unhandled went wrong, and an exception was thrown. Reveal a generic message.
215
+ # :nocov:
206
216
  msg = parallel_catalog_obj.exception.message
207
217
  message = "Catalog for '#{key}' (#{branch}) failed to compile with #{parallel_catalog_obj.exception.class}: #{msg}"
208
218
  message += "\n" + parallel_catalog_obj.exception.backtrace.map { |x| " #{x}" }.join("\n") if @options[:debug]
209
219
  raise OctocatalogDiff::Errors::CatalogError, message
220
+ # :nocov:
210
221
  end
211
222
  end
212
223
 
@@ -220,22 +231,25 @@ module OctocatalogDiff
220
231
  time_start = Time.now
221
232
  catalog.build(logger)
222
233
  time_it_took = Time.now - time_start
223
- retries_str = " retries = #{catalog.retries}" if catalog.retries.is_a?(Fixnum)
234
+ retries_str = " retries = #{catalog.retries}" if catalog.retries.is_a?(Integer)
224
235
  time_str = "in #{time_it_took} seconds#{retries_str}"
225
236
  status_str = catalog.valid? ? 'successfully built' : 'failed'
226
237
  logger.debug "Catalog for #{opts[:branch]} #{status_str} with #{catalog.builder} #{time_str}"
227
238
  catalog
228
239
  end
229
240
 
230
- # Validate a catalog in the parallel execution
241
+ # The catalog validator method can indicate failure one of two ways:
242
+ # - Raise an exception (this is preferred, since it gives a specific error message)
243
+ # - Return false (supported but discouraged, since it only surfaces a generic error)
231
244
  # @param catalog [OctocatalogDiff::Catalog] Catalog object
232
245
  # @param logger [Logger] Logger object (presently unused)
233
246
  # @param args [Hash] Additional arguments set specifically for validator
234
- # @return [Boolean] true if catalog is valid, false otherwise
247
+ # @return [Boolean] Return true if catalog is valid, false otherwise
235
248
  def catalog_validator(catalog = nil, _logger = @logger, args = {})
236
- return false unless catalog.is_a?(OctocatalogDiff::Catalog)
237
- catalog.validate_references if args[:task] == :to
238
- catalog.valid?
249
+ raise ArgumentError, "Expects a catalog, got #{catalog.class}" unless catalog.is_a?(OctocatalogDiff::Catalog)
250
+ raise OctocatalogDiff::Errors::CatalogError, "Catalog failed: #{catalog.error_message}" unless catalog.valid?
251
+ catalog.validate_references if args[:task] == :to # Raises exception for broken references
252
+ true
239
253
  end
240
254
  end
241
255
  end
@@ -118,12 +118,20 @@ module OctocatalogDiff
118
118
  else
119
119
  raise ArgumentError, 'SSL client auth enabled but no client keypair specified'
120
120
  end
121
- if result[:pem]
122
- result[:pem_password] = options[:ssl_client_password] if options[:ssl_client_password]
123
- # Make sure there's not a password required, or that if the password is given, it is correct.
124
- # We do not want to wait on STDIN.
125
- # This will raise OpenSSL::PKey::RSAError if the key needs a password.
126
- OpenSSL::PKey::RSA.new(result[:pem], result[:pem_password] || '')
121
+
122
+ # Make sure there's not a password required, or that if the password is given, it is correct.
123
+ # This will raise OpenSSL::PKey::RSAError if the key needs a password.
124
+ if result[:pem] && options[:ssl_client_password]
125
+ result[:pem_password] = options[:ssl_client_password]
126
+ _trash = OpenSSL::PKey::RSA.new(result[:pem], result[:pem_password])
127
+ elsif result[:pem]
128
+ # Ruby 2.4 requires a minimum password length of 4. If no password is needed for
129
+ # the certificate, the specified password here is effectively ignored.
130
+ # We do not want to wait on STDIN, so a password-protected certificate without a
131
+ # password will cause this to raise an error. There are two checks here, to exclude
132
+ # an edge case where somebody did actually put '1234' as their password.
133
+ _trash = OpenSSL::PKey::RSA.new(result[:pem], '1234')
134
+ _trash = OpenSSL::PKey::RSA.new(result[:pem], '5678')
127
135
  end
128
136
  end
129
137
 
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Helper to use the 'parallel' gem to perform tasks
3
+ # A class to parallelize process executation.
4
+ # This is a utility class to execute tasks in parallel, with our own forking implementation
5
+ # that passes through logs and reliably handles errors. If parallel processing has been disabled,
6
+ # this instead executes the tasks serially, but provides the same API as the parallel tasks.
4
7
 
5
- require 'parallel'
6
8
  require 'stringio'
7
9
 
8
10
  module OctocatalogDiff
9
11
  module Util
10
- # This is a utility class to execute tasks in parallel, using the 'parallel' gem.
11
- # If parallel processing has been disabled, this instead executes the tasks serially,
12
- # but provides the same API as the parallel tasks.
13
12
  class Parallel
13
+ # This exception is called for a task that didn't complete.
14
+ class IncompleteTask < RuntimeError; end
15
+
16
+ # --------------------------------------
14
17
  # This class represents a parallel task. It requires a method reference, which will be executed with
15
18
  # any supplied arguments. It can optionally take a text description and a validator function.
19
+ # --------------------------------------
16
20
  class Task
17
21
  attr_reader :description
18
22
  attr_accessor :args
@@ -35,10 +39,12 @@ module OctocatalogDiff
35
39
  end
36
40
  end
37
41
 
42
+ # --------------------------------------
38
43
  # This class represents the result from a parallel task. The status is set to true (success), false (error),
39
44
  # or nil (task was killed before it could complete). The exception (for failure) and output object (for success)
40
45
  # are readable attributes. The validity of the results, determined by executing the 'validate' method of the Task,
41
46
  # is available to be set and fetched.
47
+ # --------------------------------------
42
48
  class Result
43
49
  attr_reader :output, :args
44
50
  attr_accessor :status, :exception
@@ -51,121 +57,170 @@ module OctocatalogDiff
51
57
  end
52
58
  end
53
59
 
60
+ # --------------------------------------
61
+ # Static methods in the class
62
+ # --------------------------------------
63
+
54
64
  # Entry point for parallel processing. By default this will perform parallel processing,
55
65
  # but it will also accept an option to do serial processing instead.
56
66
  # @param task_array [Array<Parallel::Task>] Tasks to run
57
67
  # @param logger [Logger] Optional logger object
58
68
  # @param parallelized [Boolean] True for parallel processing, false for serial processing
69
+ # @param raise_exception [Boolean] True to raise exception immediately if one occurs; false to return exception in results
59
70
  # @return [Array<Parallel::Result>] Parallel results (same order as tasks)
60
- def self.run_tasks(task_array, logger = nil, parallelized = true)
71
+ def self.run_tasks(task_array, logger = nil, parallelized = true, raise_exception = false)
61
72
  # Create a throwaway logger object if one is not given
62
73
  logger ||= Logger.new(StringIO.new)
63
74
 
64
- # Validate input - we need an array. If the array is empty then return an empty array right away.
75
+ # Validate input - we need an array of OctocatalogDiff::Util::Parallel::Task. If the array is empty then
76
+ # return an empty array right away.
65
77
  raise ArgumentError, "run_tasks() argument must be array, not #{task_array.class}" unless task_array.is_a?(Array)
66
78
  return [] if task_array.empty?
67
79
 
68
- # Make sure each element in the array is a OctocatalogDiff::Util::Parallel::Task
69
- task_array.each do |x|
70
- next if x.is_a?(OctocatalogDiff::Util::Parallel::Task)
71
- raise ArgumentError, "Element #{x.inspect} must be a OctocatalogDiff::Util::Parallel::Task, not a #{x.class}"
80
+ invalid_inputs = task_array.reject { |task| task.is_a?(OctocatalogDiff::Util::Parallel::Task) }
81
+ if invalid_inputs.any?
82
+ ele = invalid_inputs.first
83
+ raise ArgumentError, "Element #{ele.inspect} must be a OctocatalogDiff::Util::Parallel::Task, not a #{ele.class}"
72
84
  end
73
85
 
74
- # Actually do the processing - choose here between parallel and serial
75
- parallelized ? run_tasks_parallel(task_array, logger) : run_tasks_serial(task_array, logger)
86
+ # Initialize the result array. For now all entries in the array indicate that the task was killed.
87
+ # Actual statuses will replace this initial status. If the initial status wasn't replaced, then indeed,
88
+ # the task was killed.
89
+ result = task_array.map { |x| Result.new(exception: IncompleteTask.new('Killed'), args: x.args) }
90
+ logger.debug "Initialized parallel task result array: size=#{result.size}"
91
+
92
+ # Execute as per the requested method (serial or parallel) and handle results.
93
+ exception = parallelized ? run_tasks_parallel(result, task_array, logger) : run_tasks_serial(result, task_array, logger)
94
+ raise exception if exception && raise_exception
95
+ result
76
96
  end
77
97
 
78
- # Use the parallel gem to run each task in the task array. Under the hood this is forking a process for
79
- # each task, and serializing/deserializing the arguments and the outputs.
98
+ # Utility method! Not intended to be called from outside this class.
99
+ # ---
100
+ # Use a forking strategy to run tasks in parallel. Each task in the array is forked in a child
101
+ # process, and when that task completes it writes its result (OctocatalogDiff::Util::Parallel::Result)
102
+ # into a serialized data file. Once children are forked this method waits for their return, deserializing
103
+ # the output from each data file and updating the `result` array with actual results.
104
+ # @param result [Array<OctocatalogDiff::Util::Parallel::Result>] Parallel task results
80
105
  # @param task_array [Array<OctocatalogDiff::Util::Parallel::Task>] Tasks to perform
81
106
  # @param logger [Logger] Logger
82
- # @return [Array<OctocatalogDiff::Util::Parallel::Result>] Parallel task results
83
- def self.run_tasks_parallel(task_array, logger)
84
- # Create an empty array of results. The status is nil and the exception is pre-populated. If the code
85
- # runs successfully and doesn't get killed, all of these default values will be overwritten. If the code
86
- # gets killed before the task finishes, this exception will remain.
87
- result = task_array.map do |x|
88
- Result.new(exception: ::Parallel::Kill.new('Killed'), args: x.args)
107
+ # @return [Exception] First exception encountered by a child process; returns nil if no exceptions encountered.
108
+ def self.run_tasks_parallel(result, task_array, logger)
109
+ pidmap = {}
110
+ ipc_tempdir = Dir.mktmpdir
111
+
112
+ # Child process forking
113
+ task_array.each_with_index do |task, index|
114
+ # simplecov doesn't see this because it's forked
115
+ # :nocov:
116
+ this_pid = fork do
117
+ task_result = execute_task(task, logger)
118
+ File.open(File.join(ipc_tempdir, "#{Process.pid}.dat"), 'w') { |f| f.write Marshal.dump(task_result) }
119
+ Kernel.exit! 0 # Kernel.exit! avoids at_exit from parents being triggered by children exiting
120
+ end
121
+ # :nocov:
122
+
123
+ pidmap[this_pid] = { index: index, start_time: Time.now }
124
+ logger.debug "Launched pid=#{this_pid} for index=#{index}"
125
+ logger.reopen if logger.respond_to?(:reopen)
89
126
  end
90
- logger.debug "Initialized parallel task result array: size=#{result.size}"
91
127
 
92
- # Do parallel processing
93
- ::Parallel.each(task_array,
94
- finish: lambda do |item, i, parallel_result|
95
- # Set the result array element to the result
96
- result[i] = parallel_result
97
-
98
- # Kill all other parallel tasks if this task failed by throwing an exception
99
- raise ::Parallel::Kill unless parallel_result.exception.nil?
100
-
101
- # Run the validator to determine if the result is in fact valid. The validator
102
- # returns true or false. If true, set the 'valid' attribute in the result. If
103
- # false, kill all other parallel tasks.
104
- if item.validate(parallel_result.output, logger)
105
- logger.debug("Success #{item.description}")
106
- else
107
- logger.warn("Failed #{item.description}")
108
- result[i].status = false
109
- raise ::Parallel::Kill
110
- end
111
- end) do |ele|
112
- # simplecov does not detect that this code runs because it's forked, but this is
113
- # tested extensively in the parallel_spec.rb spec file.
114
- # :nocov:
128
+ # Waiting for children and handling results
129
+ while pidmap.any?
130
+ this_pid, exit_obj = Process.wait2(0)
131
+ next unless this_pid && pidmap.key?(this_pid)
132
+ index = pidmap[this_pid][:index]
133
+ exitstatus = exit_obj.exitstatus
134
+ raise "PID=#{this_pid} exited abnormally: #{exit_obj.inspect}" if exitstatus.nil?
135
+ raise "PID=#{this_pid} exited with status #{exitstatus}" unless exitstatus.zero?
136
+
137
+ input = File.read(File.join(ipc_tempdir, "#{this_pid}.dat"))
138
+ result[index] = Marshal.load(input) # rubocop:disable Security/MarshalLoad
139
+ time_delta = Time.now - pidmap[this_pid][:start_time]
140
+ pidmap.delete(this_pid)
141
+
142
+ logger.debug "PID=#{this_pid} completed in #{time_delta} seconds, #{input.length} bytes"
143
+
144
+ next if result[index].status
145
+ return result[index].exception
146
+ end
147
+
148
+ logger.debug 'All child processes completed with no exceptions raised'
149
+
150
+ # Cleanup: Kill any child processes that are still running, and clean the temporary directory
151
+ # where data files were stored.
152
+ ensure
153
+ pidmap.each do |pid, _pid_data|
115
154
  begin
116
- logger.debug("Begin #{ele.description}")
117
- output = ele.execute(logger)
118
- logger.debug("Success #{ele.description}")
119
- Result.new(output: output, status: true, args: ele.args)
120
- rescue => exc
121
- logger.debug("Failed #{ele.description}: #{exc.class} #{exc.message}")
122
- Result.new(exception: exc, status: false, args: ele.args)
155
+ Process.kill('TERM', pid)
156
+ rescue Errno::ESRCH # rubocop:disable Lint/HandleExceptions
157
+ # If the process doesn't exist, that's fine.
123
158
  end
124
- # :nocov:
125
159
  end
126
160
 
127
- # Return result
128
- result
161
+ retries = 0
162
+ while File.directory?(ipc_tempdir) && retries < 10
163
+ retries += 1
164
+ begin
165
+ FileUtils.remove_entry_secure ipc_tempdir
166
+ rescue Errno::ENOTEMPTY, Errno::ENOENT # rubocop:disable Lint/HandleExceptions
167
+ # Errno::ENOTEMPTY will trigger a retry because the directory exists
168
+ # Errno::ENOENT will break the loop because the directory won't exist next time it's checked
169
+ end
170
+ end
129
171
  end
130
172
 
173
+ # Utility method! Not intended to be called from outside this class.
174
+ # ---
131
175
  # Perform the tasks in serial.
176
+ # @param result [Array<OctocatalogDiff::Util::Parallel::Result>] Parallel task results
132
177
  # @param task_array [Array<OctocatalogDiff::Util::Parallel::Task>] Tasks to perform
133
178
  # @param logger [Logger] Logger
134
- # @return [Array<OctocatalogDiff::Util::Parallel::Result>] Parallel task results
135
- def self.run_tasks_serial(task_array, logger)
136
- # Create an empty array of results. The status is nil and the exception is pre-populated. If the code
137
- # runs successfully, all of these default values will be overwritten. If a predecessor task fails, all
138
- # later task will have the defined exception.
139
- result = task_array.map do |x|
140
- Result.new(exception: ::RuntimeError.new('Cancellation - A prior task failed'), args: x.args)
141
- end
142
-
179
+ def self.run_tasks_serial(result, task_array, logger)
143
180
  # Perform the tasks 1 by 1 - each successful task will replace an element in the 'result' array,
144
181
  # whereas a failed task will replace the current element with an exception, and all later tasks
145
182
  # will not be replaced (thereby being populated with the cancellation error).
146
- task_counter = 0
147
- task_array.each do |ele|
148
- begin
149
- logger.debug("Begin #{ele.description}")
150
- output = ele.execute(logger)
151
- result[task_counter] = Result.new(output: output, status: true, args: ele.args)
152
- rescue => exc
153
- logger.debug("Failed #{ele.description}: #{exc.class} #{exc.message}")
154
- result[task_counter] = Result.new(exception: exc, status: false, args: ele.args)
155
- end
183
+ task_array.each_with_index do |ele, task_counter|
184
+ result[task_counter] = execute_task(ele, logger)
185
+ next if result[task_counter].status
186
+ return result[task_counter].exception
187
+ end
188
+ nil
189
+ end
156
190
 
157
- if ele.validate(output, logger)
158
- logger.debug("Success #{ele.description}")
191
+ # Utility method! Not intended to be called from outside this class.
192
+ # ---
193
+ # Process a single task. Called by run_tasks_parallel / run_tasks_serial.
194
+ # This method will report all exceptions in the OctocatalogDiff::Util::Parallel::Result object
195
+ # itself, and not raise them.
196
+ # @param task [OctocatalogDiff::Util::Parallel::Task] Task object
197
+ # @param logger [Logger] Logger
198
+ # @return [OctocatalogDiff::Util::Parallel::Result] Parallel task result
199
+ def self.execute_task(task, logger)
200
+ begin
201
+ logger.debug("Begin #{task.description}")
202
+ output = task.execute(logger)
203
+ result = Result.new(output: output, status: true, args: task.args)
204
+ rescue => exc
205
+ logger.debug("Failed #{task.description}: #{exc.class} #{exc.message}")
206
+ # Immediately return without running the validation, since this already failed.
207
+ return Result.new(exception: exc, status: false, args: task.args)
208
+ end
209
+
210
+ begin
211
+ if task.validate(output, logger)
212
+ logger.debug("Success #{task.description}")
159
213
  else
160
- logger.warn("Failed #{ele.description}")
161
- result[task_counter].status = false
214
+ # Preferably the validator method raised its own exception. However if it
215
+ # simply returned false, raise our own exception here.
216
+ raise "Failed #{task.description} validation (unspecified error)"
162
217
  end
163
-
164
- break unless result[task_counter].status
165
- task_counter += 1
218
+ rescue => exc
219
+ logger.warn("Failed #{task.description} validation: #{exc.class} #{exc.message}")
220
+ result.status = false
221
+ result.exception = exc
166
222
  end
167
223
 
168
- # Return the result
169
224
  result
170
225
  end
171
226
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: octocatalog-diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub, Inc.
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-05-09 00:00:00.000000000 Z
12
+ date: 2017-05-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: diffy
@@ -53,20 +53,6 @@ dependencies:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
55
  version: 0.3.0
56
- - !ruby/object:Gem::Dependency
57
- name: parallel
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 1.11.1
63
- type: :runtime
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: 1.11.1
70
56
  - !ruby/object:Gem::Dependency
71
57
  name: rugged
72
58
  requirement: !ruby/object:Gem::Requirement