diff --git a/pkg/syncutil/parser/syncannotationreader.go b/pkg/syncutil/parser/syncannotationreader.go new file mode 100644 index 00000000000..ae22dac98d8 --- /dev/null +++ b/pkg/syncutil/parser/syncannotationreader.go @@ -0,0 +1,87 @@ +package parser + +import ( + "encoding/json" + "strings" + + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// syncAnnotationName is the name of the annotation that stores +// GVKS that are required to be synced. +const SyncAnnotationName = "metadata.gatekeeper.sh/requires-sync-data" + +// SyncRequirements contains a list of ANDed requirements, each of which +// contains an expanded set of equivalent (ORed) GVKs. +type SyncRequirements []GVKEquivalenceSet + +// GVKEquivalenceSet is a set of GVKs that a template can use +// interchangeably in its referential policy implementation. +type GVKEquivalenceSet map[schema.GroupVersionKind]struct{} + +// CompactSyncRequirements contains a list of ANDed requirements, each of +// which contains a list of equivalent (ORed) GVKs in compact form. +type CompactSyncRequirements [][]CompactGVKEquivalenceSet + +// compactGVKEquivalenceSet contains a set of equivalent GVKs, expressed +// in the compact form [groups, versions, kinds] where any combination of +// items from these three fields can be considered a valid equivalent. +// Used for unmarshalling as this is the form used in requiressync annotations. +type CompactGVKEquivalenceSet struct { + Groups []string `json:"groups"` + Versions []string `json:"versions"` + Kinds []string `json:"kinds"` +} + +// ReadSyncRequirements parses the sync requirements from a +// constraint template. +func ReadSyncRequirements(t *templates.ConstraintTemplate) (SyncRequirements, error) { + if t.ObjectMeta.Annotations != nil { + if annotation, exists := t.ObjectMeta.Annotations[SyncAnnotationName]; exists { + annotation = strings.Trim(annotation, "\n\"") + compactSyncRequirements := CompactSyncRequirements{} + decoder := json.NewDecoder(strings.NewReader(annotation)) + decoder.DisallowUnknownFields() + err := decoder.Decode(&compactSyncRequirements) + if err != nil { + return nil, err + } + requirements, err := ExpandCompactRequirements(compactSyncRequirements) + if err != nil { + return nil, err + } + return requirements, nil + } + } + return SyncRequirements{}, nil +} + +// Takes a compactGVKSet and expands it into a GVKEquivalenceSet. +func ExpandCompactEquivalenceSet(compactEquivalenceSet CompactGVKEquivalenceSet) GVKEquivalenceSet { + equivalenceSet := GVKEquivalenceSet{} + for _, group := range compactEquivalenceSet.Groups { + for _, version := range compactEquivalenceSet.Versions { + for _, kind := range compactEquivalenceSet.Kinds { + equivalenceSet[schema.GroupVersionKind{Group: group, Version: version, Kind: kind}] = struct{}{} + } + } + } + return equivalenceSet +} + +// Takes a CompactSyncRequirements (the json form provided in the template +// annotation) and expands it into a SyncRequirements. +func ExpandCompactRequirements(compactSyncRequirements CompactSyncRequirements) (SyncRequirements, error) { + syncRequirements := SyncRequirements{} + for _, compactRequirement := range compactSyncRequirements { + requirement := GVKEquivalenceSet{} + for _, compactEquivalenceSet := range compactRequirement { + for equivalentGVK := range ExpandCompactEquivalenceSet(compactEquivalenceSet) { + requirement[equivalentGVK] = struct{}{} + } + } + syncRequirements = append(syncRequirements, requirement) + } + return syncRequirements, nil +} diff --git a/pkg/syncutil/parser/syncannotationreader_test.go b/pkg/syncutil/parser/syncannotationreader_test.go new file mode 100644 index 00000000000..aa26b076eaa --- /dev/null +++ b/pkg/syncutil/parser/syncannotationreader_test.go @@ -0,0 +1,196 @@ +package parser + +import ( + "reflect" + "testing" + + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestReadSyncRequirements(t *testing.T) { + tests := []struct { + name string + template *templates.ConstraintTemplate + want SyncRequirements + wantErr bool + }{ + { + name: "test with basic valid annotation", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "\n\"[[{\"groups\": [\"group1\"], \"versions\": [\"version1\"], \"kinds\": [\"kind1\"]}]]\"", + }, + }, + }, + want: SyncRequirements{ + { + { + Group: "group1", + Version: "version1", + Kind: "kind1", + }: struct{}{}, + }, + }, + }, + { + name: "test with valid annotation with multiple groups, versions, and kinds", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "\n\"[[{\"groups\": [\"group1\", \"group2\"], \"versions\": [\"version1\", \"version2\"], \"kinds\": [\"kind1\", \"kind2\"]}]]\"", + }, + }, + }, + want: SyncRequirements{ + { + { + Group: "group1", + Version: "version1", + Kind: "kind1", + }: struct{}{}, + { + Group: "group1", + Version: "version1", + Kind: "kind2", + }: struct{}{}, + { + Group: "group1", + Version: "version2", + Kind: "kind1", + }: struct{}{}, + { + Group: "group1", + Version: "version2", + Kind: "kind2", + }: struct{}{}, + { + Group: "group2", + Version: "version1", + Kind: "kind1", + }: struct{}{}, + { + Group: "group2", + Version: "version1", + Kind: "kind2", + }: struct{}{}, + { + Group: "group2", + Version: "version2", + Kind: "kind1", + }: struct{}{}, + { + Group: "group2", + Version: "version2", + Kind: "kind2", + }: struct{}{}, + }, + }, + }, + { + name: "test with valid annotation with multiple equivalence sets", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "\n\"[[{\"groups\": [\"group1\"], \"versions\": [\"version1\"], \"kinds\": [\"kind1\"]}, {\"groups\": [\"group2\"], \"versions\": [\"version2\"], \"kinds\": [\"kind2\"]}]]\"", + }, + }, + }, + want: SyncRequirements{ + { + { + Group: "group1", + Version: "version1", + Kind: "kind1", + }: struct{}{}, + { + Group: "group2", + Version: "version2", + Kind: "kind2", + }: struct{}{}, + }, + }, + }, + { + name: "test with valid annotation with multiple requirements", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "\n\"[[{\"groups\": [\"group1\"], \"versions\": [\"version1\"], \"kinds\": [\"kind1\"]}], [{\"groups\": [\"group2\"], \"versions\": [\"version2\"], \"kinds\": [\"kind2\"]}]]\"", + }, + }, + }, + want: SyncRequirements{ + { + { + Group: "group1", + Version: "version1", + Kind: "kind1", + }: struct{}{}, + }, + { + { + Group: "group2", + Version: "version2", + Kind: "kind2", + }: struct{}{}, + }, + }, + }, + { + name: "test with no requires-sync-data annotation", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + }, + want: SyncRequirements{}, + }, + { + name: "test with empty requires-sync-data annotation", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "", + }, + }, + }, + wantErr: true, + }, + { + name: "test with invalid requires-sync-data annotation", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "test with requires-sync-data annotation with invalid keys", + template: &templates.ConstraintTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "metadata.gatekeeper.sh/requires-sync-data": "\n\"[[{\"group\": [\"group1\"], \"version\": [\"version1\"], \"kind\": [\"kind1\"]}]]\"", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadSyncRequirements(tt.template) + if (err != nil) != tt.wantErr { + t.Errorf("ReadSyncRequirements() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ReadSyncRequirements() got = %v, want %v", got, tt.want) + } + }) + } +}