diff --git a/constraint/pkg/client/matchers.go b/constraint/pkg/client/matchers.go new file mode 100644 index 000000000..f20458000 --- /dev/null +++ b/constraint/pkg/client/matchers.go @@ -0,0 +1,158 @@ +package client + +import ( + "fmt" + "sort" + "sync" + + "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" + "github.com/open-policy-agent/frameworks/constraint/pkg/core/constraints" +) + +// matcherKey uniquely identifies a Matcher. +// For a given Constraint (uniquely identified by Kind/Name), there is at most +// one Matcher for each Target. +type matcherKey struct { + target string + kind string + name string +} + +// constraintMatchers tracks the Matchers for each Constraint. +// Filters Constraints relevant to a passed review. +type constraintMatchers struct { + // matchers is the set of Constraint matchers by their Target, Kind, and Name. + // matchers is a map from Target to a map from Kind to a map from Name to matcher. + matchers map[string]map[string]map[string]constraints.Matcher + + mtx sync.RWMutex +} + +// Add inserts the Matcher for the Constraint with kind and name. +// Replaces the current Matcher if one already exists. +func (c *constraintMatchers) Add(key matcherKey, matcher constraints.Matcher) { + c.mtx.Lock() + defer c.mtx.Unlock() + + if c.matchers == nil { + c.matchers = make(map[string]map[string]map[string]constraints.Matcher) + } + + targetMatchers := c.matchers[key.target] + if targetMatchers == nil { + targetMatchers = make(map[string]map[string]constraints.Matcher) + } + + kindMatchers := targetMatchers[key.kind] + if kindMatchers == nil { + kindMatchers = make(map[string]constraints.Matcher) + } + + kindMatchers[key.name] = matcher + targetMatchers[key.kind] = kindMatchers + c.matchers[key.target] = targetMatchers +} + +// Remove deletes the Matcher for the Constraint with kind and name. +// Returns normally if no entry for the Constraint existed. +func (c *constraintMatchers) Remove(key matcherKey) { + c.mtx.Lock() + defer c.mtx.Unlock() + + if len(c.matchers) == 0 { + return + } + + targetMatchers := c.matchers[key.target] + if len(targetMatchers) == 0 { + return + } + + kindMatchers := targetMatchers[key.kind] + if len(kindMatchers) == 0 { + return + } + + delete(kindMatchers, key.name) + + // Remove empty parents to avoid memory leaks. + if len(kindMatchers) == 0 { + delete(targetMatchers, key.kind) + } else { + targetMatchers[key.kind] = kindMatchers + } + + if len(targetMatchers) == 0 { + delete(c.matchers, key.target) + } else { + c.matchers[key.target] = targetMatchers + } +} + +// RemoveAll removes all Matchers for Constraints with kind. +// Returns normally if no entry for the kind existed. +func (c *constraintMatchers) RemoveAll(kind string) { + c.mtx.Lock() + defer c.mtx.Unlock() + + if len(c.matchers) == 0 { + return + } + + for h, handlerMatchers := range c.matchers { + delete(handlerMatchers, kind) + + if len(handlerMatchers) == 0 { + // It is safe to delete keys from a map while traversing it. + delete(c.matchers, h) + } else { + c.matchers[h] = handlerMatchers + } + } + + delete(c.matchers, kind) +} + +// ConstraintsFor returns the set of Constraints which should run against review +// according to their Matchers. Returns a map from Kind to the names of the +// Constraints of that Kind which should be run against review. +// +// Returns errors for each Constraint which was unable to properly run match +// criteria. +func (c *constraintMatchers) ConstraintsFor(review interface{}) (map[string]map[string][]string, error) { + c.mtx.RLock() + defer c.mtx.RUnlock() + + result := make(map[string]map[string][]string) + + errs := errors.ErrorMap{} + + for target, targetMatchers := range c.matchers { + resultTargetMatchers := make(map[string][]string) + for kind, kindMatchers := range targetMatchers { + var resultKindMatchers []string + + for name, matcher := range kindMatchers { + if matches, err := matcher.Match(review); err != nil { + // key uniquely identifies the Constraint whose matcher was unable to + // run, for use in debugging. + key := fmt.Sprintf("%s %s %s", target, kind, name) + errs[key] = err + } else if matches { + resultKindMatchers = append(resultKindMatchers, name) + } + } + + sort.Strings(resultKindMatchers) + resultTargetMatchers[kind] = resultKindMatchers + } + + result[target] = resultTargetMatchers + } + + if len(errs) > 0 { + return nil, &errs + } + + return result, nil +} diff --git a/constraint/pkg/client/matchers_test.go b/constraint/pkg/client/matchers_test.go new file mode 100644 index 000000000..d8e687158 --- /dev/null +++ b/constraint/pkg/client/matchers_test.go @@ -0,0 +1,560 @@ +package client + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" + "github.com/open-policy-agent/frameworks/constraint/pkg/core/constraints" + "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" +) + +func TestConstraintMatchers_Add(t *testing.T) { + tests := []struct { + name string + before *constraintMatchers + key matcherKey + matcher constraints.Matcher + want *constraintMatchers + }{ + { + name: "add to empty", + before: &constraintMatchers{}, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + matcher: handlertest.Matcher{}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + }, + { + name: "overwrite with identical", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + matcher: handlertest.Matcher{}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + }, + { + name: "overwrite with new", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + matcher: handlertest.Matcher{Namespace: "bbb"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "bbb"}, + }, + }, + }, + }, + }, + { + name: "add with different name", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "cog"}, + matcher: handlertest.Matcher{Namespace: "bbb"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + "cog": handlertest.Matcher{Namespace: "bbb"}, + }, + }, + }, + }, + }, + { + name: "add with different kind", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "cog", name: "qux"}, + matcher: handlertest.Matcher{Namespace: "bbb"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + "cog": { + "qux": handlertest.Matcher{Namespace: "bbb"}, + }, + }, + }, + }, + }, + { + name: "add with different target", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + }, + }, + }, + key: matcherKey{target: "cog", kind: "bar", name: "qux"}, + matcher: handlertest.Matcher{Namespace: "bbb"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{Namespace: "aaa"}, + }, + }, + "cog": { + "bar": { + "qux": handlertest.Matcher{Namespace: "bbb"}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.before + + got.Add(tt.key, tt.matcher) + + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(constraintMatchers{})); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestConstraintMatchers_Remove(t *testing.T) { + tests := []struct { + name string + before *constraintMatchers + key matcherKey + want *constraintMatchers + }{ + { + name: "remove from empty", + before: &constraintMatchers{}, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{}, + }, + { + name: "remove from empty target", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": {}, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": {}, + }, + }, + }, + { + name: "remove from empty kind", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": {}, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": {}, + }, + }, + }, + }, + { + name: "remove last from target", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{}, + }, + }, + { + name: "remove last from kind", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + "cog": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "cog": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + }, + { + name: "remove", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + "cog": handlertest.Matcher{}, + }, + }, + }, + }, + key: matcherKey{target: "foo", kind: "bar", name: "qux"}, + want: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "cog": handlertest.Matcher{}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.before + + got.Remove(tt.key) + + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(constraintMatchers{})); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestConstraintMatchers_RemoveAll(t *testing.T) { + tests := []struct { + name string + before *constraintMatchers + kind string + want *constraintMatchers + }{ + { + name: "remove from empty", + before: &constraintMatchers{}, + kind: "bar", + want: &constraintMatchers{}, + }, + { + name: "remove from empty handler", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": {}, + }, + }, + kind: "bar", + want: &constraintMatchers{}, + }, + { + name: "remove last from handler", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + kind: "bar", + want: &constraintMatchers{}, + }, + { + name: "remove from handler", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + "cog": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + kind: "bar", + want: &constraintMatchers{}, + }, + { + name: "remove from multiple handlers", + before: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + "cog": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + kind: "bar", + want: &constraintMatchers{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.before + + got.RemoveAll(tt.kind) + + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(constraintMatchers{})); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestConstraintMatchers_ConstraintsFor(t *testing.T) { + tests := []struct { + name string + matchers *constraintMatchers + review interface{} + want map[string]map[string][]string + wantErrs error + }{ + { + name: "no matchers", + matchers: &constraintMatchers{}, + review: &handlertest.Review{ + Object: handlertest.Object{Namespace: "aaa"}, + }, + want: map[string]map[string][]string{}, + wantErrs: nil, + }, + { + name: "match one", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + review: &handlertest.Review{ + Object: handlertest.Object{Namespace: "aaa"}, + }, + want: map[string]map[string][]string{ + "foo": { + "bar": []string{"qux"}, + }, + }, + wantErrs: nil, + }, + { + name: "match two same kind", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + "cog": handlertest.Matcher{}, + }, + }, + }, + }, + review: &handlertest.Review{ + Object: handlertest.Object{}, + }, + want: map[string]map[string][]string{ + "foo": { + "bar": []string{"cog", "qux"}, + }, + }, + wantErrs: nil, + }, + { + name: "match two different kinds", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + "cog": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + review: &handlertest.Review{ + Object: handlertest.Object{}, + }, + want: map[string]map[string][]string{ + "foo": { + "bar": []string{"qux"}, + "cog": []string{"qux"}, + }, + }, + wantErrs: nil, + }, + { + name: "match two different targets", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + "cog": { + "bar": { + "qux": handlertest.Matcher{}, + }, + }, + }, + }, + review: &handlertest.Review{ + Object: handlertest.Object{}, + }, + want: map[string]map[string][]string{ + "foo": { + "bar": []string{"qux"}, + }, + "cog": { + "bar": []string{"qux"}, + }, + }, + wantErrs: nil, + }, + { + name: "match one but not the other", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{ + Namespace: "aaa", + }, + "cog": handlertest.Matcher{ + Namespace: "bbb", + }, + }, + }, + }, + }, + review: &handlertest.Review{ + Object: handlertest.Object{Namespace: "aaa"}, + }, + want: map[string]map[string][]string{ + "foo": { + "bar": []string{"qux"}, + }, + }, + wantErrs: nil, + }, + { + name: "error matching", + matchers: &constraintMatchers{ + matchers: map[string]map[string]map[string]constraints.Matcher{ + "foo": { + "bar": { + "qux": handlertest.Matcher{ + Namespace: "aaa", + }, + }, + }, + }, + }, + review: "nope", + want: nil, + wantErrs: &errors.ErrorMap{ + "foo bar qux": handlertest.ErrInvalidType, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.matchers.ConstraintsFor(tt.review) + if diff := cmp.Diff(tt.wantErrs, err, cmpopts.EquateErrors()); diff != "" { + t.Fatal(diff) + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Error(diff) + } + }) + } +} diff --git a/constraint/pkg/handler/handlertest/handler.go b/constraint/pkg/handler/handlertest/handler.go index 1b5c15a5a..c3009b269 100644 --- a/constraint/pkg/handler/handlertest/handler.go +++ b/constraint/pkg/handler/handlertest/handler.go @@ -167,5 +167,5 @@ func (h *Handler) ToMatcher(constraint *unstructured.Unstructured) (constraints. return nil, fmt.Errorf("unable to get spec.matchNamespace: %w", err) } - return Matcher{namespace: ns}, nil + return Matcher{Namespace: ns}, nil } diff --git a/constraint/pkg/handler/handlertest/matcher.go b/constraint/pkg/handler/handlertest/matcher.go index 7ebca777a..d7b158c5e 100644 --- a/constraint/pkg/handler/handlertest/matcher.go +++ b/constraint/pkg/handler/handlertest/matcher.go @@ -1,27 +1,30 @@ package handlertest import ( + "errors" "fmt" "github.com/open-policy-agent/frameworks/constraint/pkg/core/constraints" ) +var ErrInvalidType = errors.New("unrecognized type") + type Matcher struct { - namespace string + Namespace string } func (m Matcher) Match(review interface{}) (bool, error) { - if m.namespace == "" { + if m.Namespace == "" { return true, nil } reviewObj, ok := review.(*Review) if !ok { - return false, fmt.Errorf("unrecognized type %T, want %T", - review, &Review{}) + return false, fmt.Errorf("%w: got %T, want %T", + ErrInvalidType, review, &Review{}) } - return m.namespace == reviewObj.Object.Namespace, nil + return m.Namespace == reviewObj.Object.Namespace, nil } var _ constraints.Matcher = Matcher{}