Skip to content

Commit

Permalink
Move to syncutils pkg, add tests
Browse files Browse the repository at this point in the history
Signed-off-by: Anlan Du <adu47249@gmail.com>
  • Loading branch information
anlandu committed May 10, 2023
1 parent 6f20882 commit 1968f4c
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 66 deletions.
66 changes: 0 additions & 66 deletions pkg/gator/reader/read_constraints.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package reader

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand All @@ -14,36 +13,13 @@ import (
"github.com/open-policy-agent/gatekeeper/pkg/gator"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
)

type versionless interface {
ToVersionless() (*templates.ConstraintTemplate, error)
}

// syncAnnotationName is the name of the annotation that stores
// GVKS that are required to be synced.
const SyncAnnotationName = "metadata.gatekeeper.sh/requiresSyncData"

// SyncAnnotationContents 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{}

// 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 solely for unmarshalling.
type compactGVKEquivalenceSet struct {
Groups []string `json:"groups"`
Versions []string `json:"versions"`
Kinds []string `json:"kinds"`
}

// jsonLookaheadBytes is the number of bytes the JSON and YAML decoder will
// look into the data it's reading to determine if the document is JSON or
// YAML. 1024 was a guess that's worked so far.
Expand Down Expand Up @@ -231,45 +207,3 @@ func ReadK8sResources(r io.Reader) ([]*unstructured.Unstructured, error) {

return objs, nil
}

// ReadSyncRequirements parses the sync requirements from a
// constraint template.
func ReadSyncRequirements(t *templates.ConstraintTemplate) (*SyncRequirements, error) {
syncRequirements := make(SyncRequirements, 0)
if t.ObjectMeta.Annotations != nil {
if annotation, exists := t.ObjectMeta.Annotations[SyncAnnotationName]; exists {
annotation = strings.Trim(annotation, "\n\"")
compactSyncRequirements := make([][]compactGVKEquivalenceSet, 0)
decoder := json.NewDecoder(bytes.NewReader([]byte(annotation)))
decoder.DisallowUnknownFields()
err := decoder.Decode(&compactSyncRequirements)
if err != nil {
return nil, err
}
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
}

// Takes a compactGVKSet and expands and unions it with the set of
// GVKs pointed to by the 'expandedEquivalentSet' argument.
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
}
86 changes: 86 additions & 0 deletions pkg/syncutil/syncannotationreader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package syncutil

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/requiresSyncData"

// 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

Check failure on line 23 in pkg/syncutil/syncannotationreader.go

View workflow job for this annotation

GitHub Actions / Lint

Comment should end in a period (godot)
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 solely for unmarshalling.
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 and unions it with the set of
// GVKs pointed to by the 'expandedEquivalentSet' argument.
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
}

// Convert

Check failure on line 73 in pkg/syncutil/syncannotationreader.go

View workflow job for this annotation

GitHub Actions / Lint

Comment should end in a period (godot)
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/syncannotationreader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package syncutil

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/requiresSyncData": "\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/requiresSyncData": "\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/requiresSyncData": "\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/requiresSyncData": "\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 requiressyncdata annotation",
template: &templates.ConstraintTemplate{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
},
want: SyncRequirements{},
},
{
name: "test with empty requiressyncdata annotation",
template: &templates.ConstraintTemplate{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"metadata.gatekeeper.sh/requiresSyncData": "",
},
},
},
wantErr: true,
},
{
name: "test with invalid requiressyncdata annotation",
template: &templates.ConstraintTemplate{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"metadata.gatekeeper.sh/requiresSyncData": "invalid",
},
},
},
wantErr: true,
},
{
name: "test with requiressyncdata annotation with invalid keys",
template: &templates.ConstraintTemplate{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"metadata.gatekeeper.sh/requiresSyncData": "\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)
}
})
}
}

0 comments on commit 1968f4c

Please sign in to comment.