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

feat: Sync annotation unmarshaling in gator #2734

Merged
87 changes: 87 additions & 0 deletions pkg/syncutil/parser/syncannotationreader.go
Original file line number Diff line number Diff line change
@@ -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
}
196 changes: 196 additions & 0 deletions pkg/syncutil/parser/syncannotationreader_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}