Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

object validation for DataHandlerBase#normalize_hash #7264

Merged
merged 3 commits into from
May 18, 2018

Conversation

jeremymv2
Copy link
Contributor

@jeremymv2 jeremymv2 commented May 15, 2018

Signed-off-by: Jeremy J. Miller [email protected]

Description

Chef Server responses to ChefFS requests pass through a data handler pipeline where they get normalized via DataHandlerBase#normalize_hash. Without this change, the function will blow up with undefined method 'has_key?' for nil:NilClass when the Chef Server response contains unexpected non-json null character.

Specifically, this change adds input validation checks to the DataHandlerBase#normalize_hash method so that it can survive being given null data responses from older Chef Servers and continue processing the remainder of the objects.

If passed anything other than a Hash object, the function will not try to iterate over it, but simply return a different Hash filled in with the object's default/normalized values, also informing the user that defaults are being used.

Quick overview, the normalization here is a three step process:

  1. Create a new Hash (results) to hold the normalized data.
  2. Iterate over the defaults Hash, adding values to results but preferring already existing values from object when the key exists in both.
  3. Iterate over the object Hash, adding values to results that don't already exist.

Issues Resolved

ZD #18855

This error was encountered when running knife ec backup ..

The (very old version of) Chef Server was returning empty data for one node - when looking at the serialized_object it only had \x00. This was causing a nil value to be passed in as the first argument to normalize_hash.

Error Summary Report

{

"timestamp": "2018-05-10 01:52:45 +0000",

"message": "undefined method `has_key?' for nil:NilClass",

"backtrace": [

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/data_handler/data_handler_base.rb:68:in `block in normalize_hash'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/data_handler/data_handler_base.rb:67:in `each_pair'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/data_handler/data_handler_base.rb:67:in `normalize_hash'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/data_handler/node_data_handler.rb:9:in `normalize'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb:125:in `minimize_value'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb:104:in `read'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/file_system.rb:331:in `copy_entries'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/file_system.rb:359:in `block in copy_entries'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb:263:in `process_input'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb:253:in `process_one'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/parallelizer.rb:92:in `call'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/chef-13.6.4/lib/chef/chef_fs/parallelizer.rb:92:in `worker_loop'",

"/opt/chefdk/embedded/lib/ruby/gems/2.4.0/gems/logging-2.2.2/lib/logging/diagnostic_context.rb:474:in `block in create_with_logging_context'"

],

"exception": "NoMethodError"

}

Error(s) Summary file located at: '/app/backup/08052018/errors/backup-2018-05-10T01:52:45+00:00.json'

Check List

@jeremymv2 jeremymv2 requested a review from a team May 15, 2018 19:42
@jeremymv2 jeremymv2 changed the title better object validation for DataHandlerBase#normalize_hash object validation for DataHandlerBase#normalize_hash May 15, 2018
@jeremymv2 jeremymv2 force-pushed the jeremymv2/fix_normalize_hash branch from 7b897c1 to 7acbb49 Compare May 15, 2018 22:33
rhass
rhass previously requested changes May 16, 2018
@@ -64,10 +64,14 @@ def normalize_hash(object, defaults)
# Make a normalized result in the specified order for diffing
result = {}
defaults.each_pair do |key, default|
result[key] = object.has_key?(key) ? object[key] : default
result[key] = object.is_a?(Hash) && object.key?(key) ? object[key] : default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant. The new conditional block does the same thing and add the exception handler. We probably should just remove this block entirely.

Copy link
Contributor Author

@jeremymv2 jeremymv2 May 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I am not in agreement with this statement. If we do what you are suggesting, then we end up with a resulting Hash whose keys values are replaced with default values when the key exists in both the original and the defaults Hashes. We want the value from the original Hash preserved 🙂

Quick overview, the normalization here is a three step process:

  1. Create a new Hash (results) to hold the normalized data
  2. Iterate over the defaults Hash, adding values to results but preferring already existing values from object when the key exists in both.
  3. Iterate over the object Hash, adding values to results that don't already exist

We can't change step 2 as it's essential. I've added another commit with a 3rd rspec test showing what the importance of step 2 is: Add items into result but prefer the items that already exist in object over the default value from defaults.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would benefit from an updated comment describing exactly the above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rhass did I make a convincing enough argument to change your mind? :bowtie:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just now saw this.

Copy link
Contributor Author

@jeremymv2 jeremymv2 May 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I failed to mention another important part in overviewing! 🤦‍♂️ Previously when the line was just object.key?(key) to the left of the ? we'd get a undefined method 'has_key?' for nil:NilClass error whenever object was nil

That's because Ruby calls the has_key? method of a nil object. The NilClass does not have such a method, but it does have the method .is_a? (which it inherits from Object)

When we combine object.is_a?(Hash) && object.has_key?(key) we can guard the right side of the && knowing Ruby evaluates the left side of the && first and stops evaluation if the left is false. When object is nil object.is_a?(Hash) evaluates to false so Ruby doesn't try to evaluate object.has_key?(key), assigns the value from the last part of the ternary and doesn't raise a runtime undefined method exception.

irb(main):005:0> object = nil
=> nil
irb(main):006:0> object.has_key?('foo')
Traceback (most recent call last):
        2: from /opt/chefdk/embedded/bin/irb:11:in `<main>'
        1: from (irb):6
NoMethodError (undefined method `has_key?' for nil:NilClass)
irb(main):007:0> object.is_a?(Hash) && object.has_key?('foo')
=> false
irb(main):008:0>

jeremymv2 added 2 commits May 16, 2018 07:50
Signed-off-by: Jeremy J. Miller <[email protected]>
Signed-off-by: Jeremy J. Miller <[email protected]>
Copy link
Contributor

@thommay thommay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tenor-156694376

@itmustbejj itmustbejj dismissed rhass’s stale review May 18, 2018 00:19

After discussing with @jm, the reason for the additional is_a?(Hash) check is to handle nil values raising noMethod errors for key? method

@itmustbejj itmustbejj merged commit b13936f into chef:master May 18, 2018
@jeremymv2 jeremymv2 deleted the jeremymv2/fix_normalize_hash branch May 18, 2018 00:20
@lock
Copy link

lock bot commented Jul 17, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jul 17, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants