-
Notifications
You must be signed in to change notification settings - Fork 732
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
Add pathtest functionality to assign mutators #1101
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very preliminary first pass, haven't gone through all the code yet - will circle back for complete review.
pkg/mutation/assign_mutator.go
Outdated
for _, pt := range pts { | ||
p, err := parser.Parse(pt.SubPath) | ||
if err != nil { | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain the parser will include enough context as to which path failed - we might want to wrap and include that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM, added a wrap
pkg/mutation/assign_mutator.go
Outdated
|
||
func (m *AssignMutator) getTester() (*patht.Tester, error) { | ||
if m.tester != nil { | ||
return m.tester, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the concurrency story for this mutator? Do we need to guard here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be accessed in a concurrent manner, but no reason not to throw a lock around this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an RWMutex
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This version is missing the second nil check of the double-checked locking pattern - two threads that saw tester == nil above can serially enter the code below and initialize it twice. Not a huge deal, but probably not what we intended.
I'd suggest using a sync.Once
to simplify and fix that issue all at once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can definitely add the second nil
check.
WRT sync.Once, I think we probably want to run the code every time if the value is nil, so that we can continue to return an appropriate error if nil
is the result of an error.
The other option would be to cache the error as well, but that has the potential to make otherwise transient bugs sticky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds like we have a path forward here but removing lazy initialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! Committed a change that switches to eager initialization, should be good now.
40a6dc1
to
a7ee588
Compare
Signed-off-by: Max Smythe <smythe@google.com>
Signed-off-by: Max Smythe <smythe@google.com>
Signed-off-by: Max Smythe <smythe@google.com>
Signed-off-by: Max Smythe <smythe@google.com>
pkg/mutation/assign_mutator.go
Outdated
|
||
func (m *AssignMutator) getTester() (*patht.Tester, error) { | ||
if m.tester != nil { | ||
return m.tester, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This version is missing the second nil check of the double-checked locking pattern - two threads that saw tester == nil above can serially enter the code below and initialize it twice. Not a huge deal, but probably not what we intended.
I'd suggest using a sync.Once
to simplify and fix that issue all at once.
pkg/mutation/assign_mutator.go
Outdated
@@ -91,6 +135,9 @@ func (m *AssignMutator) DeepCopy() types.Mutator { | |||
} | |||
copy(res.path.Nodes, m.path.Nodes) | |||
copy(res.bindings, m.bindings) | |||
m.mux.RLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify why we need a lock for this field and not others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the reason is that the others are created on start-up via MutatorForAssign.
I could move the initialization there instead of lazy-loading to avoid locking entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would have the added benefit of avoiding the need to have as many error
returns as we do currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - I feel like it simplifies things if we only need to deal with a fully initialized mutator. 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed
// mutateInternal mutates the resource recursively. It returns false if there has been no change | ||
// to any downstream objects in the tree, indicating that the mutation should not be persisted | ||
func (s *mutatorState) mutateInternal(current interface{}, depth int) (bool, interface{}, error) { | ||
pathEntry := s.mutator.Path().Nodes[depth] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add bounds checks? It's possible for len(path.Nodes) == 0
with an empty location for example. Are we depending on prior validations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think making sure len(path) != 0 is a good idea, as a mutation with no path makes no sense and we should catch that anywhere it leaks.
Looking at other validation code, it appears we are only validating whether location
parses, which it would if it were empty. So, I think there are two work items:
- Validate Mutate inputs using the minimum standards necessary for making a coherent mutate call
- Add more
location
validation to Assign (I think AssignMeta enforces using ametadata
prefix)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM. Assuming we apply additional validations earlier on, are you thinking we should still do bounds checking in places like this or build on the preconditions from validation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should do "minimum sane input" bounds checking.
If there is no reason a function should see a given input (e.g. mutating a zero length path has no semantic meaning other than maybe overwriting the entire object), we should validate that so we can surface bugs like uninitialized structs.
I don't think we should go stricter than that though, to avoid needing to tweak validation in too many places if we add functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added the sanity checks to the mutate() function
nextAsObject, ok := next.(map[string]interface{}) | ||
if !ok { // Path entry type does not match current object | ||
return fmt.Errorf("two consecutive list path entries not allowed: %+v %+v", castPathEntry, nextPathEntry) | ||
return nil, fmt.Errorf("two consecutive list path entries not allowed: %+v %+v", castPathEntry, nextPathEntry) | ||
} | ||
nextAsObject[castPathEntry.KeyField] = *castPathEntry.KeyValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this dereference safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be... I don't think it's possible to trigger a create against a globbed path entry as a missing entry just doesn't fan out.
We can return an error per above, if you prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'd prefer not depending on too many far-away assumptions to avoid panics, as they have a habit of changing under your feet. And even when they don't, it makes it harder to reason about correctness of the code in isolation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added nil checking
} | ||
newValueAsObject, ok := newValue.(map[string]interface{}) | ||
if !ok { | ||
return fmt.Errorf("last path entry of type list requires an object value, pathEntry: %+v", listPathEntry) | ||
return false, nil, fmt.Errorf("last path entry of type list requires an object value, pathEntry: %+v", listPathEntry) | ||
} | ||
|
||
key := listPathEntry.KeyField | ||
keyValue := *listPathEntry.KeyValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this dereference is safe because we know Glob==false, but that requires a lot of knowledge of the parser. It might be worth adding a KeyValue accessor that is nil-safe just in case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
I think the correct answer is to return an error if we get an unexpected nil
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added nil checking
Signed-off-by: Max Smythe <smythe@google.com>
Codecov Report
@@ Coverage Diff @@
## master #1101 +/- ##
==========================================
+ Coverage 48.08% 48.39% +0.30%
==========================================
Files 62 63 +1
Lines 4274 4319 +45
==========================================
+ Hits 2055 2090 +35
- Misses 1964 1974 +10
Partials 255 255
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
🎊 |
* Add pathtest functionality to assign mutators Signed-off-by: Max Smythe <smythe@google.com> * Add locking, deepcopy path tester memoization Signed-off-by: Max Smythe <smythe@google.com> * Add path parsing error context Signed-off-by: Max Smythe <smythe@google.com> * Tester should also have a DeepCopy method Signed-off-by: Max Smythe <smythe@google.com> * Eager initialize path tester, guard against nil Signed-off-by: Max Smythe <smythe@google.com>
Signed-off-by: Max Smythe smythe@google.com
What this PR does / why we need it:
Implementing path tests per #1079
Which issue(s) this PR fixes (optional, using
fixes #<issue number>(, fixes #<issue_number>, ...)
format, will close the issue(s) when the PR gets merged):Fixes #1079
Special notes for your reviewer: