diff --git a/pkg/mutation/assign_mutator.go b/pkg/mutation/assign_mutator.go new file mode 100644 index 00000000000..da9ad60d5ce --- /dev/null +++ b/pkg/mutation/assign_mutator.go @@ -0,0 +1,117 @@ +package mutation + +import ( + "github.com/google/go-cmp/cmp" + mutationsv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" + "github.com/open-policy-agent/gatekeeper/pkg/mutation/path" + "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" +) + +// AssignMutator is a mutator object built out of a +// Assign instance. +type AssignMutator struct { + id ID + assign *mutationsv1alpha1.Assign + path []path.Entry + bindings []SchemaBinding +} + +// 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) Mutate(obj *unstructured.Unstructured) error { + // TODO implement + return nil +} +func (m *AssignMutator) ID() ID { + return m.id +} + +func (m *AssignMutator) SchemaBindings() []SchemaBinding { + return m.bindings +} + +func (m *AssignMutator) HasDiff(mutator Mutator) bool { + toCheck, ok := mutator.(*AssignMutator) + if !ok { // different types, different + return true + } + + if !cmp.Equal(toCheck.id, m.id) { + return true + } + if !cmp.Equal(toCheck.path, m.path) { + return true + } + if !cmp.Equal(toCheck.bindings, m.bindings) { + return true + } + + // any difference in spec may be enough + if !cmp.Equal(toCheck.assign.Spec, m.assign.Spec) { + return true + } + + return false +} + +func (m *AssignMutator) Path() []path.Entry { + return m.path +} + +func (m *AssignMutator) DeepCopy() Mutator { + res := &AssignMutator{ + id: m.id, + assign: m.assign.DeepCopy(), + path: make([]path.Entry, len(m.path)), + bindings: make([]SchemaBinding, len(m.bindings)), + } + copy(res.path, m.path) + copy(res.bindings, m.bindings) + return res +} + +// MutatorForAssign returns an AssignMutator built from +// the given assign instance. +func MutatorForAssign(assign *mutationsv1alpha1.Assign) (*AssignMutator, error) { + id, err := MakeID(assign) + if err != nil { + return nil, errors.Wrapf(err, "Failed to retrieve id for assign type") + } + return &AssignMutator{ + id: id, + assign: assign.DeepCopy(), + bindings: applyToToBindings(assign.Spec.ApplyTo), + path: nil, // TODO fill when the parsing is done + }, nil +} + +func applyToToBindings(applyTos []mutationsv1alpha1.ApplyTo) []SchemaBinding { + res := []SchemaBinding{} + for _, applyTo := range applyTos { + binding := SchemaBinding{ + Groups: make([]string, len(applyTo.Groups)), + Kinds: make([]string, len(applyTo.Kinds)), + Versions: make([]string, len(applyTo.Versions)), + } + for i, g := range applyTo.Groups { + binding.Groups[i] = g + } + for i, k := range applyTo.Kinds { + binding.Kinds[i] = k + } + for i, v := range applyTo.Versions { + binding.Versions[i] = v + } + res = append(res, binding) + } + return res +} diff --git a/pkg/mutation/assignmeta_mutator.go b/pkg/mutation/assignmeta_mutator.go new file mode 100644 index 00000000000..8b3e8732e97 --- /dev/null +++ b/pkg/mutation/assignmeta_mutator.go @@ -0,0 +1,70 @@ +package mutation + +import ( + "github.com/google/go-cmp/cmp" + mutationsv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" + "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" +) + +//AssignMetadataMutator is a mutator built out of an +// AssignMeta instance. +type AssignMetadataMutator struct { + id ID + assignMetadata *mutationsv1alpha1.AssignMetadata +} + +// 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) Mutate(obj *unstructured.Unstructured) error { + // TODO implement + return nil +} +func (m *AssignMetadataMutator) ID() ID { + return m.id +} + +func (m *AssignMetadataMutator) HasDiff(mutator Mutator) bool { + toCheck, ok := mutator.(*AssignMetadataMutator) + if !ok { // different types, different + return true + } + + if !cmp.Equal(toCheck.id, m.id) { + return true + } + // any difference in spec may be enough + if !cmp.Equal(toCheck.assignMetadata.Spec, m.assignMetadata.Spec) { + return true + } + + return false +} + +func (m *AssignMetadataMutator) DeepCopy() Mutator { + res := &AssignMetadataMutator{ + id: m.id, + assignMetadata: m.assignMetadata.DeepCopy(), + } + return res +} + +// MutatorForAssignMetadata builds an AssignMetadataMutator from the given AssignMetadata object. +func MutatorForAssignMetadata(assignMeta *mutationsv1alpha1.AssignMetadata) (*AssignMetadataMutator, error) { + id, err := MakeID(assignMeta) + if err != nil { + return nil, errors.Wrapf(err, "Failed to retrieve id for assignMetadata type") + } + return &AssignMetadataMutator{ + id: id, + assignMetadata: assignMeta.DeepCopy(), + }, nil +} diff --git a/pkg/mutation/conversion_test.go b/pkg/mutation/conversion_test.go new file mode 100644 index 00000000000..b5f3f245958 --- /dev/null +++ b/pkg/mutation/conversion_test.go @@ -0,0 +1,251 @@ +package mutation_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + mutationsv1alpha1 "github.com/open-policy-agent/gatekeeper/apis/mutations/v1alpha1" + "github.com/open-policy-agent/gatekeeper/pkg/mutation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestAssignToMutator(t *testing.T) { + assign := &mutationsv1alpha1.Assign{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: mutationsv1alpha1.AssignSpec{ + ApplyTo: []mutationsv1alpha1.ApplyTo{ + { + Groups: []string{"group1", "group2"}, + Kinds: []string{"kind1", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + { + Groups: []string{"group3", "group4"}, + Kinds: []string{"kind4", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + }, + Match: mutationsv1alpha1.Match{}, + Location: "spec.foo", + Parameters: mutationsv1alpha1.Parameters{}, + }, + } + + mutatorWithSchema, err := mutation.MutatorForAssign(assign) + if err != nil { + t.Fatalf("MutatorForAssign failed, %v", err) + } + + bindings := mutatorWithSchema.SchemaBindings() + expectedBindings := []mutation.SchemaBinding{ + { + Groups: []string{"group1", "group2"}, + Kinds: []string{"kind1", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + { + Groups: []string{"group3", "group4"}, + Kinds: []string{"kind4", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + } + + if !cmp.Equal(bindings, expectedBindings) { + t.Errorf("Bindings are not as expected: %s", cmp.Diff(bindings, expectedBindings)) + } + +} + +func TestAssignMetadataToMutator(t *testing.T) { + assignMeta := &mutationsv1alpha1.AssignMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: mutationsv1alpha1.AssignMetadataSpec{ + Match: mutationsv1alpha1.Match{}, + Location: "spec.foo", + Parameters: mutationsv1alpha1.MetadataParameters{}, + }, + } + + _, err := mutation.MutatorForAssignMetadata(assignMeta) + if err != nil { + t.Fatalf("MutatorForAssignMetadata for failed, %v", err) + } +} + +func TestAssignHasDiff(t *testing.T) { + first := &mutationsv1alpha1.Assign{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: mutationsv1alpha1.AssignSpec{ + ApplyTo: []mutationsv1alpha1.ApplyTo{ + { + Groups: []string{"group1", "group2"}, + Kinds: []string{"kind1", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + { + Groups: []string{"group3", "group4"}, + Kinds: []string{"kind4", "kind2", "kind3"}, + Versions: []string{"version1"}, + }, + }, + Match: mutationsv1alpha1.Match{}, + Location: "spec.foo", + Parameters: mutationsv1alpha1.Parameters{}, + }, + } + // This is normally filled during the serialization + gvk := schema.GroupVersionKind{ + Kind: "kindname", + Group: "groupname", + Version: "versionname", + } + first.APIVersion, first.Kind = gvk.ToAPIVersionAndKind() + + second := first.DeepCopy() + + table := []struct { + tname string + modify func(*mutationsv1alpha1.Assign) + areDifferent bool + }{ + { + "same", + func(a *mutationsv1alpha1.Assign) {}, + false, + }, + { + "differentApplyTo", + func(a *mutationsv1alpha1.Assign) { + a.Spec.ApplyTo[1].Kinds[0] = "kind" + }, + true, + }, + { + "differentLocation", + func(a *mutationsv1alpha1.Assign) { + a.Spec.Location = "bar" + }, + true, + }, + { + "differentParameters", + func(a *mutationsv1alpha1.Assign) { + a.Spec.Parameters.IfIn = []string{"Foo", "Bar"} + }, + true, + }, + } + + for _, tc := range table { + t.Run(tc.tname, func(t *testing.T) { + secondAssign := second.DeepCopy() + tc.modify(secondAssign) + firstMutator, err := mutation.MutatorForAssign(first) + if err != nil { + t.Fatal(tc.tname, "Failed to convert first to mutator", err) + } + secondMutator, err := mutation.MutatorForAssign(secondAssign) + if err != nil { + t.Fatal(tc.tname, "Failed to convert second to mutator", err) + } + hasDiff := firstMutator.HasDiff(secondMutator) + hasDiff1 := secondMutator.HasDiff(firstMutator) + if hasDiff != hasDiff1 { + t.Error(tc.tname, "Diff first from second is different from second to first") + } + if hasDiff != tc.areDifferent { + t.Errorf("test %s: Expected to be different: %v, diff result is %v", tc.tname, tc.areDifferent, hasDiff) + } + }) + } +} + +func TestAssignMetadataHasDiff(t *testing.T) { + first := &mutationsv1alpha1.AssignMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: mutationsv1alpha1.AssignMetadataSpec{ + Match: mutationsv1alpha1.Match{}, + Location: "spec.foo", + Parameters: mutationsv1alpha1.MetadataParameters{}, + }, + } + + // This is normally filled during the serialization + gvk := schema.GroupVersionKind{ + Kind: "kindname", + Group: "groupname", + Version: "versionname", + } + first.APIVersion, first.Kind = gvk.ToAPIVersionAndKind() + + second := first.DeepCopy() + + table := []struct { + tname string + modify func(*mutationsv1alpha1.AssignMetadata) + areDifferent bool + }{ + { + "same", + func(a *mutationsv1alpha1.AssignMetadata) {}, + false, + }, + { + "differentLocation", + func(a *mutationsv1alpha1.AssignMetadata) { + a.Spec.Location = "location" + }, + true, + }, + { + "differentName", + func(a *mutationsv1alpha1.AssignMetadata) { + a.Name = "anothername" + }, + true, + }, + { + "differentMatch", + func(a *mutationsv1alpha1.AssignMetadata) { + a.Spec.Match.Namespaces = []string{"foo", "bar"} + }, + true, + }, + } + + for _, tc := range table { + t.Run(tc.tname, func(t *testing.T) { + secondAssignMeta := second.DeepCopy() + tc.modify(secondAssignMeta) + firstMutator, err := mutation.MutatorForAssignMetadata(first) + if err != nil { + t.Fatal(tc.tname, "Failed to convert first to mutator", err) + } + secondMutator, err := mutation.MutatorForAssignMetadata(secondAssignMeta) + if err != nil { + t.Fatal(tc.tname, "Failed to convert second to mutator", err) + } + hasDiff := firstMutator.HasDiff(secondMutator) + hasDiff1 := secondMutator.HasDiff(firstMutator) + if hasDiff != hasDiff1 { + t.Error(tc.tname, "Diff first from second is different from second to first") + } + if hasDiff != tc.areDifferent { + t.Errorf("test %s: Expected to be different: %v, diff result is %v", tc.tname, tc.areDifferent, hasDiff) + } + }) + } +} diff --git a/pkg/mutation/mutator.go b/pkg/mutation/mutator.go index 93f43c7d99f..117f38e21d1 100644 --- a/pkg/mutation/mutator.go +++ b/pkg/mutation/mutator.go @@ -37,6 +37,8 @@ type Mutator interface { // Has diff tells if the mutator has meaningful differences // with the provided mutator HasDiff(mutator Mutator) bool + + DeepCopy() Mutator } // MutatorWithSchema is a mutator exposing the implied @@ -51,8 +53,7 @@ type MutatorWithSchema interface { func MakeID(obj runtime.Object) (ID, error) { meta, err := meta.Accessor(obj) if err != nil { - return ID{}, errors.Wrapf(err, "Failed to get accessor for %s - %s", obj.GetObjectKind().GroupVersionKind().Group, obj.GetObjectKind().GroupVersionKind().Kind) - + return ID{}, errors.Wrapf(err, "Failed to get accessor for %s %s", obj.GetObjectKind().GroupVersionKind().Group, obj.GetObjectKind().GroupVersionKind().Kind) } return ID{ Group: obj.GetObjectKind().GroupVersionKind().Group,