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