From 0af9133c11e31a7bc9137cec8dbbc5a556c4eff2 Mon Sep 17 00:00:00 2001 From: Federico Paolinelli Date: Tue, 1 Dec 2020 00:55:19 +0100 Subject: [PATCH] Implement the match logic (#932) * Implement matching logic Adding a Match function that accepts a Match instance, an object to be matched against and its related namespace. Adding a AppliesTo function that accepts a ApplyTo slice and an object to be matched against. Signed-off-by: Federico Paolinelli * Integrate the mutators with the match logic Each mutator matches uses the Matches function to check if the given object matches the mutator. This also includes a change in the mutator interface as metav1.Object do not have accessors for kind / group. Signed-off-by: Federico Paolinelli Co-authored-by: Rita Zhang --- pkg/mutation/assign_mutator.go | 12 +- pkg/mutation/assignmeta_mutator.go | 12 +- pkg/mutation/match.go | 157 ++++++++++ pkg/mutation/match_test.go | 471 +++++++++++++++++++++++++++++ pkg/mutation/mutation.go | 7 +- pkg/mutation/mutator.go | 3 +- pkg/mutation/system_test.go | 2 +- 7 files changed, 652 insertions(+), 12 deletions(-) create mode 100644 pkg/mutation/match.go create mode 100644 pkg/mutation/match_test.go diff --git a/pkg/mutation/assign_mutator.go b/pkg/mutation/assign_mutator.go index cae919d75f6..2dd16b4ec4a 100644 --- a/pkg/mutation/assign_mutator.go +++ b/pkg/mutation/assign_mutator.go @@ -6,8 +6,8 @@ import ( "github.com/open-policy-agent/gatekeeper/pkg/mutation/path/parser" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" ) // AssignMutator is a mutator object built out of a @@ -22,9 +22,13 @@ type AssignMutator struct { // AssignMutator implements mutatorWithSchema var _ MutatorWithSchema = &AssignMutator{} -func (m *AssignMutator) Matches(obj metav1.Object, ns *corev1.Namespace) bool { - // TODO implement using matches function - return false +func (m *AssignMutator) Matches(obj runtime.Object, ns *corev1.Namespace) bool { + matches, err := Matches(m.assign.Spec.Match, obj, ns) + if err != nil { + log.Error(err, "AssignMutator.Matches failed", "assign", m.assign.Name) + return false + } + return matches } func (m *AssignMutator) Mutate(obj *unstructured.Unstructured) error { diff --git a/pkg/mutation/assignmeta_mutator.go b/pkg/mutation/assignmeta_mutator.go index f7a9f98c948..c668ccf1437 100644 --- a/pkg/mutation/assignmeta_mutator.go +++ b/pkg/mutation/assignmeta_mutator.go @@ -9,8 +9,8 @@ import ( "github.com/open-policy-agent/gatekeeper/pkg/mutation/path/parser" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" ) var ( @@ -44,9 +44,13 @@ type AssignMetadataMutator struct { // assignMetadataMutator implements mutator var _ Mutator = &AssignMetadataMutator{} -func (m *AssignMetadataMutator) Matches(obj metav1.Object, ns *corev1.Namespace) bool { - // TODO implement using matches function - return false +func (m *AssignMetadataMutator) Matches(obj runtime.Object, ns *corev1.Namespace) bool { + matches, err := Matches(m.assignMetadata.Spec.Match, obj, ns) + if err != nil { + log.Error(err, "AssignMetadataMutator.Matches failed", "assignMeta", m.assignMetadata.Name) + return false + } + return matches } func (m *AssignMetadataMutator) Mutate(obj *unstructured.Unstructured) error { diff --git a/pkg/mutation/match.go b/pkg/mutation/match.go new file mode 100644 index 00000000000..994e9edd318 --- /dev/null +++ b/pkg/mutation/match.go @@ -0,0 +1,157 @@ +package mutation + +import ( + "fmt" + + mutationsv1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// Matches verifies if the given object belonging to the given namespace +// matches the current mutator. +func Matches(match mutationsv1.Match, obj runtime.Object, ns *corev1.Namespace) (bool, error) { + meta, err := meta.Accessor(obj) + if err != nil { + return false, fmt.Errorf("Accessor failed for %s", obj.GetObjectKind().GroupVersionKind().Kind) + } + + foundMatch := false + + for _, kk := range match.Kinds { + kindMatches := false + groupMatches := false + + for _, k := range kk.Kinds { + if k == "*" || k == obj.GetObjectKind().GroupVersionKind().Kind { + kindMatches = true + break + } + } + if len(kk.Kinds) == 0 { + kindMatches = true + } + + for _, g := range kk.APIGroups { + if g == "*" || g == obj.GetObjectKind().GroupVersionKind().Group { + groupMatches = true + break + } + } + if len(kk.APIGroups) == 0 { + groupMatches = true + } + + if kindMatches && groupMatches { + foundMatch = true + } + } + if len(match.Kinds) == 0 { + foundMatch = true + } + + if !foundMatch { + return false, nil + } + + if match.Scope == apiextensionsv1beta1.ClusterScoped && + meta.GetNamespace() != "" { + return false, nil + } + + if match.Scope == apiextensionsv1beta1.NamespaceScoped && + meta.GetNamespace() == "" { + return false, nil + } + + found := false + for _, n := range match.Namespaces { + if meta.GetNamespace() == n { + found = true + break + } + } + if !found && len(match.Namespaces) > 0 { + return false, nil + } + + for _, n := range match.ExcludedNamespaces { + if meta.GetNamespace() == n { + return false, nil + } + } + if match.LabelSelector != nil { + selector, err := metav1.LabelSelectorAsSelector(match.LabelSelector) + if err != nil { + return false, err + } + if !selector.Matches(labels.Set(meta.GetLabels())) { + return false, nil + } + } + + if match.NamespaceSelector != nil { + selector, err := metav1.LabelSelectorAsSelector(match.NamespaceSelector) + if err != nil { + return false, err + } + + switch { + case isNamespace(obj): // if the object is a namespace, namespace selector matches against the object + if !selector.Matches(labels.Set(meta.GetLabels())) { + return false, nil + } + case meta.GetNamespace() == "": + // cluster scoped, matches by default + case !selector.Matches(labels.Set(ns.Labels)): + return false, nil + } + } + + return true, nil +} + +// AppliesTo checks if any item the given slice of ApplyTo applies to the given object +func AppliesTo(applyTo []mutationsv1.ApplyTo, obj *unstructured.Unstructured) bool { + for _, apply := range applyTo { + matchesGroup := false + matchesVersion := false + matchesKind := false + + gvk := obj.GroupVersionKind() + for _, g := range apply.Groups { + if g == gvk.Group { + matchesGroup = true + break + } + } + for _, g := range apply.Versions { + if g == gvk.Version { + matchesVersion = true + break + } + } + for _, g := range apply.Kinds { + if g == gvk.Kind { + matchesKind = true + break + } + } + if matchesGroup && + matchesVersion && + matchesKind { + return true + } + } + return false +} + +func isNamespace(obj runtime.Object) bool { + return obj.GetObjectKind().GroupVersionKind().Kind == "Namespace" && + obj.GetObjectKind().GroupVersionKind().Group == "" +} diff --git a/pkg/mutation/match_test.go b/pkg/mutation/match_test.go new file mode 100644 index 00000000000..6c030ce5bc9 --- /dev/null +++ b/pkg/mutation/match_test.go @@ -0,0 +1,471 @@ +package mutation_test + +import ( + "testing" + + configv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/config/v1alpha1" + mutationsv1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" + "github.com/open-policy-agent/gatekeeper/pkg/mutation" + corev1 "k8s.io/api/core/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestMatch(t *testing.T) { + table := []struct { + tname string + toMatch *unstructured.Unstructured + match mutationsv1.Match + namespace *corev1.Namespace + shouldMatch bool + }{ + { + tname: "match kind with *", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"*"}, + APIGroups: []string{"*"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "match group and no kinds specified should match", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"notmatching", "neithermatching"}, + APIGroups: []string{"*"}, + }, + { + APIGroups: []string{"*"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "match kind and no group specified should match", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind", "neithermatching"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "match kind and group explicit", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"notmatching", "neithermatching"}, + APIGroups: []string{"*"}, + }, + { + Kinds: []string{"notmatching", "kind"}, + APIGroups: []string{"*"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "kind group don't matches", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"notmatching", "neithermatching"}, + APIGroups: []string{"*"}, + }, + { + Kinds: []string{"notmatching", "kind"}, + APIGroups: []string{"*"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "kind group don't matches", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"notmatching", "neithermatching"}, + APIGroups: []string{"*"}, + }, + { + Kinds: []string{"notmatching", "kind"}, + APIGroups: []string{"notmatchinggroup"}, + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "namespace matches", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Namespaces: []string{"nonmatching", "namespace"}, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "namespace is not in the matches list", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Namespaces: []string{"nonmatching", "notmatchingeither"}, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "namespace fails if clusterscoped", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Namespaces: []string{"nonmatching", "namespace"}, + Scope: apiextensionsv1beta1.ClusterScoped, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "namespace is excluded", + toMatch: makeObject("kind", "group", "namespace", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + Namespaces: []string{"nonmatching", "namespace"}, + ExcludedNamespaces: []string{"namespace"}, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "namespace scoped fails if cluster scoped", + toMatch: makeObject("kind", "group", "", "name"), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + Scope: apiextensionsv1beta1.NamespaceScoped, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "label selector", + toMatch: makeObject("kind", "group", "", "name", func(o *unstructured.Unstructured) { + meta, _ := meta.Accessor(o) + meta.SetLabels(map[string]string{ + "labelname": "labelvalue", + }) + }), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: true, + }, + { + tname: "label selector not matching", + toMatch: makeObject("kind", "group", "", "name", func(o *unstructured.Unstructured) { + meta, _ := meta.Accessor(o) + meta.SetLabels(map[string]string{ + "labelname": "labelvalue", + }) + }), + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + "labelnotmatching": "foo", + }, + }, + }, + namespace: &corev1.Namespace{}, + shouldMatch: false, + }, + { + tname: "namespace selector", + toMatch: makeObject("kind", "group", "", "name"), + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "labelname": "labelvalue", + }, + }, + }, + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + }, + }, + }, + shouldMatch: true, + }, + { + tname: "namespace selector not matching", + toMatch: makeObject("kind", "group", "foo", "name"), + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "labelname": "labelvalue", + }, + }, + }, + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + "foo": "bar", + }, + }, + }, + shouldMatch: false, + }, + { + tname: "namespace selector not matching, but cluster scoped", + toMatch: makeObject("kind", "group", "", "name"), + namespace: nil, + match: mutationsv1.Match{ + Kinds: []mutationsv1.Kinds{ + { + Kinds: []string{"kind"}, + APIGroups: []string{"group"}, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + "foo": "bar", + }, + }, + }, + shouldMatch: true, + }, + { + tname: "namespace selector is applied to the object, if the object is a namespace", + toMatch: makeNamespace("namespace", func(o *unstructured.Unstructured) { + meta, _ := meta.Accessor(o) + meta.SetLabels(map[string]string{ + "labelname": "labelvalue", + }) + }), + namespace: nil, + match: mutationsv1.Match{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "labelvalue", + }, + }, + }, + shouldMatch: true, + }, + { + tname: "namespace selector is applied to the namespace, and does not matches", + toMatch: makeNamespace("namespace", func(o *unstructured.Unstructured) { + meta, _ := meta.Accessor(o) + meta.SetLabels(map[string]string{ + "labelname": "labelvalue", + }) + }), + namespace: nil, + match: mutationsv1.Match{ + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "labelname": "badvalue", + }, + }, + }, + shouldMatch: false, + }, + } + for _, tc := range table { + t.Run(tc.tname, func(t *testing.T) { + matches, err := mutation.Matches(tc.match, tc.toMatch, tc.namespace) + if err != nil { + t.Error("Match failed for ", tc.tname) + } + if matches != tc.shouldMatch { + t.Errorf("%s: expecting match to be %v, was %v", tc.tname, tc.shouldMatch, matches) + } + }) + } +} + +func makeObject(kind, group, namespace, name string, options ...func(*unstructured.Unstructured)) *unstructured.Unstructured { + config := &configv1alpha1.Config{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + gvk := schema.GroupVersionKind{ + Kind: kind, + Group: group, + Version: "v1", + } + config.APIVersion, config.Kind = gvk.ToAPIVersionAndKind() + unstruct, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(config) + + res := &unstructured.Unstructured{Object: unstruct} + for _, o := range options { + o(res) + } + return res +} + +func makeNamespace(name string, options ...func(*unstructured.Unstructured)) *unstructured.Unstructured { + namespace := &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{ + Kind: "Namespace", + APIVersion: "", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + unstruct, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(namespace) + + res := &unstructured.Unstructured{Object: unstruct} + for _, o := range options { + o(res) + } + return res +} + +func TestApplyTo(t *testing.T) { + table := []struct { + tname string + toMatch *unstructured.Unstructured + applyTo []mutationsv1.ApplyTo + shouldApply bool + }{ + { + tname: "one item, applies", + toMatch: makeObject("kind", "group", "namespace", "name"), + applyTo: []mutationsv1.ApplyTo{ + { + Groups: []string{"group"}, + Kinds: []string{"kind"}, + Versions: []string{"v1"}, + }, + }, + shouldApply: true, + }, + { + tname: "one item, many columns", + toMatch: makeObject("kind", "group", "namespace", "name"), + applyTo: []mutationsv1.ApplyTo{ + { + Groups: []string{"aa", "bb", "group"}, + Kinds: []string{"aa", "bb", "kind"}, + Versions: []string{"aa", "bb", "v1"}, + }, + }, + shouldApply: true, + }, + { + tname: "first don't match, second does", + toMatch: makeObject("kind", "group", "namespace", "name"), + applyTo: []mutationsv1.ApplyTo{ + { + Groups: []string{"group"}, + Kinds: []string{"not matching"}, + Versions: []string{"v1"}, + }, + { + Groups: []string{"group"}, + Kinds: []string{"kind"}, + Versions: []string{"v1"}, + }, + }, + shouldApply: true, + }, + { + tname: "no one is matching", + toMatch: makeObject("kind", "group", "namespace", "name"), + applyTo: []mutationsv1.ApplyTo{ + { + Groups: []string{"group"}, + Kinds: []string{"not matching"}, + Versions: []string{"v1"}, + }, + { + Groups: []string{"neither", "neither1"}, + Kinds: []string{"kind"}, + Versions: []string{"v1"}, + }, + }, + shouldApply: false, + }, + } + for _, tc := range table { + t.Run(tc.tname, func(t *testing.T) { + appliesTo := mutation.AppliesTo(tc.applyTo, tc.toMatch) + if appliesTo != tc.shouldApply { + t.Errorf("%s: expecting match to be %v, was %v", tc.tname, tc.shouldApply, appliesTo) + } + }) + } +} diff --git a/pkg/mutation/mutation.go b/pkg/mutation/mutation.go index 407a7a42da4..d6dfa5ea99d 100644 --- a/pkg/mutation/mutation.go +++ b/pkg/mutation/mutation.go @@ -14,10 +14,15 @@ package mutation import ( "flag" + + logf "sigs.k8s.io/controller-runtime/pkg/log" ) // MutationEnabled indicates if the mutation feature is enabled -var MutationEnabled *bool +var ( + MutationEnabled *bool + log = logf.Log.WithName("mutation") +) func init() { MutationEnabled = flag.Bool("enable-mutation", false, "(alpha) Enable the mutation feature") diff --git a/pkg/mutation/mutator.go b/pkg/mutation/mutator.go index 98dd8e26ae6..22a1abf0b0f 100644 --- a/pkg/mutation/mutator.go +++ b/pkg/mutation/mutator.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ) @@ -29,7 +28,7 @@ type SchemaBinding struct { // Mutator represent a mutation object. type Mutator interface { // Matches tells if the given object is eligible for this mutation. - Matches(obj metav1.Object, ns *corev1.Namespace) bool + Matches(obj runtime.Object, ns *corev1.Namespace) bool // Mutate applies the mutation to the given object Mutate(obj *unstructured.Unstructured) error // ID returns the id of the current mutator. diff --git a/pkg/mutation/system_test.go b/pkg/mutation/system_test.go index cc2a6a22951..4f2e209a028 100644 --- a/pkg/mutation/system_test.go +++ b/pkg/mutation/system_test.go @@ -23,7 +23,7 @@ type MockMutator struct { UnstableFor int // makes the mutation unstable for the first n mutations } -func (m *MockMutator) Matches(obj metav1.Object, ns *corev1.Namespace) bool { +func (m *MockMutator) Matches(obj runtime.Object, ns *corev1.Namespace) bool { return true // always matches }