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

Add pathtest functionality to assign mutators #1101

Merged
merged 6 commits into from
Feb 9, 2021

Conversation

maxsmythe
Copy link
Contributor

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:

Copy link
Contributor

@shomron shomron left a 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.

for _, pt := range pts {
p, err := parser.Parse(pt.SubPath)
if err != nil {
return nil, err
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SGTM, added a wrap


func (m *AssignMutator) getTester() (*patht.Tester, error) {
if m.tester != nil {
return m.tester, nil
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an RWMutex

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

pkg/mutation/assign_mutator.go Outdated Show resolved Hide resolved
pkg/mutation/path/tester/tester.go Outdated Show resolved Hide resolved
@maxsmythe maxsmythe force-pushed the pathtest branch 2 times, most recently from 40a6dc1 to a7ee588 Compare February 2, 2021 05:06
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>

func (m *AssignMutator) getTester() (*patht.Tester, error) {
if m.tester != nil {
return m.tester, nil
Copy link
Contributor

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.

@@ -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()
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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. 👍🏻

Copy link
Contributor Author

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]
Copy link
Contributor

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?

Copy link
Contributor Author

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:

  1. Validate Mutate inputs using the minimum standards necessary for making a coherent mutate call
  2. Add more location validation to Assign (I think AssignMeta enforces using a metadata prefix)

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@maxsmythe maxsmythe Feb 6, 2021

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this dereference safe?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

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
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 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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added nil checking

pkg/mutation/assign_mutator.go Outdated Show resolved Hide resolved
Signed-off-by: Max Smythe <smythe@google.com>
@codecov-io
Copy link

codecov-io commented Feb 6, 2021

Codecov Report

Merging #1101 (118abb7) into master (be2f5b4) will increase coverage by 0.30%.
The diff coverage is 57.48%.

Impacted file tree graph

@@            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              
Flag Coverage Δ
unittests 48.39% <57.48%> (+0.30%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
pkg/audit/manager.go 0.00% <0.00%> (ø)
pkg/controller/constraint/constraint_controller.go 4.22% <0.00%> (-0.02%) ⬇️
pkg/controller/sync/sync_controller.go 0.00% <0.00%> (ø)
pkg/metrics/exporter.go 0.00% <0.00%> (ø)
pkg/readiness/ready_tracker.go 69.09% <ø> (-0.73%) ⬇️
pkg/util/pack.go 0.00% <0.00%> (ø)
pkg/watch/replay.go 71.08% <ø> (+0.96%) ⬆️
pkg/webhook/mutation.go 12.68% <0.00%> (ø)
pkg/webhook/policy.go 27.31% <0.00%> (ø)
pkg/target/target.go 66.03% <25.00%> (ø)
... and 16 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9d424ff...118abb7. Read the comment docs.

Copy link
Contributor

@shomron shomron left a comment

Choose a reason for hiding this comment

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

LGTM!

@maxsmythe
Copy link
Contributor Author

🎊

@maxsmythe maxsmythe merged commit e8ed13c into open-policy-agent:master Feb 9, 2021
jdolce pushed a commit to jdolce/gatekeeper that referenced this pull request Feb 12, 2021
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mutation: Add Path Tests to Assign Mutator
3 participants