From 1ea86c3c3d4929989da563170194e000a8804e4d Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 18 Sep 2023 19:19:09 -0700 Subject: [PATCH 01/24] initial setup Signed-off-by: Anlan Du --- cmd/gator/gator.go | 2 + cmd/gator/sync/create/create.go | 82 +++++++++++++++++++++++++++++++++ cmd/gator/sync/sync.go | 30 ++++++++++++ cmd/gator/sync/verify/verify.go | 39 ++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 cmd/gator/sync/create/create.go create mode 100644 cmd/gator/sync/sync.go create mode 100644 cmd/gator/sync/verify/verify.go diff --git a/cmd/gator/gator.go b/cmd/gator/gator.go index be611a9713c..cd0c57e363e 100644 --- a/cmd/gator/gator.go +++ b/cmd/gator/gator.go @@ -4,6 +4,7 @@ import ( "os" "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/expand" + "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync" "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/test" "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/verify" "github.com/open-policy-agent/gatekeeper/v3/pkg/version" @@ -15,6 +16,7 @@ var commands = []*cobra.Command{ verify.Cmd, test.Cmd, expand.Cmd, + sync.Cmd, k8sVersion.WithFont("alligator2"), } diff --git a/cmd/gator/sync/create/create.go b/cmd/gator/sync/create/create.go new file mode 100644 index 00000000000..fad2bc85fca --- /dev/null +++ b/cmd/gator/sync/create/create.go @@ -0,0 +1,82 @@ +package synccreate + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +// createCmd represents the create command +var Cmd = &cobra.Command{ + Use: "create", + Short: "Create SyncSet(s) based on the requires-sync-data annotations in the input templates.", + RunE: runE, +} + +var ( + flagFilenames []string + flagDiscoveryResults string + flagImplementation enumFlag + flagOutput enumFlag +) + +const ( + flagNameTemplateFilename = "template-filename" + flagNameDiscoveryResults = "discovery-results" + flagNameImplementation = "implementation" + flagNameOutput = "output" +) + +func newEnumFlag(allowed []string, d string) *enumFlag { + return &enumFlag{ + Allowed: allowed, + Value: d, + } +} + +type enumFlag struct { + Allowed []string + Value string +} + +// String is used both by fmt.Print and by Cobra in help text +func (o *enumFlag) String() string { + return o.Value +} + +// Set must have pointer receiver so it doesn't change the value of a copy +func (o *enumFlag) Set(v string) error { + isIncluded := func(opts []string, val string) bool { + for _, opt := range opts { + if val == opt { + return true + } + } + return false + } + if !isIncluded(o.Allowed, v) { + return fmt.Errorf("%s is not included in %s", v, strings.Join(o.Allowed, ",")) + } + o.Value = v + return nil +} + +func (o *enumFlag) Type() string { + return strings.Join(o.Allowed, "|") +} + +func init() { + Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameTemplateFilename, "f", []string{}, "a file or directory containing Constraint Templates. Can be specified multiple times.") + Cmd.MarkFlagRequired(flagNameTemplateFilename) + + Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") + flagImplementation := newEnumFlag([]string{"greedy", "optimal"}, "greedy") + Cmd.Flags().VarP(flagImplementation, flagNameImplementation, "i", "the implementation to use for creating SyncSets. One of: greedy|optimal.") + flagOutput := newEnumFlag([]string{"single", "bundled"}, "bundled") + Cmd.Flags().VarP(flagOutput, flagNameOutput, "o", "whether to bundle required GVKs into one SyncSet or output one SyncSet per template. One of: single|bundled.") +} + +func runE(cmd *cobra.Command, args []string) error { + return nil +} diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go new file mode 100644 index 00000000000..9ecb681566c --- /dev/null +++ b/cmd/gator/sync/sync.go @@ -0,0 +1,30 @@ +/* +Copyright © 2023 NAME HERE +*/ +package sync + +import ( + "fmt" + + synccreate "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/create" + syncverify "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/verify" + "github.com/spf13/cobra" +) + +var commands = []*cobra.Command{ + syncverify.Cmd, + synccreate.Cmd, +} + +// syncCmd represents the sync command +var Cmd = &cobra.Command{ + Use: "sync", + Short: "Manage SyncSets and Config", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Usage: gator sync create or gator sync verify") + }, +} + +func init() { + Cmd.AddCommand(commands...) +} diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go new file mode 100644 index 00000000000..147f38199d9 --- /dev/null +++ b/cmd/gator/sync/verify/verify.go @@ -0,0 +1,39 @@ +package syncverify + +import ( + "github.com/spf13/cobra" +) + +// Cmd represents the verify command +var Cmd = &cobra.Command{ + Use: "verify", + Short: "Verify that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", + RunE: runE, +} + +var ( + flagFilenames []string + flagSyncDataFilenames []string + flagDiscoveryResults string +) + +const ( + flagNameTemplateFilename = "template-filename" + flagNameSyncDataFilename = "sync-data-filename" + flagNameDiscoveryResults = "discovery-results" +) + +func init() { + Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameTemplateFilename, "f", []string{}, "a file or directory containing the Constraint Templates to verify SyncSets/Config against. Can be specified multiple times.") + Cmd.MarkFlagRequired(flagNameTemplateFilename) + + Cmd.Flags().StringArrayVarP(&flagSyncDataFilenames, flagNameSyncDataFilename, "s", []string{}, "a file or directory containing the SyncSet(s) and/or Config to verify. Can be specified multiple times.") + Cmd.MarkFlagRequired(flagNameSyncDataFilename) + + Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") + +} + +func runE(cmd *cobra.Command, args []string) error { + return nil +} From 434523e34fb129834584e0b811ba89cd589b18f7 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 20 Oct 2023 22:09:40 -0700 Subject: [PATCH 02/24] Tests etc Signed-off-by: Anlan Du --- cmd/gator/sync/verify/verify.go | 58 +++++++--- pkg/gator/errors.go | 10 ++ pkg/gator/fixtures/fixtures.go | 153 +++++++++++++++++++++++++++ pkg/gator/reader/read_constraints.go | 97 +++++++++++++++-- pkg/gator/sync/verify/verify.go | 119 +++++++++++++++++++++ pkg/gator/sync/verify/verify_test.go | 132 +++++++++++++++++++++++ website/docs/gator.md | 74 +++++++++++++ 7 files changed, 619 insertions(+), 24 deletions(-) create mode 100644 pkg/gator/sync/verify/verify.go create mode 100644 pkg/gator/sync/verify/verify_test.go diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index 147f38199d9..863b5482af9 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -1,6 +1,13 @@ -package syncverify +package verify import ( + "bytes" + "fmt" + "os" + + cmdutils "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/util" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/sync/verify" "github.com/spf13/cobra" ) @@ -8,32 +15,55 @@ import ( var Cmd = &cobra.Command{ Use: "verify", Short: "Verify that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", - RunE: runE, + Run: run, } var ( - flagFilenames []string - flagSyncDataFilenames []string - flagDiscoveryResults string + flagFilenames []string + flagImages []string + flagDiscoveryResults string ) const ( - flagNameTemplateFilename = "template-filename" - flagNameSyncDataFilename = "sync-data-filename" + flagNameFilename = "filename" + flagNameImage = "image" flagNameDiscoveryResults = "discovery-results" ) func init() { - Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameTemplateFilename, "f", []string{}, "a file or directory containing the Constraint Templates to verify SyncSets/Config against. Can be specified multiple times.") - Cmd.MarkFlagRequired(flagNameTemplateFilename) - - Cmd.Flags().StringArrayVarP(&flagSyncDataFilenames, flagNameSyncDataFilename, "s", []string{}, "a file or directory containing the SyncSet(s) and/or Config to verify. Can be specified multiple times.") - Cmd.MarkFlagRequired(flagNameSyncDataFilename) + Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") + Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") } -func runE(cmd *cobra.Command, args []string) error { - return nil +func run(cmd *cobra.Command, args []string) { + unstrucs, err := reader.ReadSources(flagFilenames, flagImages, "") + if err != nil { + cmdutils.ErrFatalf("reading: %v", err) + } + if len(unstrucs) == 0 { + cmdutils.ErrFatalf("no input data identified") + } + + missingRequirements, err := verify.Verify(unstrucs, flagDiscoveryResults) + + if err != nil { + cmdutils.ErrFatalf("verifying: %v", err) + } + + if len(missingRequirements) > 0 { + cmdutils.ErrFatalf("The following requirements were not met: \n%v", resultsToString(missingRequirements)) + } + + os.Exit(0) +} + +func resultsToString(results map[string][]int) string { + var buf bytes.Buffer + for template, reqs := range results { + buf.WriteString(fmt.Sprintf("%s: %v\n", template, reqs)) + } + return buf.String() } diff --git a/pkg/gator/errors.go b/pkg/gator/errors.go index 101fa6958b4..7851e51baf0 100644 --- a/pkg/gator/errors.go +++ b/pkg/gator/errors.go @@ -9,10 +9,20 @@ var ( // ErrNotAConstraint indicates the user-indicated file does not contain a // Constraint. ErrNotAConstraint = errors.New("not a Constraint") + // ErrNotAConfig indicates the user-indicated file does not contain a + // Config. + ErrNotAConfig = errors.New("not a Config") + // ErrNotASyncSet indicates the user-indicated file does not contain a + // SyncSet. + ErrNotASyncSet = errors.New("not a SyncSet") // ErrAddingTemplate indicates a problem instantiating a Suite's ConstraintTemplate. ErrAddingTemplate = errors.New("adding template") // ErrAddingConstraint indicates a problem instantiating a Suite's Constraint. ErrAddingConstraint = errors.New("adding constraint") + // ErrAddingSyncSet indicates a problem instantiating a Suite's SyncSet. + ErrAddingSyncSet = errors.New("adding syncset") + // ErrAddingConfig indicates a problem instantiating a Suite's Config. + ErrAddingConfig = errors.New("adding config") // ErrInvalidSuite indicates a Suite does not define the required fields. ErrInvalidSuite = errors.New("invalid Suite") // ErrCreatingClient indicates an error instantiating the Client which compiles diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 9a4a0b7e4d0..991c3feac66 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -326,6 +326,16 @@ metadata: name: k8suniqueserviceselector annotations: description: Requires Services to have unique selectors within a namespace. + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": [""], + "versions": ["v1"], + "kinds": ["Service"] + } + ] + ]" spec: crd: spec: @@ -368,6 +378,118 @@ spec: } ` + TemplateReferentialMultEquivSets = ` +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8suniqueingresshost + annotations: + metadata.gatekeeper.sh/title: "Unique Ingress Host" + metadata.gatekeeper.sh/version: 1.0.3 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": ["extensions"], + "versions": ["v1beta1"], + "kinds": ["Ingress"] + }, + { + "groups": ["networking.k8s.io"], + "versions": ["v1beta1", "v1"], + "kinds": ["Ingress"] + } + ] + ]" + description: >- + Requires all Ingress rule hosts to be unique. + + Does not handle hostname wildcards: + https://kubernetes.io/docs/concepts/services-networking/ingress/ +spec: + crd: + spec: + names: + kind: K8sUniqueIngressHost + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8suniqueingresshost + + identical(obj, review) { + obj.metadata.namespace == review.object.metadata.namespace + obj.metadata.name == review.object.metadata.name + } + + violation[{"msg": msg}] { + input.review.kind.kind == "Ingress" + re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) + host := input.review.object.spec.rules[_].host + other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] + re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) + other.spec.rules[_].host == host + not identical(other, input.review) + msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) + } +` + + TemplateReferentialMultReqs = ` +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8suniqueingresshostmultireq + annotations: + metadata.gatekeeper.sh/title: "Unique Ingress Host" + metadata.gatekeeper.sh/version: 1.0.3 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups": ["extensions"], + "versions": ["v1beta1"], + "kinds": ["Ingress"] + } + ], + [ + { + "groups": ["networking.k8s.io"], + "versions": ["v1beta1", "v1"], + "kinds": ["Ingress"] + } + ] + ]" + description: >- + Requires all Ingress rule hosts to be unique. + + Does not handle hostname wildcards: + https://kubernetes.io/docs/concepts/services-networking/ingress/ +spec: + crd: + spec: + names: + kind: K8sUniqueIngressHost + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8suniqueingresshost + + identical(obj, review) { + obj.metadata.namespace == review.object.metadata.namespace + obj.metadata.name == review.object.metadata.name + } + + violation[{"msg": msg}] { + input.review.kind.kind == "Ingress" + re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) + host := input.review.object.spec.rules[_].host + other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] + re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) + other.spec.rules[_].host == host + not identical(other, input.review) + msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) + } +` + ConstraintReferential = ` apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sUniqueServiceSelector @@ -581,5 +703,36 @@ spec: kinds: - apiGroups: ["*"] kinds: ["*"] +` + SyncSet = ` +apiVersion: syncset.gatekeeper.sh/v1alpha1 +kind: SyncSet +metadata: + name: syncset + namespace: "gatekeeper-system" +spec: + gvks: + - group: "extensions" + version: "v1beta1" + kind: "Ingress" + - group: "apps" + version: "v1" + kind: "Deployment" +` + Config = ` +apiVersion: config.gatekeeper.sh/v1alpha1 +kind: Config +metadata: + name: config + namespace: "gatekeeper-system" +spec: + sync: + syncOnly: + - group: "" + version: "v1" + kind: "Service" + - group: "apps" + version: "v1" + kind: "Deployment" ` ) diff --git a/pkg/gator/reader/read_constraints.go b/pkg/gator/reader/read_constraints.go index 09623dbc695..4d243e0ef3f 100644 --- a/pkg/gator/reader/read_constraints.go +++ b/pkg/gator/reader/read_constraints.go @@ -10,9 +10,12 @@ import ( templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + configv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/config/v1alpha1" + syncsetv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/syncset/v1alpha1" "github.com/open-policy-agent/gatekeeper/v3/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" ) @@ -101,17 +104,10 @@ func ReadTemplate(scheme *runtime.Scheme, f fs.FS, path string) (*templates.Cons return template, nil } -// TODO (https://github.com/open-policy-agent/gatekeeper/issues/1779): Move -// this function into a location that makes it more obviously a shared resource -// between `gator test` and `gator verify` - -// ToTemplate converts an unstructured template into a versionless ConstraintTemplate struct. -func ToTemplate(scheme *runtime.Scheme, u *unstructured.Unstructured) (*templates.ConstraintTemplate, error) { +// ToTemplate converts an unstructured object into an object with the schema defined +// by u's group, version, and kind. +func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime.Object, error) { gvk := u.GroupVersionKind() - if gvk.Group != templatesv1.SchemeGroupVersion.Group || gvk.Kind != "ConstraintTemplate" { - return nil, fmt.Errorf("%w", gator.ErrNotATemplate) - } - t, err := scheme.New(gvk) if err != nil { // The type isn't registered in the scheme. @@ -130,6 +126,23 @@ func ToTemplate(scheme *runtime.Scheme, u *unstructured.Unstructured) (*template if err != nil { return nil, fmt.Errorf("%w: %w", gator.ErrAddingTemplate, err) } + return t, nil +} + +// TODO (https://github.com/open-policy-agent/gatekeeper/issues/1779): Move +// this function into a location that makes it more obviously a shared resource +// between `gator test` and `gator verify` + +// ToTemplate converts an unstructured template into a versionless ConstraintTemplate struct. +func ToTemplate(scheme *runtime.Scheme, u *unstructured.Unstructured) (*templates.ConstraintTemplate, error) { + if u.GroupVersionKind().Group != templatesv1.SchemeGroupVersion.Group || u.GroupVersionKind().Kind != "ConstraintTemplate" { + return nil, fmt.Errorf("%w", gator.ErrNotATemplate) + } + + t, err := ToStructured(scheme, u) + if err != nil { + return nil, fmt.Errorf("%w: %w", gator.ErrAddingTemplate, err) + } v, isVersionless := t.(versionless) if !isVersionless { @@ -146,6 +159,44 @@ func ToTemplate(scheme *runtime.Scheme, u *unstructured.Unstructured) (*template return template, nil } +// ToSyncSet converts an unstructured SyncSet into a SyncSet struct. +func ToSyncSet(scheme *runtime.Scheme, u *unstructured.Unstructured) (*syncsetv1alpha1.SyncSet, error) { + if u.GroupVersionKind().Group != syncsetv1alpha1.GroupVersion.Group || u.GroupVersionKind().Kind != "SyncSet" { + return nil, fmt.Errorf("%w", gator.ErrNotASyncSet) + } + + s, err := ToStructured(scheme, u) + if err != nil { + return nil, fmt.Errorf("%w: %w", gator.ErrAddingSyncSet, err) + } + + syncSet, isSyncSet := s.(*syncsetv1alpha1.SyncSet) + if !isSyncSet { + return nil, fmt.Errorf("%w: %T", gator.ErrAddingSyncSet, syncSet) + } + + return syncSet, nil +} + +// ToConfig converts an unstructured Config into a Config struct. +func ToConfig(scheme *runtime.Scheme, u *unstructured.Unstructured) (*configv1alpha1.Config, error) { + if u.GroupVersionKind().Group != configv1alpha1.GroupVersion.Group || u.GroupVersionKind().Kind != "Config" { + return nil, fmt.Errorf("%w", gator.ErrNotAConfig) + } + + s, err := ToStructured(scheme, u) + if err != nil { + return nil, fmt.Errorf("%w: %w", gator.ErrAddingConfig, err) + } + + config, isConfig := s.(*configv1alpha1.Config) + if !isConfig { + return nil, fmt.Errorf("%w: %T", gator.ErrAddingConfig, config) + } + + return config, nil +} + // ReadObject reads a file from the filesystem abstraction at the specified // path, and returns an unstructured.Unstructured object if the file can be // successfully unmarshalled. @@ -207,3 +258,29 @@ func ReadK8sResources(r io.Reader) ([]*unstructured.Unstructured, error) { return objs, nil } + +type DiscoveryResults map[schema.GroupVersionKind]struct{} + +func ReadDiscoveryResults(r string) (DiscoveryResults, error) { + if r == "" { + return nil, nil + } + var stringAsJson map[string]map[string][]string + if err := json.Unmarshal([]byte(r), &stringAsJson); err != nil { + return nil, err + } + results := DiscoveryResults{} + for group, versions := range stringAsJson { + for version, kinds := range versions { + for _, kind := range kinds { + results[schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }] = struct{}{} + } + } + } + + return results, nil +} diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go new file mode 100644 index 00000000000..184b8cfabec --- /dev/null +++ b/pkg/gator/sync/verify/verify.go @@ -0,0 +1,119 @@ +package verify + +import ( + "fmt" + + cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" + templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + gkapis "github.com/open-policy-agent/gatekeeper/v3/apis" + "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var scheme *runtime.Scheme + +func init() { + scheme = runtime.NewScheme() + err := cfapis.AddToScheme(scheme) + if err != nil { + panic(err) + } + err = gkapis.AddToScheme(scheme) + if err != nil { + panic(err) + } +} + +func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) (map[string][]int, error) { + + discoveryResults, err := reader.ReadDiscoveryResults(flagDiscoveryResults) + if err != nil { + return nil, fmt.Errorf("reading: %v", err) + } + + templates := []*templates.ConstraintTemplate{} + syncedGVKs := map[schema.GroupVersionKind]struct{}{} + + for _, obj := range unstrucs { + if isSyncSet(obj) { + syncSet, err := reader.ToSyncSet(scheme, obj) + if err != nil { + return nil, fmt.Errorf("converting unstructured %q to syncset: %w", obj.GetName(), err) + } + for _, gvkEntry := range syncSet.Spec.GVKs { + gvk := schema.GroupVersionKind{ + Group: gvkEntry.Group, + Version: gvkEntry.Version, + Kind: gvkEntry.Kind, + } + if _, exists := discoveryResults[gvk]; exists || discoveryResults == nil { + syncedGVKs[gvk] = struct{}{} + } + } + } + if isConfig(obj) { + config, err := reader.ToConfig(scheme, obj) + if err != nil { + return nil, fmt.Errorf("converting unstructured %q to config: %w", obj.GetName(), err) + } + for _, syncOnlyEntry := range config.Spec.Sync.SyncOnly { + gvk := schema.GroupVersionKind{ + Group: syncOnlyEntry.Group, + Version: syncOnlyEntry.Version, + Kind: syncOnlyEntry.Kind, + } + if _, exists := discoveryResults[gvk]; exists || discoveryResults == nil { + syncedGVKs[gvk] = struct{}{} + } + } + } + if isTemplate(obj) { + templ, err := reader.ToTemplate(scheme, obj) + if err != nil { + return nil, fmt.Errorf("converting unstructured %q to template: %w", obj.GetName(), err) + } + templates = append(templates, templ) + } + } + + missingReqs := map[string][]int{} + + for _, templ := range templates { + // Fetch syncrequirements from template + syncRequirements, err := parser.ReadSyncRequirements(templ) + if err != nil { + return nil, fmt.Errorf("reading sync requirements from template %q: %w", templ.GetName(), err) + } + for i, requirement := range syncRequirements { + requirementMet := false + for gvk := range requirement { + if _, exists := syncedGVKs[gvk]; exists { + requirementMet = true + } + } + if !requirementMet { + missingReqs[templ.Name] = append(missingReqs[templ.Name], i+1) + } + } + } + return missingReqs, nil +} + +func isTemplate(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == templatesv1.SchemeGroupVersion.Group && gvk.Kind == "ConstraintTemplate" +} + +func isConfig(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == "config.gatekeeper.sh" && gvk.Kind == "Config" +} + +func isSyncSet(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == "syncset.gatekeeper.sh" && gvk.Kind == "SyncSet" +} diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go new file mode 100644 index 00000000000..11c722b27ac --- /dev/null +++ b/pkg/gator/sync/verify/verify_test.go @@ -0,0 +1,132 @@ +package verify + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/yaml" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestVerify(t *testing.T) { + tcs := []struct { + name string + inputs []string + discovery string + want map[string][]int + err error + }{ + { + name: "basic req unfulfilled", + inputs: []string{ + fixtures.TemplateReferential, + }, + want: map[string][]int{ + "k8suniqueserviceselector": {1}, + }, + }, + { + name: "basic req fulfilled by syncset", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + }, + want: map[string][]int{}, + }, + { + name: "basic req fulfilled by syncset and discoveryresults", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + }, + discovery: `{"": {"v1": ["Service"]}}`, + want: map[string][]int{}, + }, + { + name: "basic req fulfilled by syncset but not discoveryresults", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + }, + discovery: `{"extensions": {"v1beta1": ["Ingress"]}}`, + want: map[string][]int{ + "k8suniqueserviceselector": {1}, + }, + }, + { + name: "multi equivalentset req fulfilled by syncset", + inputs: []string{ + fixtures.TemplateReferentialMultEquivSets, + fixtures.SyncSet, + }, + want: map[string][]int{}, + }, + { + name: "multi requirement, one req fulfilled by syncset", + inputs: []string{ + fixtures.TemplateReferentialMultReqs, + fixtures.SyncSet, + }, + want: map[string][]int{ + "k8suniqueingresshostmultireq": {2}, + }, + }, + { + name: "multiple templates, syncset and config", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.TemplateReferentialMultEquivSets, + fixtures.TemplateReferentialMultReqs, + fixtures.Config, + fixtures.SyncSet, + }, + want: map[string][]int{ + "k8suniqueingresshostmultireq": {2}, + }, + }, + { + name: "no data of any kind", + inputs: []string{}, + want: map[string][]int{}, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + // convert the test resources to unstructureds + var objs []*unstructured.Unstructured + for _, input := range tc.inputs { + u, err := readUnstructured([]byte(input)) + require.NoError(t, err) + objs = append(objs, u) + } + + got, err := Verify(objs, tc.discovery) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else if err != nil { + require.NoError(t, err) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) + } + }) + } +} + +func readUnstructured(bytes []byte) (*unstructured.Unstructured, error) { + u := &unstructured.Unstructured{ + Object: make(map[string]interface{}), + } + + err := yaml.Unmarshal(bytes, u) + if err != nil { + return nil, err + } + + return u, nil +} diff --git a/website/docs/gator.md b/website/docs/gator.md index a3c691aaf19..46cc54a8b38 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -427,6 +427,80 @@ However, not including the `namespace` definition in the call to `gator expand` error expanding resources: error expanding resource nginx-deployment: failed to mutate resultant resource nginx-deployment-pod: matching for mutator Assign.mutations.gatekeeper.sh /always-pull-image failed for Pod my-ns nginx-deployment-pod: failed to run Match criteria: namespace selector for namespace-scoped object but missing Namespace ``` +## The `gator sync verify` subcommand + +Certain templates require [replicating data](sync.md) into OPA to enable proper evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more equivalence sets. Each of these equivalence sets has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within an equivalence set within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): +``` +[ + [ // Requirement 1 + { // Equivalence set 1 + "groups": ["group1", group2"] + "versions": ["version1", "version2", "version3"] + "kinds": ["kind1", "kind2"] + }, + { // Equivalence Set 2 + "groups": ["group3", group4"] + "versions": ["version3", "version4"] + "kinds": ["kind3", "kind4"] + } + ], + [ // Requirement 2 + { // Equivalence Set 3 + "groups": ["group5"] + "versions": ["version5"] + "kinds": ["kind5"] + } + ] +] +``` +This annotation contains two requirements. Requirement 1 contains two equivalence sets. Syncing resources of group1, version3, kind1 (drawn from equivalence set 1) would be sufficient to fulfill Requirement 1. So, too, would syncing resources of group3, version3, kind4 (drawn from equivalence set 2). Syncing resources of group1, version1, and kind3 would not be, however. + +Requirement 2 is simpler: it denotes that group5, version5, kind5 must be synced for the policy to work properly. + +This template annotation is descriptive, not prescriptive. The prescription of which resources to sync is done in `SyncSet` resources and/or the Gatekeeper `config` resource. The management of these various requirements can get challenging as the number of templates requiring replicated data increases. + +`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. he user to pass in any number of Constraint Templates, SyncSets, and Gatekeeper Config objects (although in practice only one Config object should exist), and it will inform you which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). + +### Usage + +#### Specifying Inputs + +`gator sync verify` expects a `--filename` or `--image` flag, or input fron stdin. The flags can be used individually, in combination, and/or repeated. + +``` +gator sync verify --filename="template.yaml" –filename="syncsets/" +``` + +Or, using an OCI Artifact containing templates as described previously: + +``` +gator sync verify --filename="config.yaml" --image=localhost:5000/gator/template-library:v1 +``` + +Optionally, the `--discovery-results` flag can be used to pass in a list of supported GVKs discovered on a cluster. If this is nonempty, only GVKs that are both included in a SyncSet/Config and are supported will be considered to fulfill a template's requirements. Discovery results should be passed as a string representing a JSON object mapping groups to lists of versions, which contain lists of kinds, like such: +``` +{ + "group1": { + "version1": ["kind1", "kind2"], + "version2": ["kind3", "kind4"], + }, + "group2": { + "version3": ["kind3"] + } +} +``` + +#### Exit Codes + +`gator sync verify` will return a `0` exit status when the Templates, SyncSets, and +Configs are successfully ingested and no requirements are unfulfilled. + +An error during evaluation, for example a failure to read a file, will result in +a `1` exit status with an error message printed to stderr. + +Unfulfilled requirements will generate a `1` exit status as well, and the unfulfilled requirements per template will be printed to stdout. + + ## Bundling Policy into OCI Artifacts It may be useful to bundle policy files into OCI Artifacts for ingestion during From 004dd3ebc842eeff6fc2b3e05caf6ef943cb2546 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 20 Oct 2023 22:13:25 -0700 Subject: [PATCH 03/24] Remove create cmd Signed-off-by: Anlan Du --- cmd/gator/sync/create/create.go | 82 --------------------------------- cmd/gator/sync/sync.go | 2 - 2 files changed, 84 deletions(-) delete mode 100644 cmd/gator/sync/create/create.go diff --git a/cmd/gator/sync/create/create.go b/cmd/gator/sync/create/create.go deleted file mode 100644 index fad2bc85fca..00000000000 --- a/cmd/gator/sync/create/create.go +++ /dev/null @@ -1,82 +0,0 @@ -package synccreate - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" -) - -// createCmd represents the create command -var Cmd = &cobra.Command{ - Use: "create", - Short: "Create SyncSet(s) based on the requires-sync-data annotations in the input templates.", - RunE: runE, -} - -var ( - flagFilenames []string - flagDiscoveryResults string - flagImplementation enumFlag - flagOutput enumFlag -) - -const ( - flagNameTemplateFilename = "template-filename" - flagNameDiscoveryResults = "discovery-results" - flagNameImplementation = "implementation" - flagNameOutput = "output" -) - -func newEnumFlag(allowed []string, d string) *enumFlag { - return &enumFlag{ - Allowed: allowed, - Value: d, - } -} - -type enumFlag struct { - Allowed []string - Value string -} - -// String is used both by fmt.Print and by Cobra in help text -func (o *enumFlag) String() string { - return o.Value -} - -// Set must have pointer receiver so it doesn't change the value of a copy -func (o *enumFlag) Set(v string) error { - isIncluded := func(opts []string, val string) bool { - for _, opt := range opts { - if val == opt { - return true - } - } - return false - } - if !isIncluded(o.Allowed, v) { - return fmt.Errorf("%s is not included in %s", v, strings.Join(o.Allowed, ",")) - } - o.Value = v - return nil -} - -func (o *enumFlag) Type() string { - return strings.Join(o.Allowed, "|") -} - -func init() { - Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameTemplateFilename, "f", []string{}, "a file or directory containing Constraint Templates. Can be specified multiple times.") - Cmd.MarkFlagRequired(flagNameTemplateFilename) - - Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") - flagImplementation := newEnumFlag([]string{"greedy", "optimal"}, "greedy") - Cmd.Flags().VarP(flagImplementation, flagNameImplementation, "i", "the implementation to use for creating SyncSets. One of: greedy|optimal.") - flagOutput := newEnumFlag([]string{"single", "bundled"}, "bundled") - Cmd.Flags().VarP(flagOutput, flagNameOutput, "o", "whether to bundle required GVKs into one SyncSet or output one SyncSet per template. One of: single|bundled.") -} - -func runE(cmd *cobra.Command, args []string) error { - return nil -} diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go index 9ecb681566c..668c3d811b5 100644 --- a/cmd/gator/sync/sync.go +++ b/cmd/gator/sync/sync.go @@ -6,14 +6,12 @@ package sync import ( "fmt" - synccreate "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/create" syncverify "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/verify" "github.com/spf13/cobra" ) var commands = []*cobra.Command{ syncverify.Cmd, - synccreate.Cmd, } // syncCmd represents the sync command From 0e3b88cd4fee267c6d9d5c395fe918282edc9eac Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 20 Oct 2023 22:25:26 -0700 Subject: [PATCH 04/24] Update gator.md Signed-off-by: Anlan Du --- website/docs/gator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/gator.md b/website/docs/gator.md index 46cc54a8b38..432559412f6 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -498,7 +498,7 @@ Configs are successfully ingested and no requirements are unfulfilled. An error during evaluation, for example a failure to read a file, will result in a `1` exit status with an error message printed to stderr. -Unfulfilled requirements will generate a `1` exit status as well, and the unfulfilled requirements per template will be printed to stdout. +Unfulfilled requirements will generate a `1` exit status as well, and the unfulfilled requirements per template will be printed to stderr. ## Bundling Policy into OCI Artifacts From 25177854adf7fc6dd2fd77a07a9867332c180eb8 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 23 Oct 2023 12:33:10 -0700 Subject: [PATCH 05/24] Lint Signed-off-by: Anlan Du --- cmd/gator/sync/sync.go | 2 +- cmd/gator/sync/verify/verify.go | 5 +---- pkg/gator/reader/read_constraints.go | 6 +++--- pkg/gator/sync/verify/verify.go | 3 +-- pkg/gator/sync/verify/verify_test.go | 3 +-- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go index 668c3d811b5..5b5fbe0b7c9 100644 --- a/cmd/gator/sync/sync.go +++ b/cmd/gator/sync/sync.go @@ -14,7 +14,7 @@ var commands = []*cobra.Command{ syncverify.Cmd, } -// syncCmd represents the sync command +// Cmd represents the sync command. var Cmd = &cobra.Command{ Use: "sync", Short: "Manage SyncSets and Config", diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index 863b5482af9..179b886a9be 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -// Cmd represents the verify command +// Cmd represents the verify command. var Cmd = &cobra.Command{ Use: "verify", Short: "Verify that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", @@ -31,11 +31,9 @@ const ( ) func init() { - Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") - } func run(cmd *cobra.Command, args []string) { @@ -48,7 +46,6 @@ func run(cmd *cobra.Command, args []string) { } missingRequirements, err := verify.Verify(unstrucs, flagDiscoveryResults) - if err != nil { cmdutils.ErrFatalf("verifying: %v", err) } diff --git a/pkg/gator/reader/read_constraints.go b/pkg/gator/reader/read_constraints.go index 4d243e0ef3f..2ee35fe782a 100644 --- a/pkg/gator/reader/read_constraints.go +++ b/pkg/gator/reader/read_constraints.go @@ -265,12 +265,12 @@ func ReadDiscoveryResults(r string) (DiscoveryResults, error) { if r == "" { return nil, nil } - var stringAsJson map[string]map[string][]string - if err := json.Unmarshal([]byte(r), &stringAsJson); err != nil { + var stringAsJSON map[string]map[string][]string + if err := json.Unmarshal([]byte(r), &stringAsJSON); err != nil { return nil, err } results := DiscoveryResults{} - for group, versions := range stringAsJson { + for group, versions := range stringAsJSON { for version, kinds := range versions { for _, kind := range kinds { results[schema.GroupVersionKind{ diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index 184b8cfabec..aa0bc0e228c 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -29,10 +29,9 @@ func init() { } func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) (map[string][]int, error) { - discoveryResults, err := reader.ReadDiscoveryResults(flagDiscoveryResults) if err != nil { - return nil, fmt.Errorf("reading: %v", err) + return nil, fmt.Errorf("reading: %w", err) } templates := []*templates.ConstraintTemplate{} diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go index 11c722b27ac..8756796728d 100644 --- a/pkg/gator/sync/verify/verify_test.go +++ b/pkg/gator/sync/verify/verify_test.go @@ -6,9 +6,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" ) func TestVerify(t *testing.T) { From 7d398a2a37949564c1ffc5046b7ecca5d6d7d198 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 23 Oct 2023 13:18:03 -0700 Subject: [PATCH 06/24] Update gator.md Signed-off-by: Anlan Du --- website/docs/gator.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/gator.md b/website/docs/gator.md index 432559412f6..08003cb41ac 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -429,7 +429,7 @@ error expanding resources: error expanding resource nginx-deployment: failed to ## The `gator sync verify` subcommand -Certain templates require [replicating data](sync.md) into OPA to enable proper evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more equivalence sets. Each of these equivalence sets has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within an equivalence set within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): +Certain templates require [replicating data](sync.md) into OPA to enable correct evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more equivalence sets. Each of these equivalence sets has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within an equivalence set within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): ``` [ [ // Requirement 1 @@ -457,9 +457,9 @@ This annotation contains two requirements. Requirement 1 contains two equivalenc Requirement 2 is simpler: it denotes that group5, version5, kind5 must be synced for the policy to work properly. -This template annotation is descriptive, not prescriptive. The prescription of which resources to sync is done in `SyncSet` resources and/or the Gatekeeper `config` resource. The management of these various requirements can get challenging as the number of templates requiring replicated data increases. +This template annotation is descriptive, not prescriptive. The prescription of which resources to sync is done in `SyncSet` resources and/or the Gatekeeper `Config` resource. The management of these various requirements can get challenging as the number of templates requiring replicated data increases. -`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. he user to pass in any number of Constraint Templates, SyncSets, and Gatekeeper Config objects (although in practice only one Config object should exist), and it will inform you which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). +`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. The user passes in any number of Constraint Templates, SyncSets, and Gatekeeper Config objects (although in practice only one Config object should exist), and the command will determine which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). ### Usage From 9af46d11ed23017633c014c78dee393ae79136b6 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 23 Oct 2023 16:16:22 -0700 Subject: [PATCH 07/24] Comments Signed-off-by: Anlan Du --- cmd/gator/sync/sync.go | 6 +- cmd/gator/sync/verify/verify.go | 10 ++- pkg/gator/fixtures/fixtures.go | 42 +++++++++++ ...{read_constraints.go => read_resources.go} | 36 +++++++--- ...traints_test.go => read_resources_test.go} | 0 pkg/gator/sync/verify/verify.go | 41 ++++------- pkg/gator/sync/verify/verify_test.go | 70 +++++++++++-------- pkg/gator/test/test.go | 15 +--- 8 files changed, 133 insertions(+), 87 deletions(-) rename pkg/gator/reader/{read_constraints.go => read_resources.go} (90%) rename pkg/gator/reader/{read_constraints_test.go => read_resources_test.go} (100%) diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go index 5b5fbe0b7c9..61c731832ef 100644 --- a/cmd/gator/sync/sync.go +++ b/cmd/gator/sync/sync.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 NAME HERE -*/ package sync import ( @@ -14,12 +11,11 @@ var commands = []*cobra.Command{ syncverify.Cmd, } -// Cmd represents the sync command. var Cmd = &cobra.Command{ Use: "sync", Short: "Manage SyncSets and Config", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Usage: gator sync create or gator sync verify") + fmt.Println("Usage: gator sync verify") }, } diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index 179b886a9be..74b82d15a51 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -11,7 +11,6 @@ import ( "github.com/spf13/cobra" ) -// Cmd represents the verify command. var Cmd = &cobra.Command{ Use: "verify", Short: "Verify that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", @@ -45,7 +44,7 @@ func run(cmd *cobra.Command, args []string) { cmdutils.ErrFatalf("no input data identified") } - missingRequirements, err := verify.Verify(unstrucs, flagDiscoveryResults) + missingRequirements, templateErrors, err := verify.Verify(unstrucs, flagDiscoveryResults) if err != nil { cmdutils.ErrFatalf("verifying: %v", err) } @@ -54,10 +53,15 @@ func run(cmd *cobra.Command, args []string) { cmdutils.ErrFatalf("The following requirements were not met: \n%v", resultsToString(missingRequirements)) } + if len(templateErrors) > 0 { + cmdutils.ErrFatalf("Encountered errors parsing the following templates: \n%v", resultsToString(templateErrors)) + } + + fmt.Println("All template requirements met.") os.Exit(0) } -func resultsToString(results map[string][]int) string { +func resultsToString[T any](results map[string]T) string { var buf bytes.Buffer for template, reqs := range results { buf.WriteString(fmt.Sprintf("%s: %v\n", template, reqs)) diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 991c3feac66..77a53eacb6f 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -490,6 +490,48 @@ spec: } ` + TemplateReferentialBadAnnotation = ` +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8suniqueingresshostbadannotation + annotations: + metadata.gatekeeper.sh/title: "Unique Ingress Host" + metadata.gatekeeper.sh/version: 1.0.3 + metadata.gatekeeper.sh/requires-sync-data: | + "{}" + description: >- + Requires all Ingress rule hosts to be unique. + + Does not handle hostname wildcards: + https://kubernetes.io/docs/concepts/services-networking/ingress/ +spec: + crd: + spec: + names: + kind: K8sUniqueIngressHost + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8suniqueingresshost + + identical(obj, review) { + obj.metadata.namespace == review.object.metadata.namespace + obj.metadata.name == review.object.metadata.name + } + + violation[{"msg": msg}] { + input.review.kind.kind == "Ingress" + re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) + host := input.review.object.spec.rules[_].host + other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] + re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) + other.spec.rules[_].host == host + not identical(other, input.review) + msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) + } +` + ConstraintReferential = ` apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sUniqueServiceSelector diff --git a/pkg/gator/reader/read_constraints.go b/pkg/gator/reader/read_resources.go similarity index 90% rename from pkg/gator/reader/read_constraints.go rename to pkg/gator/reader/read_resources.go index 2ee35fe782a..f147ed3fc51 100644 --- a/pkg/gator/reader/read_constraints.go +++ b/pkg/gator/reader/read_resources.go @@ -61,7 +61,7 @@ func ReadUnstructureds(bytes []byte) ([]*unstructured.Unstructured, error) { continue } - u, err := readUnstructured([]byte(split)) + u, err := ReadUnstructured([]byte(split)) if err != nil { return nil, fmt.Errorf("%w: %w", gator.ErrInvalidYAML, err) } @@ -72,7 +72,7 @@ func ReadUnstructureds(bytes []byte) ([]*unstructured.Unstructured, error) { return result, nil } -func readUnstructured(bytes []byte) (*unstructured.Unstructured, error) { +func ReadUnstructured(bytes []byte) (*unstructured.Unstructured, error) { u := &unstructured.Unstructured{ Object: make(map[string]interface{}), } @@ -92,7 +92,7 @@ func ReadTemplate(scheme *runtime.Scheme, f fs.FS, path string) (*templates.Cons return nil, fmt.Errorf("reading ConstraintTemplate from %q: %w", path, err) } - u, err := readUnstructured(bytes) + u, err := ReadUnstructured(bytes) if err != nil { return nil, fmt.Errorf("%w: parsing ConstraintTemplate YAML from %q: %w", gator.ErrAddingTemplate, path, err) } @@ -111,7 +111,7 @@ func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime t, err := scheme.New(gvk) if err != nil { // The type isn't registered in the scheme. - return nil, fmt.Errorf("%w: %w", gator.ErrAddingTemplate, err) + return nil, err } // YAML parsing doesn't properly handle ObjectMeta, so we must @@ -124,15 +124,11 @@ func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime } err = json.Unmarshal(jsonBytes, t) if err != nil { - return nil, fmt.Errorf("%w: %w", gator.ErrAddingTemplate, err) + return nil, err } return t, nil } -// TODO (https://github.com/open-policy-agent/gatekeeper/issues/1779): Move -// this function into a location that makes it more obviously a shared resource -// between `gator test` and `gator verify` - // ToTemplate converts an unstructured template into a versionless ConstraintTemplate struct. func ToTemplate(scheme *runtime.Scheme, u *unstructured.Unstructured) (*templates.ConstraintTemplate, error) { if u.GroupVersionKind().Group != templatesv1.SchemeGroupVersion.Group || u.GroupVersionKind().Kind != "ConstraintTemplate" { @@ -206,7 +202,7 @@ func ReadObject(f fs.FS, path string) (*unstructured.Unstructured, error) { return nil, fmt.Errorf("reading Constraint from %q: %w", path, err) } - u, err := readUnstructured(bytes) + u, err := ReadUnstructured(bytes) if err != nil { return nil, fmt.Errorf("%w: parsing Constraint from %q: %w", gator.ErrAddingConstraint, path, err) } @@ -284,3 +280,23 @@ func ReadDiscoveryResults(r string) (DiscoveryResults, error) { return results, nil } + +func IsTemplate(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == templatesv1.SchemeGroupVersion.Group && gvk.Kind == "ConstraintTemplate" +} + +func IsConfig(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == "config.gatekeeper.sh" && gvk.Kind == "Config" +} + +func IsSyncSet(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == "syncset.gatekeeper.sh" && gvk.Kind == "SyncSet" +} + +func IsConstraint(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == "constraints.gatekeeper.sh" +} diff --git a/pkg/gator/reader/read_constraints_test.go b/pkg/gator/reader/read_resources_test.go similarity index 100% rename from pkg/gator/reader/read_constraints_test.go rename to pkg/gator/reader/read_resources_test.go diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index aa0bc0e228c..4cd96b00613 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -4,7 +4,6 @@ import ( "fmt" cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" - templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" gkapis "github.com/open-policy-agent/gatekeeper/v3/apis" "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" @@ -28,20 +27,21 @@ func init() { } } -func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) (map[string][]int, error) { +func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) (map[string][]int, map[string]error, error) { discoveryResults, err := reader.ReadDiscoveryResults(flagDiscoveryResults) if err != nil { - return nil, fmt.Errorf("reading: %w", err) + return nil, nil, fmt.Errorf("reading: %w", err) } templates := []*templates.ConstraintTemplate{} syncedGVKs := map[schema.GroupVersionKind]struct{}{} + templateErrs := map[string]error{} for _, obj := range unstrucs { - if isSyncSet(obj) { + if reader.IsSyncSet(obj) { syncSet, err := reader.ToSyncSet(scheme, obj) if err != nil { - return nil, fmt.Errorf("converting unstructured %q to syncset: %w", obj.GetName(), err) + return nil, nil, fmt.Errorf("converting unstructured %q to syncset: %w", obj.GetName(), err) } for _, gvkEntry := range syncSet.Spec.GVKs { gvk := schema.GroupVersionKind{ @@ -53,11 +53,10 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) syncedGVKs[gvk] = struct{}{} } } - } - if isConfig(obj) { + } else if reader.IsConfig(obj) { config, err := reader.ToConfig(scheme, obj) if err != nil { - return nil, fmt.Errorf("converting unstructured %q to config: %w", obj.GetName(), err) + return nil, nil, fmt.Errorf("converting unstructured %q to config: %w", obj.GetName(), err) } for _, syncOnlyEntry := range config.Spec.Sync.SyncOnly { gvk := schema.GroupVersionKind{ @@ -69,13 +68,14 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) syncedGVKs[gvk] = struct{}{} } } - } - if isTemplate(obj) { + } else if reader.IsTemplate(obj) { templ, err := reader.ToTemplate(scheme, obj) if err != nil { - return nil, fmt.Errorf("converting unstructured %q to template: %w", obj.GetName(), err) + templateErrs[obj.GetName()] = err } templates = append(templates, templ) + } else { + fmt.Printf("Skipping unstructured %q because it is not a syncset, config, or template\n", obj.GetName()) } } @@ -85,7 +85,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) // Fetch syncrequirements from template syncRequirements, err := parser.ReadSyncRequirements(templ) if err != nil { - return nil, fmt.Errorf("reading sync requirements from template %q: %w", templ.GetName(), err) + templateErrs[templ.GetName()] = err } for i, requirement := range syncRequirements { requirementMet := false @@ -99,20 +99,5 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) } } } - return missingReqs, nil -} - -func isTemplate(u *unstructured.Unstructured) bool { - gvk := u.GroupVersionKind() - return gvk.Group == templatesv1.SchemeGroupVersion.Group && gvk.Kind == "ConstraintTemplate" -} - -func isConfig(u *unstructured.Unstructured) bool { - gvk := u.GroupVersionKind() - return gvk.Group == "config.gatekeeper.sh" && gvk.Kind == "Config" -} - -func isSyncSet(u *unstructured.Unstructured) bool { - gvk := u.GroupVersionKind() - return gvk.Group == "syncset.gatekeeper.sh" && gvk.Kind == "SyncSet" + return missingReqs, templateErrs, nil } diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go index 8756796728d..6a462a0c4fe 100644 --- a/pkg/gator/sync/verify/verify_test.go +++ b/pkg/gator/sync/verify/verify_test.go @@ -1,13 +1,14 @@ package verify import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/yaml" ) func TestVerify(t *testing.T) { @@ -15,7 +16,8 @@ func TestVerify(t *testing.T) { name string inputs []string discovery string - want map[string][]int + wantReqs map[string][]int + wantErrs map[string]error err error }{ { @@ -23,9 +25,23 @@ func TestVerify(t *testing.T) { inputs: []string{ fixtures.TemplateReferential, }, - want: map[string][]int{ + wantReqs: map[string][]int{ "k8suniqueserviceselector": {1}, }, + wantErrs: map[string]error{}, + }, + { + name: "one template has error, one has basic req unfulfilled", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.TemplateReferentialBadAnnotation, + }, + wantReqs: map[string][]int{ + "k8suniqueserviceselector": {1}, + }, + wantErrs: map[string]error{ + "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), + }, }, { name: "basic req fulfilled by syncset", @@ -33,7 +49,8 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.Config, }, - want: map[string][]int{}, + wantReqs: map[string][]int{}, + wantErrs: map[string]error{}, }, { name: "basic req fulfilled by syncset and discoveryresults", @@ -42,7 +59,8 @@ func TestVerify(t *testing.T) { fixtures.Config, }, discovery: `{"": {"v1": ["Service"]}}`, - want: map[string][]int{}, + wantReqs: map[string][]int{}, + wantErrs: map[string]error{}, }, { name: "basic req fulfilled by syncset but not discoveryresults", @@ -51,9 +69,10 @@ func TestVerify(t *testing.T) { fixtures.Config, }, discovery: `{"extensions": {"v1beta1": ["Ingress"]}}`, - want: map[string][]int{ + wantReqs: map[string][]int{ "k8suniqueserviceselector": {1}, }, + wantErrs: map[string]error{}, }, { name: "multi equivalentset req fulfilled by syncset", @@ -61,7 +80,8 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultEquivSets, fixtures.SyncSet, }, - want: map[string][]int{}, + wantReqs: map[string][]int{}, + wantErrs: map[string]error{}, }, { name: "multi requirement, one req fulfilled by syncset", @@ -69,9 +89,10 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultReqs, fixtures.SyncSet, }, - want: map[string][]int{ + wantReqs: map[string][]int{ "k8suniqueingresshostmultireq": {2}, }, + wantErrs: map[string]error{}, }, { name: "multiple templates, syncset and config", @@ -82,14 +103,16 @@ func TestVerify(t *testing.T) { fixtures.Config, fixtures.SyncSet, }, - want: map[string][]int{ + wantReqs: map[string][]int{ "k8suniqueingresshostmultireq": {2}, }, + wantErrs: map[string]error{}, }, { - name: "no data of any kind", - inputs: []string{}, - want: map[string][]int{}, + name: "no data of any kind", + inputs: []string{}, + wantReqs: map[string][]int{}, + wantErrs: map[string]error{}, }, } @@ -98,34 +121,25 @@ func TestVerify(t *testing.T) { // convert the test resources to unstructureds var objs []*unstructured.Unstructured for _, input := range tc.inputs { - u, err := readUnstructured([]byte(input)) + u, err := reader.ReadUnstructured([]byte(input)) require.NoError(t, err) objs = append(objs, u) } - got, err := Verify(objs, tc.discovery) + gotReqs, gotErrs, err := Verify(objs, tc.discovery) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else if err != nil { require.NoError(t, err) } - if diff := cmp.Diff(tc.want, got); diff != "" { + if diff := cmp.Diff(tc.wantReqs, gotReqs); diff != "" { t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) } - }) - } -} - -func readUnstructured(bytes []byte) (*unstructured.Unstructured, error) { - u := &unstructured.Unstructured{ - Object: make(map[string]interface{}), - } - err := yaml.Unmarshal(bytes, u) - if err != nil { - return nil, err + if diff := cmp.Diff(tc.wantErrs, gotErrs); diff != "" { + t.Errorf("diff in templateErrs objects (-want +got):\n%s", diff) + } + }) } - - return u, nil } diff --git a/pkg/gator/test/test.go b/pkg/gator/test/test.go index d09aafcb616..9c7b6cdefc9 100644 --- a/pkg/gator/test/test.go +++ b/pkg/gator/test/test.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/open-policy-agent/frameworks/constraint/pkg/apis" - templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" constraintclient "github.com/open-policy-agent/frameworks/constraint/pkg/client" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/k8scel" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" @@ -58,7 +57,7 @@ func Test(objs []*unstructured.Unstructured, tOpts Opts) (*GatorResponses, error // search for templates, add them if they exist ctx := context.Background() for _, obj := range objs { - if !isTemplate(obj) { + if !reader.IsTemplate(obj) { continue } @@ -76,7 +75,7 @@ func Test(objs []*unstructured.Unstructured, tOpts Opts) (*GatorResponses, error // add all constraints. A constraint must be added after its associated // template or OPA will return an error for _, obj := range objs { - if !isConstraint(obj) { + if !reader.IsConstraint(obj) { continue } @@ -173,16 +172,6 @@ func Test(objs []*unstructured.Unstructured, tOpts Opts) (*GatorResponses, error return responses, nil } -func isTemplate(u *unstructured.Unstructured) bool { - gvk := u.GroupVersionKind() - return gvk.Group == templatesv1.SchemeGroupVersion.Group && gvk.Kind == "ConstraintTemplate" -} - -func isConstraint(u *unstructured.Unstructured) bool { - gvk := u.GroupVersionKind() - return gvk.Group == "constraints.gatekeeper.sh" -} - func makeRegoDriver(tOpts Opts) (*rego.Driver, error) { var args []rego.Arg if tOpts.GatherStats { From 1bf831addfcef49bedb1cdfeacdb9dde0414f19e Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 23 Oct 2023 16:19:24 -0700 Subject: [PATCH 08/24] Continue on err Signed-off-by: Anlan Du --- pkg/gator/sync/verify/verify.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index 4cd96b00613..7a1cf493bdf 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -72,6 +72,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) templ, err := reader.ToTemplate(scheme, obj) if err != nil { templateErrs[obj.GetName()] = err + continue } templates = append(templates, templ) } else { @@ -86,6 +87,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) syncRequirements, err := parser.ReadSyncRequirements(templ) if err != nil { templateErrs[templ.GetName()] = err + continue } for i, requirement := range syncRequirements { requirementMet := false From 218e58b1fe6e75a64a036c6e32b132a064fc99d7 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 3 Nov 2023 12:01:24 -0700 Subject: [PATCH 09/24] Comments Signed-off-by: Anlan Du --- cmd/gator/sync/verify/verify.go | 26 ++--- .../parser/syncannotationreader.go | 52 ++++++--- pkg/gator/fixtures/fixtures.go | 100 +----------------- pkg/gator/reader/read_resources.go | 2 +- pkg/gator/sync/verify/verify.go | 17 ++- pkg/gator/sync/verify/verify_test.go | 86 ++++++++++++--- pkg/gator/test/test_test.go | 33 ++---- website/docs/gator.md | 14 +-- 8 files changed, 155 insertions(+), 175 deletions(-) diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index 74b82d15a51..cb78553e279 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -1,9 +1,9 @@ package verify import ( - "bytes" "fmt" "os" + "strings" cmdutils "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/util" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" @@ -18,21 +18,21 @@ var Cmd = &cobra.Command{ } var ( - flagFilenames []string - flagImages []string - flagDiscoveryResults string + flagFilenames []string + flagImages []string + flagSupportedGVKs string ) const ( - flagNameFilename = "filename" - flagNameImage = "image" - flagNameDiscoveryResults = "discovery-results" + flagNameFilename = "filename" + flagNameImage = "image" + flagNameSupportedGVKs = "supported-gvks" ) func init() { Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().StringVarP(&flagDiscoveryResults, flagDiscoveryResults, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, containing supported kinds.") + Cmd.Flags().StringVarP(&flagSupportedGVKs, flagSupportedGVKs, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, each of which contains supported kinds. See https://open-policy-agent.github.io/gatekeeper/website/docs/gator#the-gator-sync-verify-subcommand for an example.") } func run(cmd *cobra.Command, args []string) { @@ -44,7 +44,7 @@ func run(cmd *cobra.Command, args []string) { cmdutils.ErrFatalf("no input data identified") } - missingRequirements, templateErrors, err := verify.Verify(unstrucs, flagDiscoveryResults) + missingRequirements, templateErrors, err := verify.Verify(unstrucs, flagSupportedGVKs) if err != nil { cmdutils.ErrFatalf("verifying: %v", err) } @@ -62,9 +62,9 @@ func run(cmd *cobra.Command, args []string) { } func resultsToString[T any](results map[string]T) string { - var buf bytes.Buffer - for template, reqs := range results { - buf.WriteString(fmt.Sprintf("%s: %v\n", template, reqs)) + var sb strings.Builder + for template, vals := range results { + sb.WriteString(fmt.Sprintf("%s:\n%v\n", template, vals)) } - return buf.String() + return sb.String() } diff --git a/pkg/cachemanager/parser/syncannotationreader.go b/pkg/cachemanager/parser/syncannotationreader.go index ae22dac98d8..f296db10561 100644 --- a/pkg/cachemanager/parser/syncannotationreader.go +++ b/pkg/cachemanager/parser/syncannotationreader.go @@ -2,6 +2,7 @@ package parser import ( "encoding/json" + "fmt" "strings" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" @@ -13,7 +14,7 @@ import ( 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. +// contains a GVK equivalence set. type SyncRequirements []GVKEquivalenceSet // GVKEquivalenceSet is a set of GVKs that a template can use @@ -21,14 +22,14 @@ type SyncRequirements []GVKEquivalenceSet 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 +// which contains a list of GVK clauses. +type CompactSyncRequirements [][]GVKClause -// 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. +// GVKClause contains a set of equivalent GVKs, expressed +// in the form [groups, versions, kinds] where any combination of +// items from these three fields can be considered a valid option. // Used for unmarshalling as this is the form used in requiressync annotations. -type CompactGVKEquivalenceSet struct { +type GVKClause struct { Groups []string `json:"groups"` Versions []string `json:"versions"` Kinds []string `json:"kinds"` @@ -57,12 +58,13 @@ func ReadSyncRequirements(t *templates.ConstraintTemplate) (SyncRequirements, er return SyncRequirements{}, nil } -// Takes a compactGVKSet and expands it into a GVKEquivalenceSet. -func ExpandCompactEquivalenceSet(compactEquivalenceSet CompactGVKEquivalenceSet) GVKEquivalenceSet { +// Takes a GVK Clause and expands it into a GVKEquivalenceSet (to be unioned +// with the GVKEquivalenceSet expansions of the other clauses). +func ExpandGVKClause(clause GVKClause) GVKEquivalenceSet { equivalenceSet := GVKEquivalenceSet{} - for _, group := range compactEquivalenceSet.Groups { - for _, version := range compactEquivalenceSet.Versions { - for _, kind := range compactEquivalenceSet.Kinds { + for _, group := range clause.Groups { + for _, version := range clause.Versions { + for _, kind := range clause.Kinds { equivalenceSet[schema.GroupVersionKind{Group: group, Version: version, Kind: kind}] = struct{}{} } } @@ -76,8 +78,8 @@ func ExpandCompactRequirements(compactSyncRequirements CompactSyncRequirements) syncRequirements := SyncRequirements{} for _, compactRequirement := range compactSyncRequirements { requirement := GVKEquivalenceSet{} - for _, compactEquivalenceSet := range compactRequirement { - for equivalentGVK := range ExpandCompactEquivalenceSet(compactEquivalenceSet) { + for _, clause := range compactRequirement { + for equivalentGVK := range ExpandGVKClause(clause) { requirement[equivalentGVK] = struct{}{} } } @@ -85,3 +87,25 @@ func ExpandCompactRequirements(compactSyncRequirements CompactSyncRequirements) } return syncRequirements, nil } + +func (s GVKEquivalenceSet) String() string { + var sb strings.Builder + for gvk := range s { + if sb.Len() != 0 { + sb.WriteString(" OR ") + } + sb.WriteString(fmt.Sprintf("%s/%s:%s", gvk.Group, gvk.Version, gvk.Kind)) + } + return sb.String() +} + +func (s SyncRequirements) String() string { + var sb strings.Builder + for _, equivSet := range s { + if sb.Len() != 0 { + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf("- %v", equivSet)) + } + return sb.String() +} diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 77a53eacb6f..b4e5eab1dc1 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -401,36 +401,6 @@ metadata: } ] ]" - description: >- - Requires all Ingress rule hosts to be unique. - - Does not handle hostname wildcards: - https://kubernetes.io/docs/concepts/services-networking/ingress/ -spec: - crd: - spec: - names: - kind: K8sUniqueIngressHost - targets: - - target: admission.k8s.gatekeeper.sh - rego: | - package k8suniqueingresshost - - identical(obj, review) { - obj.metadata.namespace == review.object.metadata.namespace - obj.metadata.name == review.object.metadata.name - } - - violation[{"msg": msg}] { - input.review.kind.kind == "Ingress" - re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) - host := input.review.object.spec.rules[_].host - other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] - re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) - other.spec.rules[_].host == host - not identical(other, input.review) - msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) - } ` TemplateReferentialMultReqs = ` @@ -445,9 +415,9 @@ metadata: "[ [ { - "groups": ["extensions"], - "versions": ["v1beta1"], - "kinds": ["Ingress"] + "groups": [""], + "versions": ["v1"], + "kinds": ["Pod"] } ], [ @@ -458,36 +428,6 @@ metadata: } ] ]" - description: >- - Requires all Ingress rule hosts to be unique. - - Does not handle hostname wildcards: - https://kubernetes.io/docs/concepts/services-networking/ingress/ -spec: - crd: - spec: - names: - kind: K8sUniqueIngressHost - targets: - - target: admission.k8s.gatekeeper.sh - rego: | - package k8suniqueingresshost - - identical(obj, review) { - obj.metadata.namespace == review.object.metadata.namespace - obj.metadata.name == review.object.metadata.name - } - - violation[{"msg": msg}] { - input.review.kind.kind == "Ingress" - re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) - host := input.review.object.spec.rules[_].host - other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] - re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) - other.spec.rules[_].host == host - not identical(other, input.review) - msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) - } ` TemplateReferentialBadAnnotation = ` @@ -500,36 +440,6 @@ metadata: metadata.gatekeeper.sh/version: 1.0.3 metadata.gatekeeper.sh/requires-sync-data: | "{}" - description: >- - Requires all Ingress rule hosts to be unique. - - Does not handle hostname wildcards: - https://kubernetes.io/docs/concepts/services-networking/ingress/ -spec: - crd: - spec: - names: - kind: K8sUniqueIngressHost - targets: - - target: admission.k8s.gatekeeper.sh - rego: | - package k8suniqueingresshost - - identical(obj, review) { - obj.metadata.namespace == review.object.metadata.namespace - obj.metadata.name == review.object.metadata.name - } - - violation[{"msg": msg}] { - input.review.kind.kind == "Ingress" - re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) - host := input.review.object.spec.rules[_].host - other := data.inventory.namespace[_][otherapiversion]["Ingress"][name] - re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) - other.spec.rules[_].host == host - not identical(other, input.review) - msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) - } ` ConstraintReferential = ` @@ -754,8 +664,8 @@ metadata: namespace: "gatekeeper-system" spec: gvks: - - group: "extensions" - version: "v1beta1" + - group: "networking.k8s.io" + version: "v1" kind: "Ingress" - group: "apps" version: "v1" diff --git a/pkg/gator/reader/read_resources.go b/pkg/gator/reader/read_resources.go index f147ed3fc51..737e296aa1a 100644 --- a/pkg/gator/reader/read_resources.go +++ b/pkg/gator/reader/read_resources.go @@ -104,7 +104,7 @@ func ReadTemplate(scheme *runtime.Scheme, f fs.FS, path string) (*templates.Cons return template, nil } -// ToTemplate converts an unstructured object into an object with the schema defined +// ToStructured converts an unstructured object into an object with the schema defined // by u's group, version, and kind. func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime.Object, error) { gvk := u.GroupVersionKind() diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index 7a1cf493bdf..51a3fad1027 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -27,8 +27,10 @@ func init() { } } -func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) (map[string][]int, map[string]error, error) { - discoveryResults, err := reader.ReadDiscoveryResults(flagDiscoveryResults) +// Reads a list of unstructured objects and a string containing supported GVKs and +// outputs a set of missing sync requirements per template and ingestion problems per template +func Verify(unstrucs []*unstructured.Unstructured, flagSupportedGVKs string) (map[string]parser.SyncRequirements, map[string]error, error) { + discoveryResults, err := reader.ReadDiscoveryResults(flagSupportedGVKs) if err != nil { return nil, nil, fmt.Errorf("reading: %w", err) } @@ -36,6 +38,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) templates := []*templates.ConstraintTemplate{} syncedGVKs := map[schema.GroupVersionKind]struct{}{} templateErrs := map[string]error{} + hasConfig := false for _, obj := range unstrucs { if reader.IsSyncSet(obj) { @@ -54,10 +57,14 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) } } } else if reader.IsConfig(obj) { + if hasConfig { + return nil, nil, fmt.Errorf("Multiple configs found. Config is a singleton resource.") + } config, err := reader.ToConfig(scheme, obj) if err != nil { return nil, nil, fmt.Errorf("converting unstructured %q to config: %w", obj.GetName(), err) } + hasConfig = true for _, syncOnlyEntry := range config.Spec.Sync.SyncOnly { gvk := schema.GroupVersionKind{ Group: syncOnlyEntry.Group, @@ -80,7 +87,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) } } - missingReqs := map[string][]int{} + missingReqs := map[string]parser.SyncRequirements{} for _, templ := range templates { // Fetch syncrequirements from template @@ -89,7 +96,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) templateErrs[templ.GetName()] = err continue } - for i, requirement := range syncRequirements { + for _, requirement := range syncRequirements { requirementMet := false for gvk := range requirement { if _, exists := syncedGVKs[gvk]; exists { @@ -97,7 +104,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagDiscoveryResults string) } } if !requirementMet { - missingReqs[templ.Name] = append(missingReqs[templ.Name], i+1) + missingReqs[templ.Name] = append(missingReqs[templ.Name], requirement) } } } diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go index 6a462a0c4fe..618ba61f387 100644 --- a/pkg/gator/sync/verify/verify_test.go +++ b/pkg/gator/sync/verify/verify_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "github.com/stretchr/testify/require" @@ -16,7 +17,7 @@ func TestVerify(t *testing.T) { name string inputs []string discovery string - wantReqs map[string][]int + wantReqs map[string][]parser.GVKEquivalenceSet wantErrs map[string]error err error }{ @@ -25,8 +26,16 @@ func TestVerify(t *testing.T) { inputs: []string{ fixtures.TemplateReferential, }, - wantReqs: map[string][]int{ - "k8suniqueserviceselector": {1}, + wantReqs: map[string][]parser.GVKEquivalenceSet{ + "k8suniqueserviceselector": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + }, }, wantErrs: map[string]error{}, }, @@ -36,8 +45,16 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.TemplateReferentialBadAnnotation, }, - wantReqs: map[string][]int{ - "k8suniqueserviceselector": {1}, + wantReqs: map[string][]parser.GVKEquivalenceSet{ + "k8suniqueserviceselector": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + }, }, wantErrs: map[string]error{ "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), @@ -49,7 +66,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.Config, }, - wantReqs: map[string][]int{}, + wantReqs: map[string][]parser.GVKEquivalenceSet{}, wantErrs: map[string]error{}, }, { @@ -59,7 +76,7 @@ func TestVerify(t *testing.T) { fixtures.Config, }, discovery: `{"": {"v1": ["Service"]}}`, - wantReqs: map[string][]int{}, + wantReqs: map[string][]parser.GVKEquivalenceSet{}, wantErrs: map[string]error{}, }, { @@ -69,8 +86,16 @@ func TestVerify(t *testing.T) { fixtures.Config, }, discovery: `{"extensions": {"v1beta1": ["Ingress"]}}`, - wantReqs: map[string][]int{ - "k8suniqueserviceselector": {1}, + wantReqs: map[string][]parser.GVKEquivalenceSet{ + "k8suniqueserviceselector": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + }, }, wantErrs: map[string]error{}, }, @@ -80,7 +105,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultEquivSets, fixtures.SyncSet, }, - wantReqs: map[string][]int{}, + wantReqs: map[string][]parser.GVKEquivalenceSet{}, wantErrs: map[string]error{}, }, { @@ -89,8 +114,16 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultReqs, fixtures.SyncSet, }, - wantReqs: map[string][]int{ - "k8suniqueingresshostmultireq": {2}, + wantReqs: map[string][]parser.GVKEquivalenceSet{ + "k8suniqueingresshostmultireq": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Pod", + }: struct{}{}, + }, + }, }, wantErrs: map[string]error{}, }, @@ -103,15 +136,23 @@ func TestVerify(t *testing.T) { fixtures.Config, fixtures.SyncSet, }, - wantReqs: map[string][]int{ - "k8suniqueingresshostmultireq": {2}, + wantReqs: map[string][]parser.GVKEquivalenceSet{ + "k8suniqueingresshostmultireq": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Pod", + }: struct{}{}, + }, + }, }, wantErrs: map[string]error{}, }, { name: "no data of any kind", inputs: []string{}, - wantReqs: map[string][]int{}, + wantReqs: map[string][]parser.GVKEquivalenceSet{}, wantErrs: map[string]error{}, }, } @@ -137,8 +178,19 @@ func TestVerify(t *testing.T) { t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) } - if diff := cmp.Diff(tc.wantErrs, gotErrs); diff != "" { - t.Errorf("diff in templateErrs objects (-want +got):\n%s", diff) + for key, wantErr := range tc.wantErrs { + if gotErr, ok := gotErrs[key]; ok { + if wantErr.Error() != gotErr.Error() { + t.Errorf("error mismatch for %s: want %v, got %v", key, wantErr, gotErr) + } + } else { + t.Errorf("missing error for %s", key) + } + } + for key, gotErr := range gotErrs { + if _, ok := tc.wantErrs[key]; !ok { + t.Errorf("unexpected error for %s: %v", key, gotErr) + } } }) } diff --git a/pkg/gator/test/test_test.go b/pkg/gator/test/test_test.go index 95a0d18ac39..954921ef889 100644 --- a/pkg/gator/test/test_test.go +++ b/pkg/gator/test/test_test.go @@ -9,11 +9,11 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" "github.com/open-policy-agent/frameworks/constraint/pkg/types" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "github.com/open-policy-agent/gatekeeper/v3/pkg/target" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/yaml" ) var ( @@ -27,32 +27,32 @@ var ( func init() { var err error - templateNeverValidate, err = readUnstructured([]byte(fixtures.TemplateNeverValidate)) + templateNeverValidate, err = reader.ReadUnstructured([]byte(fixtures.TemplateNeverValidate)) if err != nil { panic(err) } - constraintNeverValidate, err = readUnstructured([]byte(fixtures.ConstraintNeverValidate)) + constraintNeverValidate, err = reader.ReadUnstructured([]byte(fixtures.ConstraintNeverValidate)) if err != nil { panic(err) } - constraintReferential, err = readUnstructured([]byte(fixtures.ConstraintReferential)) + constraintReferential, err = reader.ReadUnstructured([]byte(fixtures.ConstraintReferential)) if err != nil { panic(err) } - object, err = readUnstructured([]byte(fixtures.Object)) + object, err = reader.ReadUnstructured([]byte(fixtures.Object)) if err != nil { panic(err) } - objectReferentialInventory, err = readUnstructured([]byte(fixtures.ObjectReferentialInventory)) + objectReferentialInventory, err = reader.ReadUnstructured([]byte(fixtures.ObjectReferentialInventory)) if err != nil { panic(err) } - objectReferentialDeny, err = readUnstructured([]byte(fixtures.ObjectReferentialDeny)) + objectReferentialDeny, err = reader.ReadUnstructured([]byte(fixtures.ObjectReferentialDeny)) if err != nil { panic(err) } @@ -187,7 +187,7 @@ func TestTest(t *testing.T) { // convert the test resources to unstructureds var objs []*unstructured.Unstructured for _, input := range tc.inputs { - u, err := readUnstructured([]byte(input)) + u, err := reader.ReadUnstructured([]byte(input)) require.NoError(t, err) objs = append(objs, u) } @@ -225,7 +225,7 @@ func Test_Test_withTrace(t *testing.T) { var objs []*unstructured.Unstructured for _, input := range inputs { - u, err := readUnstructured([]byte(input)) + u, err := reader.ReadUnstructured([]byte(input)) if err != nil { t.Fatalf("readUnstructured for input %q: %v", input, err) } @@ -288,7 +288,7 @@ func Test_Test_withStats(t *testing.T) { var objs []*unstructured.Unstructured for _, input := range inputs { - u, err := readUnstructured([]byte(input)) + u, err := reader.ReadUnstructured([]byte(input)) assert.NoErrorf(t, err, "readUnstructured for input %q: %v", input, err) objs = append(objs, u) } @@ -358,16 +358,3 @@ func Test_Test_withStats(t *testing.T) { } } } - -func readUnstructured(bytes []byte) (*unstructured.Unstructured, error) { - u := &unstructured.Unstructured{ - Object: make(map[string]interface{}), - } - - err := yaml.Unmarshal(bytes, u) - if err != nil { - return nil, err - } - - return u, nil -} diff --git a/website/docs/gator.md b/website/docs/gator.md index 08003cb41ac..dff87be56f8 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -429,23 +429,23 @@ error expanding resources: error expanding resource nginx-deployment: failed to ## The `gator sync verify` subcommand -Certain templates require [replicating data](sync.md) into OPA to enable correct evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more equivalence sets. Each of these equivalence sets has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within an equivalence set within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): +Certain templates require [replicating data](sync.md) into OPA to enable correct evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more GVK clauses forming an equivalence set of interchangeable GVKs. Each of these clauses has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within a clause within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): ``` [ [ // Requirement 1 - { // Equivalence set 1 + { // Clause 1 "groups": ["group1", group2"] "versions": ["version1", "version2", "version3"] "kinds": ["kind1", "kind2"] }, - { // Equivalence Set 2 + { // Clause 2 "groups": ["group3", group4"] "versions": ["version3", "version4"] "kinds": ["kind3", "kind4"] } ], [ // Requirement 2 - { // Equivalence Set 3 + { // Clause 1 "groups": ["group5"] "versions": ["version5"] "kinds": ["kind5"] @@ -453,13 +453,13 @@ Certain templates require [replicating data](sync.md) into OPA to enable correct ] ] ``` -This annotation contains two requirements. Requirement 1 contains two equivalence sets. Syncing resources of group1, version3, kind1 (drawn from equivalence set 1) would be sufficient to fulfill Requirement 1. So, too, would syncing resources of group3, version3, kind4 (drawn from equivalence set 2). Syncing resources of group1, version1, and kind3 would not be, however. +This annotation contains two requirements. Requirement 1 contains two clauses. Syncing resources of group1, version3, kind1 (drawn from clause 1) would be sufficient to fulfill Requirement 1. So, too, would syncing resources of group3, version3, kind4 (drawn from clause 2). Syncing resources of group1, version1, and kind3 would not be, however. Requirement 2 is simpler: it denotes that group5, version5, kind5 must be synced for the policy to work properly. This template annotation is descriptive, not prescriptive. The prescription of which resources to sync is done in `SyncSet` resources and/or the Gatekeeper `Config` resource. The management of these various requirements can get challenging as the number of templates requiring replicated data increases. -`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. The user passes in any number of Constraint Templates, SyncSets, and Gatekeeper Config objects (although in practice only one Config object should exist), and the command will determine which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). +`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. The user passes in a set of Constraint Templates, SyncSets, and/or a Gatekeeper Config, and the command will determine which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). ### Usage @@ -493,7 +493,7 @@ Optionally, the `--discovery-results` flag can be used to pass in a list of supp #### Exit Codes `gator sync verify` will return a `0` exit status when the Templates, SyncSets, and -Configs are successfully ingested and no requirements are unfulfilled. +Config are successfully ingested and no requirements are unfulfilled. An error during evaluation, for example a failure to read a file, will result in a `1` exit status with an error message printed to stderr. From e798bfd1741bd535775e75121c01e69b44751054 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 3 Nov 2023 18:39:52 -0700 Subject: [PATCH 10/24] Marshal supported GVKs up front Signed-off-by: Anlan Du --- cmd/gator/sync/verify/verify.go | 4 +-- pkg/gator/reader/read_resources.go | 29 +--------------- pkg/gator/sync/verify/verify.go | 45 ++++++++++++++++++++----- pkg/gator/sync/verify/verify_test.go | 49 +++++++++++++++++----------- website/docs/gator.md | 2 +- 5 files changed, 71 insertions(+), 58 deletions(-) diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index cb78553e279..ea703be7598 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -20,7 +20,7 @@ var Cmd = &cobra.Command{ var ( flagFilenames []string flagImages []string - flagSupportedGVKs string + flagSupportedGVKs verify.SupportedGVKs ) const ( @@ -32,7 +32,7 @@ const ( func init() { Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().StringVarP(&flagSupportedGVKs, flagSupportedGVKs, "d", "", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, each of which contains supported kinds. See https://open-policy-agent.github.io/gatekeeper/website/docs/gator#the-gator-sync-verify-subcommand for an example.") + Cmd.Flags().VarP(&flagSupportedGVKs, flagNameSupportedGVKs, "s", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, each of which contains supported kinds. See https://open-policy-agent.github.io/gatekeeper/website/docs/gator#the-gator-sync-verify-subcommand for an example.") } func run(cmd *cobra.Command, args []string) { diff --git a/pkg/gator/reader/read_resources.go b/pkg/gator/reader/read_resources.go index 737e296aa1a..d0c9e9457ed 100644 --- a/pkg/gator/reader/read_resources.go +++ b/pkg/gator/reader/read_resources.go @@ -15,7 +15,6 @@ import ( "github.com/open-policy-agent/gatekeeper/v3/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" ) @@ -104,7 +103,7 @@ func ReadTemplate(scheme *runtime.Scheme, f fs.FS, path string) (*templates.Cons return template, nil } -// ToStructured converts an unstructured object into an object with the schema defined +// ToTemplate converts an unstructured object into an object with the schema defined // by u's group, version, and kind. func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime.Object, error) { gvk := u.GroupVersionKind() @@ -255,32 +254,6 @@ func ReadK8sResources(r io.Reader) ([]*unstructured.Unstructured, error) { return objs, nil } -type DiscoveryResults map[schema.GroupVersionKind]struct{} - -func ReadDiscoveryResults(r string) (DiscoveryResults, error) { - if r == "" { - return nil, nil - } - var stringAsJSON map[string]map[string][]string - if err := json.Unmarshal([]byte(r), &stringAsJSON); err != nil { - return nil, err - } - results := DiscoveryResults{} - for group, versions := range stringAsJSON { - for version, kinds := range versions { - for _, kind := range kinds { - results[schema.GroupVersionKind{ - Group: group, - Version: version, - Kind: kind, - }] = struct{}{} - } - } - } - - return results, nil -} - func IsTemplate(u *unstructured.Unstructured) bool { gvk := u.GroupVersionKind() return gvk.Group == templatesv1.SchemeGroupVersion.Group && gvk.Kind == "ConstraintTemplate" diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index 51a3fad1027..a342dbbedc7 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -1,6 +1,7 @@ package verify import ( + "encoding/json" "fmt" cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" @@ -13,6 +14,39 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +type SupportedGVKs map[schema.GroupVersionKind]struct{} + +func (s *SupportedGVKs) String() string { + return fmt.Sprintf("%v", *s) +} + +func (s *SupportedGVKs) Set(value string) error { + if value == "" { + return nil + } + var stringAsJSON map[string]map[string][]string + if err := json.Unmarshal([]byte(value), &stringAsJSON); err != nil { + return err + } + *s = SupportedGVKs{} + for group, versions := range stringAsJSON { + for version, kinds := range versions { + for _, kind := range kinds { + (*s)[schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }] = struct{}{} + } + } + } + return nil +} + +func (s *SupportedGVKs) Type() string { + return "SupportedGVKs" +} + var scheme *runtime.Scheme func init() { @@ -29,12 +63,7 @@ func init() { // Reads a list of unstructured objects and a string containing supported GVKs and // outputs a set of missing sync requirements per template and ingestion problems per template -func Verify(unstrucs []*unstructured.Unstructured, flagSupportedGVKs string) (map[string]parser.SyncRequirements, map[string]error, error) { - discoveryResults, err := reader.ReadDiscoveryResults(flagSupportedGVKs) - if err != nil { - return nil, nil, fmt.Errorf("reading: %w", err) - } - +func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) (map[string]parser.SyncRequirements, map[string]error, error) { templates := []*templates.ConstraintTemplate{} syncedGVKs := map[schema.GroupVersionKind]struct{}{} templateErrs := map[string]error{} @@ -52,7 +81,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagSupportedGVKs string) (ma Version: gvkEntry.Version, Kind: gvkEntry.Kind, } - if _, exists := discoveryResults[gvk]; exists || discoveryResults == nil { + if _, exists := supportedGVKs[gvk]; exists || supportedGVKs == nil { syncedGVKs[gvk] = struct{}{} } } @@ -71,7 +100,7 @@ func Verify(unstrucs []*unstructured.Unstructured, flagSupportedGVKs string) (ma Version: syncOnlyEntry.Version, Kind: syncOnlyEntry.Kind, } - if _, exists := discoveryResults[gvk]; exists || discoveryResults == nil { + if _, exists := supportedGVKs[gvk]; exists || supportedGVKs == nil { syncedGVKs[gvk] = struct{}{} } } diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go index 618ba61f387..a4b0e349874 100644 --- a/pkg/gator/sync/verify/verify_test.go +++ b/pkg/gator/sync/verify/verify_test.go @@ -14,19 +14,19 @@ import ( func TestVerify(t *testing.T) { tcs := []struct { - name string - inputs []string - discovery string - wantReqs map[string][]parser.GVKEquivalenceSet - wantErrs map[string]error - err error + name string + inputs []string + supportedGVKs SupportedGVKs + wantReqs map[string]parser.SyncRequirements + wantErrs map[string]error + err error }{ { name: "basic req unfulfilled", inputs: []string{ fixtures.TemplateReferential, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{ + wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { parser.GVKEquivalenceSet{ { @@ -45,7 +45,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.TemplateReferentialBadAnnotation, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{ + wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { parser.GVKEquivalenceSet{ { @@ -66,7 +66,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.Config, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{}, + wantReqs: map[string]parser.SyncRequirements{}, wantErrs: map[string]error{}, }, { @@ -75,9 +75,15 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.Config, }, - discovery: `{"": {"v1": ["Service"]}}`, - wantReqs: map[string][]parser.GVKEquivalenceSet{}, - wantErrs: map[string]error{}, + supportedGVKs: SupportedGVKs{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + wantReqs: map[string]parser.SyncRequirements{}, + wantErrs: map[string]error{}, }, { name: "basic req fulfilled by syncset but not discoveryresults", @@ -85,8 +91,13 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferential, fixtures.Config, }, - discovery: `{"extensions": {"v1beta1": ["Ingress"]}}`, - wantReqs: map[string][]parser.GVKEquivalenceSet{ + supportedGVKs: SupportedGVKs{ + { + Group: "extensions", + Version: "v1beta1", + Kind: "Ingress", + }: struct{}{}, + }, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { parser.GVKEquivalenceSet{ { @@ -105,7 +116,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultEquivSets, fixtures.SyncSet, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{}, + wantReqs: map[string]parser.SyncRequirements{}, wantErrs: map[string]error{}, }, { @@ -114,7 +125,7 @@ func TestVerify(t *testing.T) { fixtures.TemplateReferentialMultReqs, fixtures.SyncSet, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{ + wantReqs: map[string]parser.SyncRequirements{ "k8suniqueingresshostmultireq": { parser.GVKEquivalenceSet{ { @@ -136,7 +147,7 @@ func TestVerify(t *testing.T) { fixtures.Config, fixtures.SyncSet, }, - wantReqs: map[string][]parser.GVKEquivalenceSet{ + wantReqs: map[string]parser.SyncRequirements{ "k8suniqueingresshostmultireq": { parser.GVKEquivalenceSet{ { @@ -152,7 +163,7 @@ func TestVerify(t *testing.T) { { name: "no data of any kind", inputs: []string{}, - wantReqs: map[string][]parser.GVKEquivalenceSet{}, + wantReqs: map[string]parser.SyncRequirements{}, wantErrs: map[string]error{}, }, } @@ -167,7 +178,7 @@ func TestVerify(t *testing.T) { objs = append(objs, u) } - gotReqs, gotErrs, err := Verify(objs, tc.discovery) + gotReqs, gotErrs, err := Verify(objs, tc.supportedGVKs) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else if err != nil { diff --git a/website/docs/gator.md b/website/docs/gator.md index 116408638e1..11a81ab8ac6 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -545,7 +545,7 @@ Or, using an OCI Artifact containing templates as described previously: gator sync verify --filename="config.yaml" --image=localhost:5000/gator/template-library:v1 ``` -Optionally, the `--discovery-results` flag can be used to pass in a list of supported GVKs discovered on a cluster. If this is nonempty, only GVKs that are both included in a SyncSet/Config and are supported will be considered to fulfill a template's requirements. Discovery results should be passed as a string representing a JSON object mapping groups to lists of versions, which contain lists of kinds, like such: +Optionally, the `--supported-gvks` flag can be used to pass in a list of supported GVKs for the applicable cluster. If this is nonempty, only GVKs that are both included in a SyncSet/Config and are supported will be considered to fulfill a template's requirements. Supported GVKs should be passed as a string representing a JSON object mapping groups to lists of versions, which contain lists of kinds, like such: ``` { "group1": { From 7f7f3d9bf66fdb8fe31486a1dfb337e31da65b77 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Fri, 3 Nov 2023 18:54:32 -0700 Subject: [PATCH 11/24] lint Signed-off-by: Anlan Du --- cmd/gator/sync/verify/verify.go | 6 +++--- pkg/gator/sync/verify/verify.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/verify/verify.go index ea703be7598..3964c1b38c3 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/verify/verify.go @@ -50,14 +50,14 @@ func run(cmd *cobra.Command, args []string) { } if len(missingRequirements) > 0 { - cmdutils.ErrFatalf("The following requirements were not met: \n%v", resultsToString(missingRequirements)) + cmdutils.ErrFatalf("the following requirements were not met: \n%v", resultsToString(missingRequirements)) } if len(templateErrors) > 0 { - cmdutils.ErrFatalf("Encountered errors parsing the following templates: \n%v", resultsToString(templateErrors)) + cmdutils.ErrFatalf("encountered errors parsing the following templates: \n%v", resultsToString(templateErrors)) } - fmt.Println("All template requirements met.") + fmt.Println("all template requirements met") os.Exit(0) } diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/verify/verify.go index a342dbbedc7..73f55b7aa6f 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/verify/verify.go @@ -62,7 +62,7 @@ func init() { } // Reads a list of unstructured objects and a string containing supported GVKs and -// outputs a set of missing sync requirements per template and ingestion problems per template +// outputs a set of missing sync requirements per template and ingestion problems per template. func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) (map[string]parser.SyncRequirements, map[string]error, error) { templates := []*templates.ConstraintTemplate{} syncedGVKs := map[schema.GroupVersionKind]struct{}{} @@ -87,7 +87,7 @@ func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) } } else if reader.IsConfig(obj) { if hasConfig { - return nil, nil, fmt.Errorf("Multiple configs found. Config is a singleton resource.") + return nil, nil, fmt.Errorf("multiple configs found. Config is a singleton resource") } config, err := reader.ToConfig(scheme, obj) if err != nil { @@ -112,7 +112,7 @@ func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) } templates = append(templates, templ) } else { - fmt.Printf("Skipping unstructured %q because it is not a syncset, config, or template\n", obj.GetName()) + fmt.Printf("skipping unstructured %q because it is not a syncset, config, or template\n", obj.GetName()) } } From 1431bb9b7ecee6c429db22ad20938e4de973cee4 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 8 Nov 2023 19:16:56 -0800 Subject: [PATCH 12/24] Comments Signed-off-by: Anlan Du --- Makefile | 2 +- apis/addtoscheme_gvkmanifest.go | 10 + .../gvkmanifest/v1alpha1/groupversion_info.go | 20 ++ .../gvkmanifest/v1alpha1/gvkmanifest_types.go | 43 ++++ .../v1alpha1/zz_generated.deepcopy.go | 147 +++++++++++++ cmd/gator/sync/sync.go | 6 +- .../sync/{verify/verify.go => test/test.go} | 28 +-- pkg/gator/errors.go | 5 + pkg/gator/fixtures/fixtures.go | 14 +- pkg/gator/reader/read_resources.go | 29 ++- .../sync/{verify/verify.go => test/test.go} | 113 +++++----- pkg/gator/sync/test/test_test.go | 205 +++++++++++++++++ pkg/gator/sync/verify/verify_test.go | 208 ------------------ website/docs/gator.md | 41 ++-- 14 files changed, 572 insertions(+), 299 deletions(-) create mode 100644 apis/addtoscheme_gvkmanifest.go create mode 100644 apis/gvkmanifest/v1alpha1/groupversion_info.go create mode 100644 apis/gvkmanifest/v1alpha1/gvkmanifest_types.go create mode 100644 apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go rename cmd/gator/sync/{verify/verify.go => test/test.go} (59%) rename pkg/gator/sync/{verify/verify.go => test/test.go} (53%) create mode 100644 pkg/gator/sync/test/test_test.go delete mode 100644 pkg/gator/sync/verify/verify_test.go diff --git a/Makefile b/Makefile index 3ecc2294a88..70185bcb333 100644 --- a/Makefile +++ b/Makefile @@ -341,7 +341,7 @@ generate: __conversion-gen __controller-gen $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./apis/..." paths="./pkg/..." $(CONVERSION_GEN) \ --output-base=/gatekeeper \ - --input-dirs=./apis/mutations/v1,./apis/mutations/v1beta1,./apis/mutations/v1alpha1,./apis/expansion/v1alpha1,./apis/syncset/v1alpha1 \ + --input-dirs=./apis/mutations/v1,./apis/mutations/v1beta1,./apis/mutations/v1alpha1,./apis/expansion/v1alpha1,./apis/syncset/v1alpha1,./apis/gvkmanifest/v1alpha1 \ --go-header-file=./hack/boilerplate.go.txt \ --output-file-base=zz_generated.conversion diff --git a/apis/addtoscheme_gvkmanifest.go b/apis/addtoscheme_gvkmanifest.go new file mode 100644 index 00000000000..11f47b48238 --- /dev/null +++ b/apis/addtoscheme_gvkmanifest.go @@ -0,0 +1,10 @@ +package apis + +import ( + "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, v1alpha1.AddToScheme) +} diff --git a/apis/gvkmanifest/v1alpha1/groupversion_info.go b/apis/gvkmanifest/v1alpha1/groupversion_info.go new file mode 100644 index 00000000000..c4e4d067b8b --- /dev/null +++ b/apis/gvkmanifest/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the GVKManifest v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=gvkmanifest.gatekeeper.sh +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "gvkmanifest.gatekeeper.sh", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go b/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go new file mode 100644 index 00000000000..b1eddabca8b --- /dev/null +++ b/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go @@ -0,0 +1,43 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GVKManifestSpec struct { + Groups []Group `json:"groups,omitempty"` +} + +type Group struct { + Name string `json:"name,omitempty"` + Versions []Version `json:"versions,omitempty"` +} + +type Version struct { + Name string `json:"name,omitempty"` + Kinds []string `json:"kinds,omitempty"` +} + +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:object:root=true + +// GVKManifest is the Schema for the GVKManifest API. +type GVKManifest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GVKManifestSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// GVKManifestList contains a list of GVKManifests. +type GVKManifestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GVKManifest `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GVKManifest{}, &GVKManifestList{}) +} diff --git a/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..a8b17c00767 --- /dev/null +++ b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,147 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GVKManifest) DeepCopyInto(out *GVKManifest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GVKManifest. +func (in *GVKManifest) DeepCopy() *GVKManifest { + if in == nil { + return nil + } + out := new(GVKManifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GVKManifest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GVKManifestList) DeepCopyInto(out *GVKManifestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GVKManifest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GVKManifestList. +func (in *GVKManifestList) DeepCopy() *GVKManifestList { + if in == nil { + return nil + } + out := new(GVKManifestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GVKManifestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GVKManifestSpec) DeepCopyInto(out *GVKManifestSpec) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]Group, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GVKManifestSpec. +func (in *GVKManifestSpec) DeepCopy() *GVKManifestSpec { + if in == nil { + return nil + } + out := new(GVKManifestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Group) DeepCopyInto(out *Group) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]Version, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. +func (in *Group) DeepCopy() *Group { + if in == nil { + return nil + } + out := new(Group) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Version) DeepCopyInto(out *Version) { + *out = *in + if in.Kinds != nil { + in, out := &in.Kinds, &out.Kinds + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Version. +func (in *Version) DeepCopy() *Version { + if in == nil { + return nil + } + out := new(Version) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go index 61c731832ef..bb664cf04a9 100644 --- a/cmd/gator/sync/sync.go +++ b/cmd/gator/sync/sync.go @@ -3,19 +3,19 @@ package sync import ( "fmt" - syncverify "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/verify" + synctest "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/sync/test" "github.com/spf13/cobra" ) var commands = []*cobra.Command{ - syncverify.Cmd, + synctest.Cmd, } var Cmd = &cobra.Command{ Use: "sync", Short: "Manage SyncSets and Config", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Usage: gator sync verify") + fmt.Println("Usage: gator sync test") }, } diff --git a/cmd/gator/sync/verify/verify.go b/cmd/gator/sync/test/test.go similarity index 59% rename from cmd/gator/sync/verify/verify.go rename to cmd/gator/sync/test/test.go index 3964c1b38c3..30009c39adc 100644 --- a/cmd/gator/sync/verify/verify.go +++ b/cmd/gator/sync/test/test.go @@ -1,4 +1,4 @@ -package verify +package test import ( "fmt" @@ -7,32 +7,33 @@ import ( cmdutils "github.com/open-policy-agent/gatekeeper/v3/cmd/gator/util" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" - "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/sync/verify" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/sync/test" "github.com/spf13/cobra" ) var Cmd = &cobra.Command{ - Use: "verify", - Short: "Verify that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", + Use: "test", + Short: "Test that the provided SyncSet(s) and/or Config contain the GVKs required by the input templates.", Run: run, } var ( - flagFilenames []string - flagImages []string - flagSupportedGVKs verify.SupportedGVKs + flagFilenames []string + flagImages []string + flagOmitGVKManifest bool ) const ( - flagNameFilename = "filename" - flagNameImage = "image" - flagNameSupportedGVKs = "supported-gvks" + flagNameFilename = "filename" + flagNameImage = "image" + flagNameForce = "omit-gvk-manifest" ) func init() { Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().VarP(&flagSupportedGVKs, flagNameSupportedGVKs, "s", "a json string listing the GVKs supported by the cluster as a nested array of groups, containing supported versions, each of which contains supported kinds. See https://open-policy-agent.github.io/gatekeeper/website/docs/gator#the-gator-sync-verify-subcommand for an example.") + Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "o", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ + "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") } func run(cmd *cobra.Command, args []string) { @@ -44,9 +45,9 @@ func run(cmd *cobra.Command, args []string) { cmdutils.ErrFatalf("no input data identified") } - missingRequirements, templateErrors, err := verify.Verify(unstrucs, flagSupportedGVKs) + missingRequirements, templateErrors, err := test.Test(unstrucs, flagOmitGVKManifest) if err != nil { - cmdutils.ErrFatalf("verifying: %v", err) + cmdutils.ErrFatalf("checking: %v", err) } if len(missingRequirements) > 0 { @@ -57,7 +58,6 @@ func run(cmd *cobra.Command, args []string) { cmdutils.ErrFatalf("encountered errors parsing the following templates: \n%v", resultsToString(templateErrors)) } - fmt.Println("all template requirements met") os.Exit(0) } diff --git a/pkg/gator/errors.go b/pkg/gator/errors.go index 5da533fabf6..674efe3401a 100644 --- a/pkg/gator/errors.go +++ b/pkg/gator/errors.go @@ -15,12 +15,17 @@ var ( // ErrNotASyncSet indicates the user-indicated file does not contain a // SyncSet. ErrNotASyncSet = errors.New("not a SyncSet") + // ErrNotASyncSet indicates the user-indicated file does not contain a + // SyncSet. + ErrNotAGVKManifest = errors.New("not a GVKManifest") // ErrAddingTemplate indicates a problem instantiating a Suite's ConstraintTemplate. ErrAddingTemplate = errors.New("adding template") // ErrAddingConstraint indicates a problem instantiating a Suite's Constraint. ErrAddingConstraint = errors.New("adding constraint") // ErrAddingSyncSet indicates a problem instantiating a Suite's SyncSet. ErrAddingSyncSet = errors.New("adding syncset") + // ErrAddingGVKManifest indicates a problem instantiating a Suite's GVKManifest. + ErrAddingGVKManifest = errors.New("adding gvkmanifest") // ErrAddingConfig indicates a problem instantiating a Suite's Config. ErrAddingConfig = errors.New("adding config") // ErrInvalidSuite indicates a Suite does not define the required fields. diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index b4e5eab1dc1..5a8d8ddbbe6 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -661,7 +661,6 @@ apiVersion: syncset.gatekeeper.sh/v1alpha1 kind: SyncSet metadata: name: syncset - namespace: "gatekeeper-system" spec: gvks: - group: "networking.k8s.io" @@ -676,7 +675,6 @@ apiVersion: config.gatekeeper.sh/v1alpha1 kind: Config metadata: name: config - namespace: "gatekeeper-system" spec: sync: syncOnly: @@ -686,5 +684,17 @@ spec: - group: "apps" version: "v1" kind: "Deployment" +` + GVKManifest = ` +apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 +kind: GVKManifest +metadata: + name: gvkmanifest +spec: + groups: + - name: "" + versions: + - name: "v1" + kinds: ["Service"] ` ) diff --git a/pkg/gator/reader/read_resources.go b/pkg/gator/reader/read_resources.go index d0c9e9457ed..d9543a88d9e 100644 --- a/pkg/gator/reader/read_resources.go +++ b/pkg/gator/reader/read_resources.go @@ -11,6 +11,7 @@ import ( templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" configv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/config/v1alpha1" + gvkmanifestv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" syncsetv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/syncset/v1alpha1" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -192,6 +193,25 @@ func ToConfig(scheme *runtime.Scheme, u *unstructured.Unstructured) (*configv1al return config, nil } +// ToGVKManifest converts an unstructured GVKManifest into a GVKManifest struct. +func ToGVKManifest(scheme *runtime.Scheme, u *unstructured.Unstructured) (*gvkmanifestv1alpha1.GVKManifest, error) { + if u.GroupVersionKind().Group != gvkmanifestv1alpha1.GroupVersion.Group || u.GroupVersionKind().Kind != "GVKManifest" { + return nil, fmt.Errorf("%w", gator.ErrNotAGVKManifest) + } + + s, err := ToStructured(scheme, u) + if err != nil { + return nil, fmt.Errorf("%w: %w", gator.ErrAddingGVKManifest, err) + } + + gvkManifest, isGVKManifest := s.(*gvkmanifestv1alpha1.GVKManifest) + if !isGVKManifest { + return nil, fmt.Errorf("%w: %T", gator.ErrAddingGVKManifest, gvkManifest) + } + + return gvkManifest, nil +} + // ReadObject reads a file from the filesystem abstraction at the specified // path, and returns an unstructured.Unstructured object if the file can be // successfully unmarshalled. @@ -261,12 +281,17 @@ func IsTemplate(u *unstructured.Unstructured) bool { func IsConfig(u *unstructured.Unstructured) bool { gvk := u.GroupVersionKind() - return gvk.Group == "config.gatekeeper.sh" && gvk.Kind == "Config" + return gvk.Group == configv1alpha1.GroupVersion.Group && gvk.Kind == "Config" } func IsSyncSet(u *unstructured.Unstructured) bool { gvk := u.GroupVersionKind() - return gvk.Group == "syncset.gatekeeper.sh" && gvk.Kind == "SyncSet" + return gvk.Group == syncsetv1alpha1.GroupVersion.Group && gvk.Kind == "SyncSet" +} + +func IsGVKManifest(u *unstructured.Unstructured) bool { + gvk := u.GroupVersionKind() + return gvk.Group == gvkmanifestv1alpha1.GroupVersion.Group && gvk.Kind == "GVKManifest" } func IsConstraint(u *unstructured.Unstructured) bool { diff --git a/pkg/gator/sync/verify/verify.go b/pkg/gator/sync/test/test.go similarity index 53% rename from pkg/gator/sync/verify/verify.go rename to pkg/gator/sync/test/test.go index 73f55b7aa6f..d698a4db3e6 100644 --- a/pkg/gator/sync/verify/verify.go +++ b/pkg/gator/sync/test/test.go @@ -1,12 +1,13 @@ -package verify +package test import ( - "encoding/json" "fmt" cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" gkapis "github.com/open-policy-agent/gatekeeper/v3/apis" + + gvkmanifestv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -14,39 +15,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -type SupportedGVKs map[schema.GroupVersionKind]struct{} - -func (s *SupportedGVKs) String() string { - return fmt.Sprintf("%v", *s) -} - -func (s *SupportedGVKs) Set(value string) error { - if value == "" { - return nil - } - var stringAsJSON map[string]map[string][]string - if err := json.Unmarshal([]byte(value), &stringAsJSON); err != nil { - return err - } - *s = SupportedGVKs{} - for group, versions := range stringAsJSON { - for version, kinds := range versions { - for _, kind := range kinds { - (*s)[schema.GroupVersionKind{ - Group: group, - Version: version, - Kind: kind, - }] = struct{}{} - } - } - } - return nil -} - -func (s *SupportedGVKs) Type() string { - return "SupportedGVKs" -} - var scheme *runtime.Scheme func init() { @@ -63,11 +31,13 @@ func init() { // Reads a list of unstructured objects and a string containing supported GVKs and // outputs a set of missing sync requirements per template and ingestion problems per template. -func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) (map[string]parser.SyncRequirements, map[string]error, error) { - templates := []*templates.ConstraintTemplate{} +func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[string]parser.SyncRequirements, map[string]error, error) { + templates := map[*templates.ConstraintTemplate]parser.SyncRequirements{} syncedGVKs := map[schema.GroupVersionKind]struct{}{} templateErrs := map[string]error{} hasConfig := false + var gvkManifest *gvkmanifestv1alpha1.GVKManifest + var err error for _, obj := range unstrucs { if reader.IsSyncSet(obj) { @@ -81,13 +51,11 @@ func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) Version: gvkEntry.Version, Kind: gvkEntry.Kind, } - if _, exists := supportedGVKs[gvk]; exists || supportedGVKs == nil { - syncedGVKs[gvk] = struct{}{} - } + syncedGVKs[gvk] = struct{}{} } } else if reader.IsConfig(obj) { if hasConfig { - return nil, nil, fmt.Errorf("multiple configs found. Config is a singleton resource") + return nil, nil, fmt.Errorf("multiple configs found; Config is a singleton resource") } config, err := reader.ToConfig(scheme, obj) if err != nil { @@ -100,9 +68,7 @@ func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) Version: syncOnlyEntry.Version, Kind: syncOnlyEntry.Kind, } - if _, exists := supportedGVKs[gvk]; exists || supportedGVKs == nil { - syncedGVKs[gvk] = struct{}{} - } + syncedGVKs[gvk] = struct{}{} } } else if reader.IsTemplate(obj) { templ, err := reader.ToTemplate(scheme, obj) @@ -110,22 +76,63 @@ func Verify(unstrucs []*unstructured.Unstructured, supportedGVKs SupportedGVKs) templateErrs[obj.GetName()] = err continue } - templates = append(templates, templ) + syncRequirements, err := parser.ReadSyncRequirements(templ) + if err != nil { + templateErrs[templ.GetName()] = err + continue + } + templates[templ] = syncRequirements + } else if reader.IsGVKManifest(obj) { + if gvkManifest == nil { + gvkManifest, err = reader.ToGVKManifest(scheme, obj) + if err != nil { + return nil, nil, fmt.Errorf("converting unstructured %q to gvkmanifest: %w", obj.GetName(), err) + } + } else { + return nil, nil, fmt.Errorf("multiple GVK manifests found; please provide one manifest enumerating the GVKs supported by the cluster") + } } else { - fmt.Printf("skipping unstructured %q because it is not a syncset, config, or template\n", obj.GetName()) + fmt.Printf("skipping unstructured %q because it is not a syncset, config, gvk manifest, or template\n", obj.GetName()) + } + } + + // Don't assess requirement fulfillment if there was an error parsing any of the templates. + if len(templateErrs) != 0 { + return nil, templateErrs, nil + } + + // Crosscheck synced gvks with supported gvks. + if gvkManifest == nil { + if !omitGVKManifest { + return nil, nil, fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster") + } + fmt.Print("ignoring absence of supported GVK manifest due to --omit-gvk-manifest flag; will assume all synced GVKs are supported by cluster") + } else { + supportedGVKs := map[schema.GroupVersionKind]struct{}{} + for _, group := range gvkManifest.Spec.Groups { + for _, version := range group.Versions { + for _, kind := range version.Kinds { + gvk := schema.GroupVersionKind{ + Group: group.Name, + Version: version.Name, + Kind: kind, + } + supportedGVKs[gvk] = struct{}{} + } + } + } + for gvk := range syncedGVKs { + if _, exists := supportedGVKs[gvk]; !exists { + delete(syncedGVKs, gvk) + } } } missingReqs := map[string]parser.SyncRequirements{} - for _, templ := range templates { + for templ, reqs := range templates { // Fetch syncrequirements from template - syncRequirements, err := parser.ReadSyncRequirements(templ) - if err != nil { - templateErrs[templ.GetName()] = err - continue - } - for _, requirement := range syncRequirements { + for _, requirement := range reqs { requirementMet := false for gvk := range requirement { if _, exists := syncedGVKs[gvk]; exists { diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go new file mode 100644 index 00000000000..74f7ef96ba0 --- /dev/null +++ b/pkg/gator/sync/test/test_test.go @@ -0,0 +1,205 @@ +package test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" + "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestTest(t *testing.T) { + tcs := []struct { + name string + inputs []string + omitManifest bool + wantReqs map[string]parser.SyncRequirements + wantErrs map[string]error + err error + }{ + // { + // name: "basic req unfulfilled", + // inputs: []string{ + // fixtures.TemplateReferential, + // }, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{ + // "k8suniqueserviceselector": { + // parser.GVKEquivalenceSet{ + // { + // Group: "", + // Version: "v1", + // Kind: "Service", + // }: struct{}{}, + // }, + // }, + // }, + // wantErrs: map[string]error{}, + // }, + // { + // name: "one template having error stops requirement evaluation", + // inputs: []string{ + // fixtures.TemplateReferential, + // fixtures.TemplateReferentialBadAnnotation, + // }, + // omitManifest: true, + // wantReqs: nil, + // wantErrs: map[string]error{ + // "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), + // }, + // }, + // { + // name: "basic req fulfilled by syncset", + // inputs: []string{ + // fixtures.TemplateReferential, + // fixtures.Config, + // }, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{}, + // wantErrs: map[string]error{}, + // }, + { + name: "basic req fulfilled by syncset and supported by cluster", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + fixtures.GVKManifest, + }, + wantReqs: map[string]parser.SyncRequirements{}, + wantErrs: map[string]error{}, + }, + // { + // name: "basic req fulfilled by syncset but not supported by cluster", + // inputs: []string{ + // fixtures.TemplateReferential, + // fixtures.Config, + // fixtures.GVKManifest, + // }, + // wantReqs: map[string]parser.SyncRequirements{ + // "k8suniqueserviceselector": { + // parser.GVKEquivalenceSet{ + // { + // Group: "", + // Version: "v1", + // Kind: "Service", + // }: struct{}{}, + // }, + // }, + // }, + // wantErrs: map[string]error{}, + // }, + // { + // name: "multi equivalentset req fulfilled by syncset", + // inputs: []string{ + // fixtures.TemplateReferentialMultEquivSets, + // fixtures.SyncSet, + // }, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{}, + // wantErrs: map[string]error{}, + // }, + // { + // name: "multi requirement, one req fulfilled by syncset", + // inputs: []string{ + // fixtures.TemplateReferentialMultReqs, + // fixtures.SyncSet, + // }, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{ + // "k8suniqueingresshostmultireq": { + // parser.GVKEquivalenceSet{ + // { + // Group: "", + // Version: "v1", + // Kind: "Pod", + // }: struct{}{}, + // }, + // }, + // }, + // wantErrs: map[string]error{}, + // }, + // { + // name: "multiple templates, syncset and config", + // inputs: []string{ + // fixtures.TemplateReferential, + // fixtures.TemplateReferentialMultEquivSets, + // fixtures.TemplateReferentialMultReqs, + // fixtures.Config, + // fixtures.SyncSet, + // }, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{ + // "k8suniqueingresshostmultireq": { + // parser.GVKEquivalenceSet{ + // { + // Group: "", + // Version: "v1", + // Kind: "Pod", + // }: struct{}{}, + // }, + // }, + // }, + // wantErrs: map[string]error{}, + // }, + // { + // name: "no data of any kind", + // inputs: []string{}, + // omitManifest: true, + // wantReqs: map[string]parser.SyncRequirements{}, + // wantErrs: map[string]error{}, + // }, + // { + // name: "error if manifest not provided and omitGVKManifest not set", + // inputs: []string{ + // fixtures.TemplateReferential, + // fixtures.Config, + // }, + // wantReqs: map[string]parser.SyncRequirements{}, + // wantErrs: map[string]error{}, + // err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), + // }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + // convert the test resources to unstructureds + var objs []*unstructured.Unstructured + for _, input := range tc.inputs { + u, err := reader.ReadUnstructured([]byte(input)) + require.NoError(t, err) + objs = append(objs, u) + } + + gotReqs, gotErrs, err := Test(objs, tc.omitManifest) + if tc.err != nil { + if tc.err.Error() != err.Error() { + t.Errorf("error mismatch: want %v, got %v", tc.err, err) + } + } else if err != nil { + require.NoError(t, err) + } + + if diff := cmp.Diff(tc.wantReqs, gotReqs); diff != "" { + t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) + } + + for key, wantErr := range tc.wantErrs { + if gotErr, ok := gotErrs[key]; ok { + if wantErr.Error() != gotErr.Error() { + t.Errorf("error mismatch for %s: want %v, got %v", key, wantErr, gotErr) + } + } else { + t.Errorf("missing error for %s", key) + } + } + for key, gotErr := range gotErrs { + if _, ok := tc.wantErrs[key]; !ok { + t.Errorf("unexpected error for %s: %v", key, gotErr) + } + } + }) + } +} diff --git a/pkg/gator/sync/verify/verify_test.go b/pkg/gator/sync/verify/verify_test.go deleted file mode 100644 index a4b0e349874..00000000000 --- a/pkg/gator/sync/verify/verify_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package verify - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" - "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" - "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestVerify(t *testing.T) { - tcs := []struct { - name string - inputs []string - supportedGVKs SupportedGVKs - wantReqs map[string]parser.SyncRequirements - wantErrs map[string]error - err error - }{ - { - name: "basic req unfulfilled", - inputs: []string{ - fixtures.TemplateReferential, - }, - wantReqs: map[string]parser.SyncRequirements{ - "k8suniqueserviceselector": { - parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, - }, - }, - }, - wantErrs: map[string]error{}, - }, - { - name: "one template has error, one has basic req unfulfilled", - inputs: []string{ - fixtures.TemplateReferential, - fixtures.TemplateReferentialBadAnnotation, - }, - wantReqs: map[string]parser.SyncRequirements{ - "k8suniqueserviceselector": { - parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, - }, - }, - }, - wantErrs: map[string]error{ - "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), - }, - }, - { - name: "basic req fulfilled by syncset", - inputs: []string{ - fixtures.TemplateReferential, - fixtures.Config, - }, - wantReqs: map[string]parser.SyncRequirements{}, - wantErrs: map[string]error{}, - }, - { - name: "basic req fulfilled by syncset and discoveryresults", - inputs: []string{ - fixtures.TemplateReferential, - fixtures.Config, - }, - supportedGVKs: SupportedGVKs{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, - }, - wantReqs: map[string]parser.SyncRequirements{}, - wantErrs: map[string]error{}, - }, - { - name: "basic req fulfilled by syncset but not discoveryresults", - inputs: []string{ - fixtures.TemplateReferential, - fixtures.Config, - }, - supportedGVKs: SupportedGVKs{ - { - Group: "extensions", - Version: "v1beta1", - Kind: "Ingress", - }: struct{}{}, - }, wantReqs: map[string]parser.SyncRequirements{ - "k8suniqueserviceselector": { - parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, - }, - }, - }, - wantErrs: map[string]error{}, - }, - { - name: "multi equivalentset req fulfilled by syncset", - inputs: []string{ - fixtures.TemplateReferentialMultEquivSets, - fixtures.SyncSet, - }, - wantReqs: map[string]parser.SyncRequirements{}, - wantErrs: map[string]error{}, - }, - { - name: "multi requirement, one req fulfilled by syncset", - inputs: []string{ - fixtures.TemplateReferentialMultReqs, - fixtures.SyncSet, - }, - wantReqs: map[string]parser.SyncRequirements{ - "k8suniqueingresshostmultireq": { - parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Pod", - }: struct{}{}, - }, - }, - }, - wantErrs: map[string]error{}, - }, - { - name: "multiple templates, syncset and config", - inputs: []string{ - fixtures.TemplateReferential, - fixtures.TemplateReferentialMultEquivSets, - fixtures.TemplateReferentialMultReqs, - fixtures.Config, - fixtures.SyncSet, - }, - wantReqs: map[string]parser.SyncRequirements{ - "k8suniqueingresshostmultireq": { - parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Pod", - }: struct{}{}, - }, - }, - }, - wantErrs: map[string]error{}, - }, - { - name: "no data of any kind", - inputs: []string{}, - wantReqs: map[string]parser.SyncRequirements{}, - wantErrs: map[string]error{}, - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - // convert the test resources to unstructureds - var objs []*unstructured.Unstructured - for _, input := range tc.inputs { - u, err := reader.ReadUnstructured([]byte(input)) - require.NoError(t, err) - objs = append(objs, u) - } - - gotReqs, gotErrs, err := Verify(objs, tc.supportedGVKs) - if tc.err != nil { - require.ErrorIs(t, err, tc.err) - } else if err != nil { - require.NoError(t, err) - } - - if diff := cmp.Diff(tc.wantReqs, gotReqs); diff != "" { - t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) - } - - for key, wantErr := range tc.wantErrs { - if gotErr, ok := gotErrs[key]; ok { - if wantErr.Error() != gotErr.Error() { - t.Errorf("error mismatch for %s: want %v, got %v", key, wantErr, gotErr) - } - } else { - t.Errorf("missing error for %s", key) - } - } - for key, gotErr := range gotErrs { - if _, ok := tc.wantErrs[key]; !ok { - t.Errorf("unexpected error for %s: %v", key, gotErr) - } - } - }) - } -} diff --git a/website/docs/gator.md b/website/docs/gator.md index 11a81ab8ac6..9cf2148e729 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -495,7 +495,7 @@ However, not including the `namespace` definition in the call to `gator expand` error expanding resources: error expanding resource nginx-deployment: failed to mutate resultant resource nginx-deployment-pod: matching for mutator Assign.mutations.gatekeeper.sh /always-pull-image failed for Pod my-ns nginx-deployment-pod: failed to run Match criteria: namespace selector for namespace-scoped object but missing Namespace ``` -## The `gator sync verify` subcommand +## The `gator sync test` subcommand Certain templates require [replicating data](sync.md) into OPA to enable correct evaluation. These templates can use the annotation `metadata.gatekeeper.sh/requires-sync-data` to indicate which resources need to be synced. The annotation contains a json object representing a list of requirements, each of which contains a list of one or more GVK clauses forming an equivalence set of interchangeable GVKs. Each of these clauses has `groups`, `versions`, and `kinds` fields; any group-version-kind combination within a clause within a requirement should be considered sufficient to satisfy that requirement. For example (comments added for clarity): ``` @@ -527,40 +527,49 @@ Requirement 2 is simpler: it denotes that group5, version5, kind5 must be synced This template annotation is descriptive, not prescriptive. The prescription of which resources to sync is done in `SyncSet` resources and/or the Gatekeeper `Config` resource. The management of these various requirements can get challenging as the number of templates requiring replicated data increases. -`gator sync verify` aims to mitigate this challenge by enabling the user to verify their sync configuration is correct. The user passes in a set of Constraint Templates, SyncSets, and/or a Gatekeeper Config, and the command will determine which requirements enumerated by the Constraint Templates are unfulfilled by the given SyncSet(s) and Config(s). +`gator sync test` aims to mitigate this challenge by enabling the user to check that their sync configuration is correct. The user passes in a set of Constraint Templates, GVK Manifest listing GVKs supported by the cluster, SyncSets, and/or a Gatekeeper Config, and the command will determine which requirements enumerated by the Constraint Templates are unfulfilled by the cluster and SyncSet(s)/Config. ### Usage #### Specifying Inputs -`gator sync verify` expects a `--filename` or `--image` flag, or input fron stdin. The flags can be used individually, in combination, and/or repeated. +`gator sync test` expects a `--filename` or `--image` flag, or input fron stdin. The flags can be used individually, in combination, and/or repeated. ``` -gator sync verify --filename="template.yaml" –filename="syncsets/" +gator sync test --filename="template.yaml" –-filename="syncsets/" --filename="manifest.yaml" ``` Or, using an OCI Artifact containing templates as described previously: ``` -gator sync verify --filename="config.yaml" --image=localhost:5000/gator/template-library:v1 +gator sync test --filename="config.yaml" --image=localhost:5000/gator/template-library:v1 ``` -Optionally, the `--supported-gvks` flag can be used to pass in a list of supported GVKs for the applicable cluster. If this is nonempty, only GVKs that are both included in a SyncSet/Config and are supported will be considered to fulfill a template's requirements. Supported GVKs should be passed as a string representing a JSON object mapping groups to lists of versions, which contain lists of kinds, like such: +The manifest of GVKs supported by the cluster should be passed as a GVKManifest resource (CRD visible under the apis directory in the repo): ``` -{ - "group1": { - "version1": ["kind1", "kind2"], - "version2": ["kind3", "kind4"], - }, - "group2": { - "version3": ["kind3"] - } -} +apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 +kind: GVKManifest +metadata: + name: gvkmanifest +spec: + groups: + - name: "group1" + versions: + - name: "v1" + kinds: ["Kind1", "Kind2"] + - name: "v2" + kinds: ["Kind1", "Kind3"] + - name: "group2" + versions: + - name: "v1beta1" + kinds: ["Kind4", "Kind5"] ``` +Optionally, the `--omit-gvk-manifest` flag can be used to skip the requirement of providing a manifest of supported GVKs for the cluster. If this is provided, all GVKs will be assumed to be supported by the cluster. If this assumption is not true, then the given config and templates may cause caching errors or incorrect evaluation on the cluster despite passing this command. + #### Exit Codes -`gator sync verify` will return a `0` exit status when the Templates, SyncSets, and +`gator sync test` will return a `0` exit status when the Templates, SyncSets, and Config are successfully ingested and no requirements are unfulfilled. An error during evaluation, for example a failure to read a file, will result in From 868b1440c23f44d3a602c8fc22bea5f944c9fb63 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Tue, 9 Jan 2024 16:52:55 -0800 Subject: [PATCH 13/24] comments Signed-off-by: Anlan Du --- .../gvkmanifest/v1alpha1/gvkmanifest_types.go | 9 +- .../v1alpha1/zz_generated.deepcopy.go | 74 ++++- cmd/gator/sync/test/test.go | 4 +- pkg/gator/errors.go | 6 +- pkg/gator/fixtures/fixtures.go | 74 ++--- pkg/gator/sync/test/test.go | 14 +- pkg/gator/sync/test/test_test.go | 303 ++++++++++-------- 7 files changed, 265 insertions(+), 219 deletions(-) diff --git a/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go b/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go index b1eddabca8b..8b7d8a4ce3b 100644 --- a/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go +++ b/apis/gvkmanifest/v1alpha1/gvkmanifest_types.go @@ -5,13 +5,12 @@ import ( ) type GVKManifestSpec struct { - Groups []Group `json:"groups,omitempty"` + Groups map[string]Versions `json:"groups,omitempty"` } -type Group struct { - Name string `json:"name,omitempty"` - Versions []Version `json:"versions,omitempty"` -} +type Versions map[string]Kinds + +type Kinds []string type Version struct { Name string `json:"name,omitempty"` diff --git a/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go index a8b17c00767..c913af095b9 100644 --- a/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go +++ b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go @@ -87,9 +87,27 @@ func (in *GVKManifestSpec) DeepCopyInto(out *GVKManifestSpec) { *out = *in if in.Groups != nil { in, out := &in.Groups, &out.Groups - *out = make([]Group, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) + *out = make(map[string]Versions, len(*in)) + for key, val := range *in { + var outVal map[string]Kinds + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(Versions, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(Kinds, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + (*out)[key] = outVal } } } @@ -105,25 +123,22 @@ func (in *GVKManifestSpec) DeepCopy() *GVKManifestSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Group) DeepCopyInto(out *Group) { - *out = *in - if in.Versions != nil { - in, out := &in.Versions, &out.Versions - *out = make([]Version, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } +func (in Kinds) DeepCopyInto(out *Kinds) { + { + in := &in + *out = make(Kinds, len(*in)) + copy(*out, *in) } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. -func (in *Group) DeepCopy() *Group { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kinds. +func (in Kinds) DeepCopy() Kinds { if in == nil { return nil } - out := new(Group) + out := new(Kinds) in.DeepCopyInto(out) - return out + return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -145,3 +160,32 @@ func (in *Version) DeepCopy() *Version { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Versions) DeepCopyInto(out *Versions) { + { + in := &in + *out = make(Versions, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(Kinds, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Versions. +func (in Versions) DeepCopy() Versions { + if in == nil { + return nil + } + out := new(Versions) + in.DeepCopyInto(out) + return *out +} diff --git a/cmd/gator/sync/test/test.go b/cmd/gator/sync/test/test.go index 30009c39adc..d73d1b653c1 100644 --- a/cmd/gator/sync/test/test.go +++ b/cmd/gator/sync/test/test.go @@ -26,13 +26,13 @@ var ( const ( flagNameFilename = "filename" flagNameImage = "image" - flagNameForce = "omit-gvk-manifest" + flagNameForce = "force-omit-gvk-manifest" ) func init() { Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "o", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ + Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "f", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") } diff --git a/pkg/gator/errors.go b/pkg/gator/errors.go index 674efe3401a..11d828b8c2a 100644 --- a/pkg/gator/errors.go +++ b/pkg/gator/errors.go @@ -22,11 +22,11 @@ var ( ErrAddingTemplate = errors.New("adding template") // ErrAddingConstraint indicates a problem instantiating a Suite's Constraint. ErrAddingConstraint = errors.New("adding constraint") - // ErrAddingSyncSet indicates a problem instantiating a Suite's SyncSet. + // ErrAddingSyncSet indicates a problem instantiating a user-indicated SyncSet. ErrAddingSyncSet = errors.New("adding syncset") - // ErrAddingGVKManifest indicates a problem instantiating a Suite's GVKManifest. + // ErrAddingGVKManifest indicates a problem instantiating a user-indicated GVKManifest. ErrAddingGVKManifest = errors.New("adding gvkmanifest") - // ErrAddingConfig indicates a problem instantiating a Suite's Config. + // ErrAddingConfig indicates a problem instantiating a user-indicated Config. ErrAddingConfig = errors.New("adding config") // ErrInvalidSuite indicates a Suite does not define the required fields. ErrInvalidSuite = errors.New("invalid Suite") diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 5a8d8ddbbe6..7cca872fcbe 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -336,46 +336,6 @@ metadata: } ] ]" -spec: - crd: - spec: - names: - kind: K8sUniqueServiceSelector - targets: - - target: admission.k8s.gatekeeper.sh - rego: | - package k8suniqueserviceselector - make_apiversion(kind) = apiVersion { - g := kind.group - v := kind.version - g != "" - apiVersion = sprintf("%v/%v", [g, v]) - } - make_apiversion(kind) = apiVersion { - kind.group == "" - apiVersion = kind.version - } - identical(obj, review) { - obj.metadata.namespace == review.namespace - obj.metadata.name == review.name - obj.kind == review.kind.kind - obj.apiVersion == make_apiversion(review.kind) - } - flatten_selector(obj) = flattened { - selectors := [s | s = concat(":", [key, val]); val = obj.spec.selector[key]] - flattened := concat(",", sort(selectors)) - } - violation[{"msg": msg}] { - input.review.kind.kind == "Service" - input.review.kind.version == "v1" - input.review.kind.group == "" - input_selector := flatten_selector(input.review.object) - other := data.inventory.namespace[namespace][_]["Service"][name] - not identical(other, input.review) - other_selector := flatten_selector(other) - input_selector == other_selector - msg := sprintf("same selector as service <%v> in namespace <%v>", [name, namespace]) - } ` TemplateReferentialMultEquivSets = ` @@ -669,6 +629,14 @@ spec: - group: "apps" version: "v1" kind: "Deployment" +` + BadSyncSet = ` +apiVersion: syncset.gatekeeper.sh/v1alpha1 +kind: SyncSet +metadata: + name: syncset +spec: + unknownField: "unknown" ` Config = ` apiVersion: config.gatekeeper.sh/v1alpha1 @@ -685,16 +653,32 @@ spec: version: "v1" kind: "Deployment" ` - GVKManifest = ` + BadConfig = ` +apiVersion: config.gatekeeper.sh/v1alpha1 +kind: Config +metadata: + name: config +spec: + unknownField: "unknown" +` + GVKManifestServices = ` +apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 +kind: GVKManifest +metadata: + name: gvkmanifest +spec: + groups: + "": + "v1": ["Service"] +` + GVKManifestDeployments = ` apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 kind: GVKManifest metadata: name: gvkmanifest spec: groups: - - name: "" - versions: - - name: "v1" - kinds: ["Service"] + apps: + "v1": ["Deployment"] ` ) diff --git a/pkg/gator/sync/test/test.go b/pkg/gator/sync/test/test.go index d698a4db3e6..0eef8aca9ba 100644 --- a/pkg/gator/sync/test/test.go +++ b/pkg/gator/sync/test/test.go @@ -106,15 +106,15 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri if !omitGVKManifest { return nil, nil, fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster") } - fmt.Print("ignoring absence of supported GVK manifest due to --omit-gvk-manifest flag; will assume all synced GVKs are supported by cluster") + fmt.Print("ignoring absence of supported GVK manifest due to --force-omit-gvk-manifest flag; will assume all synced GVKs are supported by cluster\n") } else { supportedGVKs := map[schema.GroupVersionKind]struct{}{} - for _, group := range gvkManifest.Spec.Groups { - for _, version := range group.Versions { - for _, kind := range version.Kinds { + for group, versions := range gvkManifest.Spec.Groups { + for version, kinds := range versions { + for _, kind := range kinds { gvk := schema.GroupVersionKind{ - Group: group.Name, - Version: version.Name, + Group: group, + Version: version, Kind: kind, } supportedGVKs[gvk] = struct{}{} @@ -144,5 +144,5 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } } } - return missingReqs, templateErrs, nil + return missingReqs, nil, nil } diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go index 74f7ef96ba0..6f441067672 100644 --- a/pkg/gator/sync/test/test_test.go +++ b/pkg/gator/sync/test/test_test.go @@ -1,6 +1,7 @@ package test import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -17,152 +18,174 @@ func TestTest(t *testing.T) { inputs []string omitManifest bool wantReqs map[string]parser.SyncRequirements + }{ + { + name: "basic req unfulfilled", + inputs: []string{ + fixtures.TemplateReferential, + }, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{ + "k8suniqueserviceselector": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + }, + }, + }, + { + name: "basic req fulfilled by config", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + }, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{}, + }, + { + name: "basic req fulfilled by config and supported by cluster", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + fixtures.GVKManifestServices, + }, + wantReqs: map[string]parser.SyncRequirements{}, + }, + { + name: "basic req fulfilled by config but not supported by cluster", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.Config, + fixtures.GVKManifestDeployments, + }, + wantReqs: map[string]parser.SyncRequirements{ + "k8suniqueserviceselector": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Service", + }: struct{}{}, + }, + }, + }, + }, + { + name: "multi equivalentset req fulfilled by syncset", + inputs: []string{ + fixtures.TemplateReferentialMultEquivSets, + fixtures.SyncSet, + }, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{}, + }, + { + name: "multi requirement, one req fulfilled by syncset", + inputs: []string{ + fixtures.TemplateReferentialMultReqs, + fixtures.SyncSet, + }, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{ + "k8suniqueingresshostmultireq": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Pod", + }: struct{}{}, + }, + }, + }, + }, + { + name: "multiple templates, syncset and config", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.TemplateReferentialMultEquivSets, + fixtures.TemplateReferentialMultReqs, + fixtures.Config, + fixtures.SyncSet, + }, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{ + "k8suniqueingresshostmultireq": { + parser.GVKEquivalenceSet{ + { + Group: "", + Version: "v1", + Kind: "Pod", + }: struct{}{}, + }, + }, + }, + }, + { + name: "no data of any kind", + inputs: []string{}, + omitManifest: true, + wantReqs: map[string]parser.SyncRequirements{}, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + // convert the test resources to unstructureds + var objs []*unstructured.Unstructured + for _, input := range tc.inputs { + u, err := reader.ReadUnstructured([]byte(input)) + require.NoError(t, err) + objs = append(objs, u) + } + + gotReqs, gotErrs, err := Test(objs, tc.omitManifest) + + require.NoError(t, err) + + if gotErrs != nil { + t.Errorf("got unexpected errors: %v", gotErrs) + } + + if diff := cmp.Diff(tc.wantReqs, gotReqs); diff != "" { + t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) + } + }) + } +} + +func TestTest_Errors(t *testing.T) { + tcs := []struct { + name string + inputs []string + omitManifest bool wantErrs map[string]error err error }{ - // { - // name: "basic req unfulfilled", - // inputs: []string{ - // fixtures.TemplateReferential, - // }, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{ - // "k8suniqueserviceselector": { - // parser.GVKEquivalenceSet{ - // { - // Group: "", - // Version: "v1", - // Kind: "Service", - // }: struct{}{}, - // }, - // }, - // }, - // wantErrs: map[string]error{}, - // }, - // { - // name: "one template having error stops requirement evaluation", - // inputs: []string{ - // fixtures.TemplateReferential, - // fixtures.TemplateReferentialBadAnnotation, - // }, - // omitManifest: true, - // wantReqs: nil, - // wantErrs: map[string]error{ - // "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), - // }, - // }, - // { - // name: "basic req fulfilled by syncset", - // inputs: []string{ - // fixtures.TemplateReferential, - // fixtures.Config, - // }, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{}, - // wantErrs: map[string]error{}, - // }, { - name: "basic req fulfilled by syncset and supported by cluster", + name: "one template having error stops requirement evaluation", + inputs: []string{ + fixtures.TemplateReferential, + fixtures.TemplateReferentialBadAnnotation, + }, + omitManifest: true, + wantErrs: map[string]error{ + "k8suniqueingresshostbadannotation": fmt.Errorf("json: cannot unmarshal object into Go value of type parser.CompactSyncRequirements"), + }, + }, + { + name: "error if manifest not provided and omitGVKManifest not set", inputs: []string{ fixtures.TemplateReferential, fixtures.Config, - fixtures.GVKManifest, }, - wantReqs: map[string]parser.SyncRequirements{}, wantErrs: map[string]error{}, + err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), }, - // { - // name: "basic req fulfilled by syncset but not supported by cluster", - // inputs: []string{ - // fixtures.TemplateReferential, - // fixtures.Config, - // fixtures.GVKManifest, - // }, - // wantReqs: map[string]parser.SyncRequirements{ - // "k8suniqueserviceselector": { - // parser.GVKEquivalenceSet{ - // { - // Group: "", - // Version: "v1", - // Kind: "Service", - // }: struct{}{}, - // }, - // }, - // }, - // wantErrs: map[string]error{}, - // }, - // { - // name: "multi equivalentset req fulfilled by syncset", - // inputs: []string{ - // fixtures.TemplateReferentialMultEquivSets, - // fixtures.SyncSet, - // }, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{}, - // wantErrs: map[string]error{}, - // }, - // { - // name: "multi requirement, one req fulfilled by syncset", - // inputs: []string{ - // fixtures.TemplateReferentialMultReqs, - // fixtures.SyncSet, - // }, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{ - // "k8suniqueingresshostmultireq": { - // parser.GVKEquivalenceSet{ - // { - // Group: "", - // Version: "v1", - // Kind: "Pod", - // }: struct{}{}, - // }, - // }, - // }, - // wantErrs: map[string]error{}, - // }, - // { - // name: "multiple templates, syncset and config", - // inputs: []string{ - // fixtures.TemplateReferential, - // fixtures.TemplateReferentialMultEquivSets, - // fixtures.TemplateReferentialMultReqs, - // fixtures.Config, - // fixtures.SyncSet, - // }, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{ - // "k8suniqueingresshostmultireq": { - // parser.GVKEquivalenceSet{ - // { - // Group: "", - // Version: "v1", - // Kind: "Pod", - // }: struct{}{}, - // }, - // }, - // }, - // wantErrs: map[string]error{}, - // }, - // { - // name: "no data of any kind", - // inputs: []string{}, - // omitManifest: true, - // wantReqs: map[string]parser.SyncRequirements{}, - // wantErrs: map[string]error{}, - // }, - // { - // name: "error if manifest not provided and omitGVKManifest not set", - // inputs: []string{ - // fixtures.TemplateReferential, - // fixtures.Config, - // }, - // wantReqs: map[string]parser.SyncRequirements{}, - // wantErrs: map[string]error{}, - // err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), - // }, } - for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { // convert the test resources to unstructureds @@ -174,6 +197,7 @@ func TestTest(t *testing.T) { } gotReqs, gotErrs, err := Test(objs, tc.omitManifest) + if tc.err != nil { if tc.err.Error() != err.Error() { t.Errorf("error mismatch: want %v, got %v", tc.err, err) @@ -182,8 +206,8 @@ func TestTest(t *testing.T) { require.NoError(t, err) } - if diff := cmp.Diff(tc.wantReqs, gotReqs); diff != "" { - t.Errorf("diff in missingRequirements objects (-want +got):\n%s", diff) + if gotReqs != nil { + t.Errorf("got unexpected requirements: %v", gotReqs) } for key, wantErr := range tc.wantErrs { @@ -195,11 +219,6 @@ func TestTest(t *testing.T) { t.Errorf("missing error for %s", key) } } - for key, gotErr := range gotErrs { - if _, ok := tc.wantErrs[key]; !ok { - t.Errorf("unexpected error for %s: %v", key, gotErr) - } - } }) } } From 60697cacc24ef25e95da90b5a524ef44d1bed62c Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Tue, 9 Jan 2024 20:16:55 -0800 Subject: [PATCH 14/24] comments Signed-off-by: Anlan Du --- cmd/gator/sync/test/test.go | 2 +- pkg/fakes/sync.go | 28 +++++++ pkg/gator/fixtures/fixtures.go | 29 ------- pkg/gator/sync/test/test_test.go | 130 ++++++++++++++++++++++++++++--- 4 files changed, 148 insertions(+), 41 deletions(-) diff --git a/cmd/gator/sync/test/test.go b/cmd/gator/sync/test/test.go index d73d1b653c1..e2f019f016d 100644 --- a/cmd/gator/sync/test/test.go +++ b/cmd/gator/sync/test/test.go @@ -32,7 +32,7 @@ const ( func init() { Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "f", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ + Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "o", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") } diff --git a/pkg/fakes/sync.go b/pkg/fakes/sync.go index 882ae52cee6..e70e08f74f3 100644 --- a/pkg/fakes/sync.go +++ b/pkg/fakes/sync.go @@ -1,6 +1,7 @@ package fakes import ( + configv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/config/v1alpha1" syncsetv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/syncset/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,6 +15,10 @@ func SyncSetFor(name string, kinds []schema.GroupVersionKind) *syncsetv1alpha1.S } return &syncsetv1alpha1.SyncSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: syncsetv1alpha1.GroupVersion.String(), + Kind: "SyncSet", + }, ObjectMeta: metav1.ObjectMeta{ Name: name, }, @@ -22,3 +27,26 @@ func SyncSetFor(name string, kinds []schema.GroupVersionKind) *syncsetv1alpha1.S }, } } + +// ConfigFor returns a config resource with a SyncOnly containing the requested set of resources. +func ConfigFor(kinds []schema.GroupVersionKind) *configv1alpha1.Config { + entries := make([]configv1alpha1.SyncOnlyEntry, len(kinds)) + for i := range kinds { + entries[i] = configv1alpha1.SyncOnlyEntry(kinds[i]) + } + + return &configv1alpha1.Config{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configv1alpha1.GroupVersion.String(), + Kind: "Config", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: configv1alpha1.ConfigSpec{ + Sync: configv1alpha1.Sync{ + SyncOnly: entries, + }, + }, + } +} diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 7cca872fcbe..4add59783e9 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -615,20 +615,6 @@ spec: kinds: - apiGroups: ["*"] kinds: ["*"] -` - SyncSet = ` -apiVersion: syncset.gatekeeper.sh/v1alpha1 -kind: SyncSet -metadata: - name: syncset -spec: - gvks: - - group: "networking.k8s.io" - version: "v1" - kind: "Ingress" - - group: "apps" - version: "v1" - kind: "Deployment" ` BadSyncSet = ` apiVersion: syncset.gatekeeper.sh/v1alpha1 @@ -637,21 +623,6 @@ metadata: name: syncset spec: unknownField: "unknown" -` - Config = ` -apiVersion: config.gatekeeper.sh/v1alpha1 -kind: Config -metadata: - name: config -spec: - sync: - syncOnly: - - group: "" - version: "v1" - kind: "Service" - - group: "apps" - version: "v1" - kind: "Deployment" ` BadConfig = ` apiVersion: config.gatekeeper.sh/v1alpha1 diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go index 6f441067672..41d20caa520 100644 --- a/pkg/gator/sync/test/test_test.go +++ b/pkg/gator/sync/test/test_test.go @@ -6,10 +6,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" + "github.com/open-policy-agent/gatekeeper/v3/pkg/fakes" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/fixtures" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/yaml" ) func TestTest(t *testing.T) { @@ -41,7 +45,18 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config", inputs: []string{ fixtures.TemplateReferential, - fixtures.Config, + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + })), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -50,8 +65,25 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config and supported by cluster", inputs: []string{ fixtures.TemplateReferential, - fixtures.Config, - fixtures.GVKManifestServices, + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + })), + toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + })), }, wantReqs: map[string]parser.SyncRequirements{}, }, @@ -59,8 +91,25 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config but not supported by cluster", inputs: []string{ fixtures.TemplateReferential, - fixtures.Config, - fixtures.GVKManifestDeployments, + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + })), + toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ + { + Group: "app", + Version: "v1", + Kind: "Deployment", + }, + })), }, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { @@ -78,7 +127,18 @@ func TestTest(t *testing.T) { name: "multi equivalentset req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultEquivSets, - fixtures.SyncSet, + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + { + Group: "networking.k8s.io", + Version: "v1", + Kind: "Ingress", + }, + })), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -87,8 +147,18 @@ func TestTest(t *testing.T) { name: "multi requirement, one req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultReqs, - fixtures.SyncSet, - }, + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + { + Group: "networking.k8s.io", + Version: "v1", + Kind: "Ingress", + }, + }))}, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueingresshostmultireq": { @@ -108,8 +178,30 @@ func TestTest(t *testing.T) { fixtures.TemplateReferential, fixtures.TemplateReferentialMultEquivSets, fixtures.TemplateReferentialMultReqs, - fixtures.Config, - fixtures.SyncSet, + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + })), + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + { + Group: "networking.k8s.io", + Version: "v1", + Kind: "Ingress", + }, + })), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ @@ -180,7 +272,18 @@ func TestTest_Errors(t *testing.T) { name: "error if manifest not provided and omitGVKManifest not set", inputs: []string{ fixtures.TemplateReferential, - fixtures.Config, + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Service", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + })), }, wantErrs: map[string]error{}, err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), @@ -222,3 +325,8 @@ func TestTest_Errors(t *testing.T) { }) } } + +func toYAMLString(obj runtime.Object) string { + yaml, _ := yaml.Marshal(obj) + return string(yaml) +} From 0fe90b9e5b05a6cc0f3764967e7a2dc77b340b65 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 10 Jan 2024 16:21:40 -0800 Subject: [PATCH 15/24] fixes Signed-off-by: Anlan Du --- cmd/gator/sync/sync.go | 2 +- cmd/gator/sync/test/test.go | 2 +- pkg/fakes/gvkmanifest.go | 34 ++++++ pkg/gator/fixtures/fixtures.go | 76 ++++++------- pkg/gator/sync/test/test.go | 12 +-- pkg/gator/sync/test/test_test.go | 176 ++++++++----------------------- 6 files changed, 128 insertions(+), 174 deletions(-) create mode 100644 pkg/fakes/gvkmanifest.go diff --git a/cmd/gator/sync/sync.go b/cmd/gator/sync/sync.go index bb664cf04a9..bc96ec26c16 100644 --- a/cmd/gator/sync/sync.go +++ b/cmd/gator/sync/sync.go @@ -14,7 +14,7 @@ var commands = []*cobra.Command{ var Cmd = &cobra.Command{ Use: "sync", Short: "Manage SyncSets and Config", - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { fmt.Println("Usage: gator sync test") }, } diff --git a/cmd/gator/sync/test/test.go b/cmd/gator/sync/test/test.go index e2f019f016d..225745f7604 100644 --- a/cmd/gator/sync/test/test.go +++ b/cmd/gator/sync/test/test.go @@ -36,7 +36,7 @@ func init() { "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") } -func run(cmd *cobra.Command, args []string) { +func run(_ *cobra.Command, _ []string) { unstrucs, err := reader.ReadSources(flagFilenames, flagImages, "") if err != nil { cmdutils.ErrFatalf("reading: %v", err) diff --git a/pkg/fakes/gvkmanifest.go b/pkg/fakes/gvkmanifest.go new file mode 100644 index 00000000000..af72b93a17b --- /dev/null +++ b/pkg/fakes/gvkmanifest.go @@ -0,0 +1,34 @@ +package fakes + +import ( + gvkmanifestv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GVKManifestFor returns a GVKManifest resource with the given name for the requested set of resources. +func GVKManifestFor(name string, gvks []schema.GroupVersionKind) *gvkmanifestv1alpha1.GVKManifest { + groups := map[string]gvkmanifestv1alpha1.Versions{} + for _, gvk := range gvks { + if groups[gvk.Group] == nil { + groups[gvk.Group] = gvkmanifestv1alpha1.Versions{} + } + if groups[gvk.Group][gvk.Version] == nil { + groups[gvk.Group][gvk.Version] = gvkmanifestv1alpha1.Kinds{} + } + groups[gvk.Group][gvk.Version] = append(groups[gvk.Group][gvk.Version], gvk.Kind) + } + + return &gvkmanifestv1alpha1.GVKManifest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvkmanifestv1alpha1.GroupVersion.String(), + Kind: "GVKManifest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: gvkmanifestv1alpha1.GVKManifestSpec{ + Groups: groups, + }, + } +} diff --git a/pkg/gator/fixtures/fixtures.go b/pkg/gator/fixtures/fixtures.go index 4add59783e9..fc63019c5e9 100644 --- a/pkg/gator/fixtures/fixtures.go +++ b/pkg/gator/fixtures/fixtures.go @@ -336,6 +336,46 @@ metadata: } ] ]" +spec: + crd: + spec: + names: + kind: K8sUniqueServiceSelector + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8suniqueserviceselector + make_apiversion(kind) = apiVersion { + g := kind.group + v := kind.version + g != "" + apiVersion = sprintf("%v/%v", [g, v]) + } + make_apiversion(kind) = apiVersion { + kind.group == "" + apiVersion = kind.version + } + identical(obj, review) { + obj.metadata.namespace == review.namespace + obj.metadata.name == review.name + obj.kind == review.kind.kind + obj.apiVersion == make_apiversion(review.kind) + } + flatten_selector(obj) = flattened { + selectors := [s | s = concat(":", [key, val]); val = obj.spec.selector[key]] + flattened := concat(",", sort(selectors)) + } + violation[{"msg": msg}] { + input.review.kind.kind == "Service" + input.review.kind.version == "v1" + input.review.kind.group == "" + input_selector := flatten_selector(input.review.object) + other := data.inventory.namespace[namespace][_]["Service"][name] + not identical(other, input.review) + other_selector := flatten_selector(other) + input_selector == other_selector + msg := sprintf("same selector as service <%v> in namespace <%v>", [name, namespace]) + } ` TemplateReferentialMultEquivSets = ` @@ -615,41 +655,5 @@ spec: kinds: - apiGroups: ["*"] kinds: ["*"] -` - BadSyncSet = ` -apiVersion: syncset.gatekeeper.sh/v1alpha1 -kind: SyncSet -metadata: - name: syncset -spec: - unknownField: "unknown" -` - BadConfig = ` -apiVersion: config.gatekeeper.sh/v1alpha1 -kind: Config -metadata: - name: config -spec: - unknownField: "unknown" -` - GVKManifestServices = ` -apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 -kind: GVKManifest -metadata: - name: gvkmanifest -spec: - groups: - "": - "v1": ["Service"] -` - GVKManifestDeployments = ` -apiVersion: gvkmanifest.gatekeeper.sh/v1alpha1 -kind: GVKManifest -metadata: - name: gvkmanifest -spec: - groups: - apps: - "v1": ["Deployment"] ` ) diff --git a/pkg/gator/sync/test/test.go b/pkg/gator/sync/test/test.go index 0eef8aca9ba..39555b16a58 100644 --- a/pkg/gator/sync/test/test.go +++ b/pkg/gator/sync/test/test.go @@ -6,7 +6,6 @@ import ( cfapis "github.com/open-policy-agent/frameworks/constraint/pkg/apis" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" gkapis "github.com/open-policy-agent/gatekeeper/v3/apis" - gvkmanifestv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" @@ -40,7 +39,8 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri var err error for _, obj := range unstrucs { - if reader.IsSyncSet(obj) { + switch { + case reader.IsSyncSet(obj): syncSet, err := reader.ToSyncSet(scheme, obj) if err != nil { return nil, nil, fmt.Errorf("converting unstructured %q to syncset: %w", obj.GetName(), err) @@ -53,7 +53,7 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } syncedGVKs[gvk] = struct{}{} } - } else if reader.IsConfig(obj) { + case reader.IsConfig(obj): if hasConfig { return nil, nil, fmt.Errorf("multiple configs found; Config is a singleton resource") } @@ -70,7 +70,7 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } syncedGVKs[gvk] = struct{}{} } - } else if reader.IsTemplate(obj) { + case reader.IsTemplate(obj): templ, err := reader.ToTemplate(scheme, obj) if err != nil { templateErrs[obj.GetName()] = err @@ -82,7 +82,7 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri continue } templates[templ] = syncRequirements - } else if reader.IsGVKManifest(obj) { + case reader.IsGVKManifest(obj): if gvkManifest == nil { gvkManifest, err = reader.ToGVKManifest(scheme, obj) if err != nil { @@ -91,7 +91,7 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } else { return nil, nil, fmt.Errorf("multiple GVK manifests found; please provide one manifest enumerating the GVKs supported by the cluster") } - } else { + default: fmt.Printf("skipping unstructured %q because it is not a syncset, config, gvk manifest, or template\n", obj.GetName()) } } diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go index 41d20caa520..12c3df21288 100644 --- a/pkg/gator/sync/test/test_test.go +++ b/pkg/gator/sync/test/test_test.go @@ -17,6 +17,27 @@ import ( ) func TestTest(t *testing.T) { + DeploymentGVK := schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + } + ServiceGVK := schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + } + IngressGVK := schema.GroupVersionKind{ + Group: "networking.k8s.io", + Version: "v1", + Kind: "Ingress", + } + PodGVK := schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + } + tcs := []struct { name string inputs []string @@ -32,11 +53,7 @@ func TestTest(t *testing.T) { wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, + ServiceGVK: struct{}{}, }, }, }, @@ -45,18 +62,7 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - })), + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -65,25 +71,8 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config and supported by cluster", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - })), - toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - })), + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ServiceGVK})), }, wantReqs: map[string]parser.SyncRequirements{}, }, @@ -91,34 +80,13 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config but not supported by cluster", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - })), - toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ - { - Group: "app", - Version: "v1", - Kind: "Deployment", - }, - })), + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{DeploymentGVK})), }, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Service", - }: struct{}{}, + ServiceGVK: struct{}{}, }, }, }, @@ -127,18 +95,7 @@ func TestTest(t *testing.T) { name: "multi equivalentset req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultEquivSets, - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - { - Group: "networking.k8s.io", - Version: "v1", - Kind: "Ingress", - }, - })), + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -147,27 +104,13 @@ func TestTest(t *testing.T) { name: "multi requirement, one req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultReqs, - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - { - Group: "networking.k8s.io", - Version: "v1", - Kind: "Ingress", - }, - }))}, + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), + }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueingresshostmultireq": { parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Pod", - }: struct{}{}, + PodGVK: struct{}{}, }, }, }, @@ -178,40 +121,14 @@ func TestTest(t *testing.T) { fixtures.TemplateReferential, fixtures.TemplateReferentialMultEquivSets, fixtures.TemplateReferentialMultReqs, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - })), - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{ - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - { - Group: "networking.k8s.io", - Version: "v1", - Kind: "Ingress", - }, - })), + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueingresshostmultireq": { parser.GVKEquivalenceSet{ - { - Group: "", - Version: "v1", - Kind: "Pod", - }: struct{}{}, + PodGVK: struct{}{}, }, }, }, @@ -250,6 +167,16 @@ func TestTest(t *testing.T) { } func TestTest_Errors(t *testing.T) { + DeploymentGVK := schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + } + ServiceGVK := schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + } tcs := []struct { name string inputs []string @@ -272,18 +199,7 @@ func TestTest_Errors(t *testing.T) { name: "error if manifest not provided and omitGVKManifest not set", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ - { - Group: "", - Version: "v1", - Kind: "Service", - }, - { - Group: "apps", - Version: "v1", - Kind: "Deployment", - }, - })), + toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), }, wantErrs: map[string]error{}, err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), From e3571b7b8a0d4575ae03d9488cc2335d8fe5203d Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 17 Jan 2024 13:22:50 -0800 Subject: [PATCH 16/24] Update website/docs/gator.md Co-authored-by: alex <8968914+acpana@users.noreply.github.com> Signed-off-by: Anlan Du --- website/docs/gator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/gator.md b/website/docs/gator.md index 9cf2148e729..3b0817bfa42 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -570,7 +570,7 @@ Optionally, the `--omit-gvk-manifest` flag can be used to skip the requirement o #### Exit Codes `gator sync test` will return a `0` exit status when the Templates, SyncSets, and -Config are successfully ingested and no requirements are unfulfilled. +Config are successfully ingested and all requirements are fulfilled. An error during evaluation, for example a failure to read a file, will result in a `1` exit status with an error message printed to stderr. From db92c4cb763f9c339e71c9cc4e4b04e3dd81c25d Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 17 Jan 2024 14:46:57 -0800 Subject: [PATCH 17/24] Comments Signed-off-by: Anlan Du --- pkg/controller/config/config_controller.go | 3 +-- pkg/gator/sync/test/test.go | 27 +++++++++++++--------- website/docs/gator.md | 13 ++++++++++- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/pkg/controller/config/config_controller.go b/pkg/controller/config/config_controller.go index c67706e8a36..0542abce4fa 100644 --- a/pkg/controller/config/config_controller.go +++ b/pkg/controller/config/config_controller.go @@ -169,8 +169,7 @@ func (r *ReconcileConfig) Reconcile(ctx context.Context, request reconcile.Reque gvksToSync := []schema.GroupVersionKind{} if exists && instance.GetDeletionTimestamp().IsZero() { for _, entry := range instance.Spec.Sync.SyncOnly { - gvk := schema.GroupVersionKind{Group: entry.Group, Version: entry.Version, Kind: entry.Kind} - gvksToSync = append(gvksToSync, gvk) + gvksToSync = append(gvksToSync, entry.ToGroupVersionKind()) } newExcluder.Add(instance.Spec.Match) diff --git a/pkg/gator/sync/test/test.go b/pkg/gator/sync/test/test.go index 39555b16a58..960f8705fa5 100644 --- a/pkg/gator/sync/test/test.go +++ b/pkg/gator/sync/test/test.go @@ -7,6 +7,7 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" gkapis "github.com/open-policy-agent/gatekeeper/v3/apis" gvkmanifestv1alpha1 "github.com/open-policy-agent/gatekeeper/v3/apis/gvkmanifest/v1alpha1" + "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/aggregator" "github.com/open-policy-agent/gatekeeper/v3/pkg/cachemanager/parser" "github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -31,8 +32,8 @@ func init() { // Reads a list of unstructured objects and a string containing supported GVKs and // outputs a set of missing sync requirements per template and ingestion problems per template. func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[string]parser.SyncRequirements, map[string]error, error) { + gvkAggregator := aggregator.NewGVKAggregator() templates := map[*templates.ConstraintTemplate]parser.SyncRequirements{} - syncedGVKs := map[schema.GroupVersionKind]struct{}{} templateErrs := map[string]error{} hasConfig := false var gvkManifest *gvkmanifestv1alpha1.GVKManifest @@ -45,13 +46,16 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri if err != nil { return nil, nil, fmt.Errorf("converting unstructured %q to syncset: %w", obj.GetName(), err) } + key := aggregator.Key{Source: "syncset", ID: syncSet.ObjectMeta.Name} + gvks := make([]schema.GroupVersionKind, len(syncSet.Spec.GVKs)) for _, gvkEntry := range syncSet.Spec.GVKs { gvk := schema.GroupVersionKind{ Group: gvkEntry.Group, Version: gvkEntry.Version, Kind: gvkEntry.Kind, } - syncedGVKs[gvk] = struct{}{} + gvks = append(gvks, gvk) + gvkAggregator.Upsert(key, gvks) } case reader.IsConfig(obj): if hasConfig { @@ -62,13 +66,17 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri return nil, nil, fmt.Errorf("converting unstructured %q to config: %w", obj.GetName(), err) } hasConfig = true + + key := aggregator.Key{Source: "config", ID: config.ObjectMeta.Name} + gvks := make([]schema.GroupVersionKind, len(config.Spec.Sync.SyncOnly)) for _, syncOnlyEntry := range config.Spec.Sync.SyncOnly { gvk := schema.GroupVersionKind{ Group: syncOnlyEntry.Group, Version: syncOnlyEntry.Version, Kind: syncOnlyEntry.Kind, } - syncedGVKs[gvk] = struct{}{} + gvks = append(gvks, gvk) + gvkAggregator.Upsert(key, gvks) } case reader.IsTemplate(obj): templ, err := reader.ToTemplate(scheme, obj) @@ -101,6 +109,7 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri return nil, templateErrs, nil } + supportedGVKs := map[schema.GroupVersionKind]struct{}{} // Crosscheck synced gvks with supported gvks. if gvkManifest == nil { if !omitGVKManifest { @@ -108,7 +117,6 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } fmt.Print("ignoring absence of supported GVK manifest due to --force-omit-gvk-manifest flag; will assume all synced GVKs are supported by cluster\n") } else { - supportedGVKs := map[schema.GroupVersionKind]struct{}{} for group, versions := range gvkManifest.Spec.Groups { for version, kinds := range versions { for _, kind := range kinds { @@ -121,11 +129,6 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri } } } - for gvk := range syncedGVKs { - if _, exists := supportedGVKs[gvk]; !exists { - delete(syncedGVKs, gvk) - } - } } missingReqs := map[string]parser.SyncRequirements{} @@ -135,8 +138,10 @@ func Test(unstrucs []*unstructured.Unstructured, omitGVKManifest bool) (map[stri for _, requirement := range reqs { requirementMet := false for gvk := range requirement { - if _, exists := syncedGVKs[gvk]; exists { - requirementMet = true + if gvkAggregator.IsPresent(gvk) { + if _, isPresent := supportedGVKs[gvk]; isPresent || omitGVKManifest { + requirementMet = true + } } } if !requirementMet { diff --git a/website/docs/gator.md b/website/docs/gator.md index 3b0817bfa42..9f2b1e1432e 100644 --- a/website/docs/gator.md +++ b/website/docs/gator.md @@ -575,7 +575,18 @@ Config are successfully ingested and all requirements are fulfilled. An error during evaluation, for example a failure to read a file, will result in a `1` exit status with an error message printed to stderr. -Unfulfilled requirements will generate a `1` exit status as well, and the unfulfilled requirements per template will be printed to stderr. +Unfulfilled requirements will generate a `1` exit status as well, and the unfulfilled requirements per template will be printed to stderr, like so: +``` +the following requirements were not met: +templatename1: +- extensions/v1beta1:Ingress +- networking.k8s.io/v1beta1:Ingress OR networking.k8s.io/v1:Ingress +templatename2: +- apps/v1:Deployment +templatename3: +- /v1:Service +``` + ## Bundling Policy into OCI Artifacts From 8051eadd6ac5c235397b42c84423185d418b5ed3 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 17 Jan 2024 14:54:14 -0800 Subject: [PATCH 18/24] align with gator expand as that seems to have the most intuitive shorthands Signed-off-by: Anlan Du --- cmd/gator/sync/test/test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/gator/sync/test/test.go b/cmd/gator/sync/test/test.go index 225745f7604..b405ab9fa9b 100644 --- a/cmd/gator/sync/test/test.go +++ b/cmd/gator/sync/test/test.go @@ -30,9 +30,9 @@ const ( ) func init() { - Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "f", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") + Cmd.Flags().StringArrayVarP(&flagFilenames, flagNameFilename, "n", []string{}, "a file or directory containing Kubernetes resources. Can be specified multiple times.") Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") - Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "o", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ + Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "f", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") } From 1dd07e01f95c52a69b4c5c01bc8131db4e297e6b Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Wed, 17 Jan 2024 15:00:15 -0800 Subject: [PATCH 19/24] Add tempdir flag Signed-off-by: Anlan Du --- cmd/gator/sync/test/test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/gator/sync/test/test.go b/cmd/gator/sync/test/test.go index b405ab9fa9b..0fabb064536 100644 --- a/cmd/gator/sync/test/test.go +++ b/cmd/gator/sync/test/test.go @@ -21,12 +21,14 @@ var ( flagFilenames []string flagImages []string flagOmitGVKManifest bool + flagTempDir string ) const ( flagNameFilename = "filename" flagNameImage = "image" flagNameForce = "force-omit-gvk-manifest" + flagNameTempDir = "tempdir" ) func init() { @@ -34,10 +36,11 @@ func init() { Cmd.Flags().StringArrayVarP(&flagImages, flagNameImage, "i", []string{}, "a URL to an OCI image containing policies. Can be specified multiple times.") Cmd.Flags().BoolVarP(&flagOmitGVKManifest, flagNameForce, "f", false, "Do not require a GVK manifest; if one is not provided, assume all GVKs listed in the requirements "+ "and configs are supported by the cluster under test. If this assumption isn't true, the given config may cause errors or templates may not be enforced correctly even after passing this test.") + Cmd.Flags().StringVarP(&flagTempDir, flagNameTempDir, "d", "", fmt.Sprintf("Specifies the temporary directory to download and unpack images to, if using the --%s flag. Optional.", flagNameImage)) } func run(_ *cobra.Command, _ []string) { - unstrucs, err := reader.ReadSources(flagFilenames, flagImages, "") + unstrucs, err := reader.ReadSources(flagFilenames, flagImages, flagTempDir) if err != nil { cmdutils.ErrFatalf("reading: %v", err) } From 6c1c9c3b322ee0d509fd746c0b93ae2930463fd5 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Thu, 18 Jan 2024 12:32:35 -0800 Subject: [PATCH 20/24] Update pkg/gator/sync/test/test_test.go Co-authored-by: alex <8968914+acpana@users.noreply.github.com> Signed-off-by: Anlan Du --- pkg/gator/sync/test/test_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go index 12c3df21288..89b4147769a 100644 --- a/pkg/gator/sync/test/test_test.go +++ b/pkg/gator/sync/test/test_test.go @@ -242,7 +242,11 @@ func TestTest_Errors(t *testing.T) { } } -func toYAMLString(obj runtime.Object) string { - yaml, _ := yaml.Marshal(obj) +func toYAMLString(t *testing.T, obj runtime.Object) string { + t.Helper() + + yaml, err := yaml.Marshal(obj) + require.NoError(t, err) + return string(yaml) } From d9af5279b0e0fbb318e7d27ce40d2c9b15bdaa7b Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Thu, 18 Jan 2024 13:24:04 -0800 Subject: [PATCH 21/24] f Signed-off-by: Anlan Du --- pkg/gator/sync/test/test_test.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/gator/sync/test/test_test.go b/pkg/gator/sync/test/test_test.go index 89b4147769a..791abeb639e 100644 --- a/pkg/gator/sync/test/test_test.go +++ b/pkg/gator/sync/test/test_test.go @@ -62,7 +62,7 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(t, fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -71,8 +71,8 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config and supported by cluster", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), - toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ServiceGVK})), + toYAMLString(t, fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(t, fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{ServiceGVK})), }, wantReqs: map[string]parser.SyncRequirements{}, }, @@ -80,8 +80,8 @@ func TestTest(t *testing.T) { name: "basic req fulfilled by config but not supported by cluster", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), - toYAMLString(fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{DeploymentGVK})), + toYAMLString(t, fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(t, fakes.GVKManifestFor("gvkmanifest", []schema.GroupVersionKind{DeploymentGVK})), }, wantReqs: map[string]parser.SyncRequirements{ "k8suniqueserviceselector": { @@ -95,7 +95,7 @@ func TestTest(t *testing.T) { name: "multi equivalentset req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultEquivSets, - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), + toYAMLString(t, fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{}, @@ -104,7 +104,7 @@ func TestTest(t *testing.T) { name: "multi requirement, one req fulfilled by syncset", inputs: []string{ fixtures.TemplateReferentialMultReqs, - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), + toYAMLString(t, fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ @@ -121,8 +121,8 @@ func TestTest(t *testing.T) { fixtures.TemplateReferential, fixtures.TemplateReferentialMultEquivSets, fixtures.TemplateReferentialMultReqs, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), - toYAMLString(fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), + toYAMLString(t, fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(t, fakes.SyncSetFor("syncset", []schema.GroupVersionKind{DeploymentGVK, IngressGVK})), }, omitManifest: true, wantReqs: map[string]parser.SyncRequirements{ @@ -199,7 +199,7 @@ func TestTest_Errors(t *testing.T) { name: "error if manifest not provided and omitGVKManifest not set", inputs: []string{ fixtures.TemplateReferential, - toYAMLString(fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), + toYAMLString(t, fakes.ConfigFor([]schema.GroupVersionKind{ServiceGVK, DeploymentGVK})), }, wantErrs: map[string]error{}, err: fmt.Errorf("no GVK manifest found; please provide a manifest enumerating the GVKs supported by the cluster"), @@ -243,10 +243,10 @@ func TestTest_Errors(t *testing.T) { } func toYAMLString(t *testing.T, obj runtime.Object) string { - t.Helper() - + t.Helper() + yaml, err := yaml.Marshal(obj) require.NoError(t, err) - + return string(yaml) } From bff5fea310d0822a9e78ff1ac56997134680bcb5 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Tue, 23 Apr 2024 17:54:15 -0700 Subject: [PATCH 22/24] gen Signed-off-by: Anlan Du --- apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go index c913af095b9..aae278ad6c3 100644 --- a/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go +++ b/apis/gvkmanifest/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* @@ -93,14 +92,16 @@ func (in *GVKManifestSpec) DeepCopyInto(out *GVKManifestSpec) { if val == nil { (*out)[key] = nil } else { - in, out := &val, &outVal + inVal := (*in)[key] + in, out := &inVal, &outVal *out = make(Versions, len(*in)) for key, val := range *in { var outVal []string if val == nil { (*out)[key] = nil } else { - in, out := &val, &outVal + inVal := (*in)[key] + in, out := &inVal, &outVal *out = make(Kinds, len(*in)) copy(*out, *in) } @@ -171,7 +172,8 @@ func (in Versions) DeepCopyInto(out *Versions) { if val == nil { (*out)[key] = nil } else { - in, out := &val, &outVal + inVal := (*in)[key] + in, out := &inVal, &outVal *out = make(Kinds, len(*in)) copy(*out, *in) } From 2cfe2aa39d5fac7637ea6afd12e97f5b1eb895d3 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Mon, 16 Sep 2024 20:01:15 -0700 Subject: [PATCH 23/24] fix bad merge Signed-off-by: Anlan Du --- pkg/gator/test/test_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/gator/test/test_test.go b/pkg/gator/test/test_test.go index 68fa0e6d324..851009c5392 100644 --- a/pkg/gator/test/test_test.go +++ b/pkg/gator/test/test_test.go @@ -38,14 +38,13 @@ func init() { panic(err) } - constraintGatorValidate, err = reader.readUnstructured([]byte(fixtures.ConstraintGatorValidate)) + constraintGatorValidate, err = reader.ReadUnstructured([]byte(fixtures.ConstraintGatorValidate)) if err != nil { panic(err) } - constraintReferential, err = reader.readUnstructured([]byte(fixtures.ConstraintReferential)) - - if err != nil { + constraintReferential, err = reader.ReadUnstructured([]byte(fixtures.ConstraintReferential)) + if err != nil { panic(err) } From bd0c922222e2bbfa08dc40bd6581dac6378ce6d4 Mon Sep 17 00:00:00 2001 From: Anlan Du Date: Tue, 17 Sep 2024 11:32:16 -0700 Subject: [PATCH 24/24] Update pkg/gator/reader/read_resources.go Co-authored-by: Rita Zhang Signed-off-by: Anlan Du --- pkg/gator/reader/read_resources.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gator/reader/read_resources.go b/pkg/gator/reader/read_resources.go index d9543a88d9e..b5ec0260f5b 100644 --- a/pkg/gator/reader/read_resources.go +++ b/pkg/gator/reader/read_resources.go @@ -104,7 +104,7 @@ func ReadTemplate(scheme *runtime.Scheme, f fs.FS, path string) (*templates.Cons return template, nil } -// ToTemplate converts an unstructured object into an object with the schema defined +// ToStructured converts an unstructured object into an object with the schema defined // by u's group, version, and kind. func ToStructured(scheme *runtime.Scheme, u *unstructured.Unstructured) (runtime.Object, error) { gvk := u.GroupVersionKind()