From 5160b63154f018a7ed94c758a6eed5bc558b9f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Fri, 10 Mar 2023 14:24:55 +0100 Subject: [PATCH] feat: use kind selectors (#6514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: compile regex globally Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * feat: use kind selectors Signed-off-by: Charles-Edouard Brétéché * clean Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * cache Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * kuttl Signed-off-by: Charles-Edouard Brétéché * kuttl Signed-off-by: Charles-Edouard Brétéché * webhooks rules Signed-off-by: Charles-Edouard Brétéché * kuttl Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché --------- Signed-off-by: Charles-Edouard Brétéché --- pkg/clients/dclient/discovery.go | 94 ++++++++++++++++ pkg/clients/dclient/fake.go | 4 + pkg/clients/dclient/utils.go | 9 +- pkg/clients/dclient/utils_test.go | 105 ++++++++++++++++++ pkg/controllers/webhook/controller.go | 17 +-- pkg/controllers/webhook/utils.go | 28 +++-- pkg/utils/kube/kind.go | 39 +++++++ pkg/utils/kube/kind_test.go | 103 +++++++++++++++++ .../kuttl/webhooks/all-scale/01-policy.yaml | 6 + .../02-webhooks.yaml} | 0 .../kuttl/webhooks/all-scale/README.md | 9 ++ .../webhooks/all-scale/policy-assert.yaml | 9 ++ .../kuttl/webhooks/all-scale/policy.yaml | 22 ++++ .../kuttl/webhooks/all-scale/webhooks.yaml | 34 ++++++ .../expected-webhooks/01-webhooks.yaml | 4 + .../kuttl/webhooks/only-pod/01-policy.yaml | 6 + .../kuttl/webhooks/only-pod/02-webhooks.yaml | 4 + .../kuttl/webhooks/only-pod/README.md | 9 ++ .../webhooks/only-pod/policy-assert.yaml | 9 ++ .../kuttl/webhooks/only-pod/policy.yaml | 22 ++++ .../kuttl/webhooks/only-pod/webhooks.yaml | 21 ++++ .../pod-all-subresources/01-policy.yaml | 6 + .../pod-all-subresources/02-webhooks.yaml | 4 + .../webhooks/pod-all-subresources/README.md | 9 ++ .../pod-all-subresources/policy-assert.yaml | 9 ++ .../webhooks/pod-all-subresources/policy.yaml | 22 ++++ .../pod-all-subresources/webhooks.yaml | 28 +++++ .../kuttl/webhooks/scale/01-policy.yaml | 6 + .../kuttl/webhooks/scale/02-webhooks.yaml | 4 + .../kuttl/webhooks/scale/README.md | 10 ++ .../kuttl/webhooks/scale/policy-assert.yaml | 9 ++ .../kuttl/webhooks/scale/policy.yaml | 22 ++++ .../kuttl/webhooks/scale/webhooks.yaml | 34 ++++++ .../kuttl/webhooks/wildcard/01-policy.yaml | 6 + .../kuttl/webhooks/wildcard/02-webhooks.yaml | 4 + .../kuttl/webhooks/wildcard/README.md | 9 ++ .../webhooks/wildcard/policy-assert.yaml | 9 ++ .../kuttl/webhooks/wildcard/policy.yaml | 22 ++++ .../kuttl/webhooks/wildcard/webhooks.yaml | 35 ++++++ 39 files changed, 776 insertions(+), 26 deletions(-) create mode 100644 pkg/clients/dclient/utils_test.go create mode 100644 test/conformance/kuttl/webhooks/all-scale/01-policy.yaml rename test/conformance/kuttl/webhooks/{expected-webhooks/00-webhooks.yaml => all-scale/02-webhooks.yaml} (100%) create mode 100644 test/conformance/kuttl/webhooks/all-scale/README.md create mode 100644 test/conformance/kuttl/webhooks/all-scale/policy-assert.yaml create mode 100644 test/conformance/kuttl/webhooks/all-scale/policy.yaml create mode 100644 test/conformance/kuttl/webhooks/all-scale/webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/expected-webhooks/01-webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/only-pod/01-policy.yaml create mode 100644 test/conformance/kuttl/webhooks/only-pod/02-webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/only-pod/README.md create mode 100644 test/conformance/kuttl/webhooks/only-pod/policy-assert.yaml create mode 100644 test/conformance/kuttl/webhooks/only-pod/policy.yaml create mode 100644 test/conformance/kuttl/webhooks/only-pod/webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/01-policy.yaml create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/02-webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/README.md create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/policy-assert.yaml create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/policy.yaml create mode 100644 test/conformance/kuttl/webhooks/pod-all-subresources/webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/scale/01-policy.yaml create mode 100644 test/conformance/kuttl/webhooks/scale/02-webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/scale/README.md create mode 100644 test/conformance/kuttl/webhooks/scale/policy-assert.yaml create mode 100644 test/conformance/kuttl/webhooks/scale/policy.yaml create mode 100644 test/conformance/kuttl/webhooks/scale/webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/wildcard/01-policy.yaml create mode 100644 test/conformance/kuttl/webhooks/wildcard/02-webhooks.yaml create mode 100644 test/conformance/kuttl/webhooks/wildcard/README.md create mode 100644 test/conformance/kuttl/webhooks/wildcard/policy-assert.yaml create mode 100644 test/conformance/kuttl/webhooks/wildcard/policy.yaml create mode 100644 test/conformance/kuttl/webhooks/wildcard/webhooks.yaml diff --git a/pkg/clients/dclient/discovery.go b/pkg/clients/dclient/discovery.go index 78c2d4a744..33f454b2bf 100644 --- a/pkg/clients/dclient/discovery.go +++ b/pkg/clients/dclient/discovery.go @@ -8,14 +8,17 @@ import ( openapiv2 "github.com/google/gnostic/openapiv2" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" + "github.com/kyverno/kyverno/pkg/utils/wildcard" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery" ) // IDiscovery provides interface to mange Kind and GVR mapping type IDiscovery interface { + FindResources(group, version, kind, subresource string) ([]schema.GroupVersionResource, error) FindResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error) // TODO: there's no mapping from GVK to GVR, this is very error prone GetGVRFromGVK(schema.GroupVersionKind) (schema.GroupVersionResource, error) @@ -145,6 +148,97 @@ func (c serverResources) FindResource(groupVersion string, kind string) (apiReso return nil, nil, schema.GroupVersionResource{}, err } +func (c serverResources) FindResources(group, version, kind, subresource string) ([]schema.GroupVersionResource, error) { + resources, err := c.findResources(group, version, kind, subresource) + if err != nil { + if !c.cachedClient.Fresh() { + c.cachedClient.Invalidate() + return c.findResources(group, version, kind, subresource) + } + } + return resources, err +} + +func (c serverResources) findResources(group, version, kind, subresource string) ([]schema.GroupVersionResource, error) { + _, serverGroupsAndResources, err := c.cachedClient.ServerGroupsAndResources() + if err != nil && !strings.Contains(err.Error(), "Got empty response for") { + if discovery.IsGroupDiscoveryFailedError(err) { + logDiscoveryErrors(err) + } else if isServerCurrentlyUnableToHandleRequest(err) { + logger.Error(err, "failed to find preferred resource version") + } else { + logger.Error(err, "failed to find preferred resource version") + return nil, err + } + } + getGVK := func(gv schema.GroupVersion, group, version, kind string) schema.GroupVersionKind { + if group == "" { + group = gv.Group + } + if version == "" { + version = gv.Version + } + return schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + } + } + resources := sets.New[schema.GroupVersionResource]() + // first match resouces + for _, list := range serverGroupsAndResources { + gv, err := schema.ParseGroupVersion(list.GroupVersion) + if err != nil { + return nil, err + } else { + for _, resource := range list.APIResources { + if !strings.Contains(resource.Name, "/") { + gvk := getGVK(gv, resource.Group, resource.Version, resource.Kind) + if wildcard.Match(group, gvk.Group) && wildcard.Match(version, gvk.Version) && wildcard.Match(kind, gvk.Kind) { + resources.Insert(gvk.GroupVersion().WithResource(resource.Name)) + } + } + } + } + } + // second match subresouces if necessary + subresources := sets.New[schema.GroupVersionResource]() + if subresource != "" { + for _, list := range serverGroupsAndResources { + for _, resource := range list.APIResources { + for parent := range resources { + if wildcard.Match(parent.Resource+"/"+subresource, resource.Name) { + subresources.Insert(parent.GroupVersion().WithResource(resource.Name)) + break + } + } + } + } + } + // third if no resource matched, try again but consider subresources this time + if resources.Len() == 0 { + for _, list := range serverGroupsAndResources { + gv, err := schema.ParseGroupVersion(list.GroupVersion) + if err != nil { + return nil, err + } else { + for _, resource := range list.APIResources { + gvk := getGVK(gv, resource.Group, resource.Version, resource.Kind) + if wildcard.Match(group, gvk.Group) && wildcard.Match(version, gvk.Version) && wildcard.Match(kind, gvk.Kind) { + resources.Insert(gv.WithResource(resource.Name)) + } + } + } + } + } + if kind == "*" && subresource == "*" { + return resources.Union(subresources).UnsortedList(), nil + } else if subresource != "" { + return subresources.UnsortedList(), nil + } + return resources.UnsortedList(), nil +} + func (c serverResources) findResource(groupVersion string, kind string) (apiResource, parentAPIResource *metav1.APIResource, gvr schema.GroupVersionResource, err error, ) { diff --git a/pkg/clients/dclient/fake.go b/pkg/clients/dclient/fake.go index 08172fb4e5..96e289665b 100644 --- a/pkg/clients/dclient/fake.go +++ b/pkg/clients/dclient/fake.go @@ -86,6 +86,10 @@ func (c *fakeDiscoveryClient) FindResource(groupVersion string, kind string) (ap return nil, nil, schema.GroupVersionResource{}, fmt.Errorf("not implemented") } +func (c *fakeDiscoveryClient) FindResources(group, version, kind, subresource string) ([]schema.GroupVersionResource, error) { + return nil, fmt.Errorf("not implemented") +} + func (c *fakeDiscoveryClient) OpenAPISchema() (*openapiv2.Document, error) { return nil, nil } diff --git a/pkg/clients/dclient/utils.go b/pkg/clients/dclient/utils.go index 343d5a23db..c0bf40560a 100644 --- a/pkg/clients/dclient/utils.go +++ b/pkg/clients/dclient/utils.go @@ -20,9 +20,16 @@ func logDiscoveryErrors(err error) { } } +// isServerCurrentlyUnableToHandleRequest returns true if the error is related to the discovery not able to handle the request +// this can happen with aggregated services when the api server can't get a `TokenReview` and is not able to send requests to +// the underlying service, this is typically due to kyverno blocking `TokenReview` admission requests. +func isServerCurrentlyUnableToHandleRequest(err error) bool { + return err != nil && strings.Contains(err.Error(), "the server is currently unable to handle the request") +} + func isMetricsServerUnavailable(gv schema.GroupVersion, err error) bool { // error message is defined at: // https://github.com/kubernetes/apimachinery/blob/2456ebdaba229616fab2161a615148884b46644b/pkg/api/errors/errors.go#L432 return (gv.Group == "metrics.k8s.io" || gv.Group == "custom.metrics.k8s.io" || gv.Group == "external.metrics.k8s.io") && - strings.Contains(err.Error(), "the server is currently unable to handle the request") + isServerCurrentlyUnableToHandleRequest(err) } diff --git a/pkg/clients/dclient/utils_test.go b/pkg/clients/dclient/utils_test.go new file mode 100644 index 0000000000..476b8f95d1 --- /dev/null +++ b/pkg/clients/dclient/utils_test.go @@ -0,0 +1,105 @@ +package dclient + +import ( + "errors" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func Test_isServerCurrentlyUnableToHandleRequest(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + err: nil, + }, + want: false, + }, { + args: args{ + err: errors.New("another error"), + }, + want: false, + }, { + args: args{ + err: errors.New("the server is currently unable to handle the request"), + }, + want: true, + }, { + args: args{ + err: errors.New("a prefix : the server is currently unable to handle the request"), + }, + want: true, + }, { + args: args{ + err: errors.New("the server is currently unable to handle the request - a suffix"), + }, + want: true, + }, { + args: args{ + err: errors.New("a prefix : the server is currently unable to handle the request - a suffix"), + }, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isServerCurrentlyUnableToHandleRequest(tt.args.err); got != tt.want { + t.Errorf("isServerCurrentlyUnableToHandleRequest() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isMetricsServerUnavailable(t *testing.T) { + type args struct { + gv schema.GroupVersion + err error + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + gv: schema.GroupVersion{Group: "core", Version: "v1"}, + err: nil, + }, + want: false, + }, { + args: args{ + gv: schema.GroupVersion{Group: "core", Version: "v1"}, + err: errors.New("the server is currently unable to handle the request"), + }, + want: false, + }, { + args: args{ + gv: schema.GroupVersion{Group: "metrics.k8s.io", Version: "v1"}, + err: errors.New("the server is currently unable to handle the request"), + }, + want: true, + }, { + args: args{ + gv: schema.GroupVersion{Group: "custom.metrics.k8s.io", Version: "v1"}, + err: errors.New("the server is currently unable to handle the request"), + }, + want: true, + }, { + args: args{ + gv: schema.GroupVersion{Group: "external.metrics.k8s.io", Version: "v1"}, + err: errors.New("the server is currently unable to handle the request"), + }, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isMetricsServerUnavailable(tt.args.gv, tt.args.err); got != tt.want { + t.Errorf("isMetricsServerUnavailable() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/controllers/webhook/controller.go b/pkg/controllers/webhook/controller.go index a6e75abb61..65fc699766 100644 --- a/pkg/controllers/webhook/controller.go +++ b/pkg/controllers/webhook/controller.go @@ -847,22 +847,13 @@ func (c *controller) mergeWebhook(dst *webhook, policy kyvernov1.PolicyInterface if _, ok := gvkMap[gvk]; !ok { gvkMap[gvk] = 1 // NOTE: webhook stores GVR in its rules while policy stores GVK in its rules definition - gv, k := kubeutils.GetKindFromGVK(gvk) - _, parentAPIResource, gvr, err := c.discoveryClient.FindResource(gv, k) + group, version, kind, subresource := kubeutils.ParseKindSelector(gvk) + gvrs, err := c.discoveryClient.FindResources(group, version, kind, subresource) if err != nil { - logger.Error(err, "unable to convert GVK to GVR", "GVK", gvk) + logger.Error(err, "unable to find resource", "group", group, "version", version, "kind", kind, "subresource", subresource) continue } - if parentAPIResource != nil { - gvr = schema.GroupVersionResource{ - Group: parentAPIResource.Group, - Version: parentAPIResource.Version, - Resource: gvr.Resource, - } - } - if strings.Contains(gvk, "*") { - gvrList = append(gvrList, schema.GroupVersionResource{Group: gvr.Group, Version: "*", Resource: gvr.Resource}) - } else { + for _, gvr := range gvrs { logger.V(4).Info("configuring webhook", "GVK", gvk, "GVR", gvr) gvrList = append(gvrList, gvr) } diff --git a/pkg/controllers/webhook/utils.go b/pkg/controllers/webhook/utils.go index 1e9a268c76..acbdc07cfe 100644 --- a/pkg/controllers/webhook/utils.go +++ b/pkg/controllers/webhook/utils.go @@ -17,30 +17,28 @@ import ( type webhook struct { maxWebhookTimeout int32 failurePolicy admissionregistrationv1.FailurePolicyType - rules map[schema.GroupVersionResource]struct{} + rules map[schema.GroupVersion]sets.Set[string] } func newWebhook(timeout int32, failurePolicy admissionregistrationv1.FailurePolicyType) *webhook { return &webhook{ maxWebhookTimeout: timeout, failurePolicy: failurePolicy, - rules: map[schema.GroupVersionResource]struct{}{}, + rules: map[schema.GroupVersion]sets.Set[string]{}, } } func (wh *webhook) buildRulesWithOperations(ops ...admissionregistrationv1.OperationType) []admissionregistrationv1.RuleWithOperations { var rules []admissionregistrationv1.RuleWithOperations - for gvr := range wh.rules { - resources := sets.New(gvr.Resource) - ephemeralContainersGVR := schema.GroupVersionResource{Resource: "pods/ephemeralcontainers", Group: "", Version: "v1"} - _, rulesContainEphemeralContainers := wh.rules[ephemeralContainersGVR] - if resources.Has("pods") && !rulesContainEphemeralContainers { + for gv, resources := range wh.rules { + // if we have pods, we add pods/ephemeralcontainers by default + if gv.Group == "" && gv.Version == "v1" && resources.Has("pods") { resources.Insert("pods/ephemeralcontainers") } rules = append(rules, admissionregistrationv1.RuleWithOperations{ Rule: admissionregistrationv1.Rule{ - APIGroups: []string{gvr.Group}, - APIVersions: []string{gvr.Version}, + APIGroups: []string{gv.Group}, + APIVersions: []string{gv.Version}, Resources: sets.List(resources), }, Operations: ops, @@ -73,7 +71,13 @@ func (wh *webhook) buildRulesWithOperations(ops ...admissionregistrationv1.Opera } func (wh *webhook) set(gvr schema.GroupVersionResource) { - wh.rules[gvr] = struct{}{} + gv := gvr.GroupVersion() + resources := wh.rules[gv] + if resources == nil { + wh.rules[gv] = sets.New(gvr.Resource) + } else { + resources.Insert(gvr.Resource) + } } func (wh *webhook) isEmpty() bool { @@ -81,8 +85,8 @@ func (wh *webhook) isEmpty() bool { } func (wh *webhook) setWildcard() { - wh.rules = map[schema.GroupVersionResource]struct{}{ - {Group: "*", Version: "*", Resource: "*/*"}: {}, + wh.rules = map[schema.GroupVersion]sets.Set[string]{ + {Group: "*", Version: "*"}: sets.New("*/*"), } } diff --git a/pkg/utils/kube/kind.go b/pkg/utils/kube/kind.go index ef0b5fc1bb..1db89096b5 100644 --- a/pkg/utils/kube/kind.go +++ b/pkg/utils/kube/kind.go @@ -9,6 +9,45 @@ import ( var versionRegex = regexp.MustCompile(`^v\d((alpha|beta)\d)?|\*$`) +func ParseKindSelector(input string) (string, string, string, string) { + parts := strings.Split(input, "/") + if len(parts) > 0 { + parts = append(parts[:len(parts)-1], strings.Split(parts[len(parts)-1], ".")...) + } + switch len(parts) { + case 1: + // we have only kind + return "*", "*", parts[0], "" + case 2: + // `*/*` means all resources and subresources + if parts[0] == "*" && parts[1] == "*" { + return "*", "*", "*", "*" + } + // detect the `*/subresource` case when part[1] is all lowercase + if parts[0] == "*" && strings.ToLower(parts[1]) == parts[1] { + return "*", "*", parts[0], parts[1] + } + // if the first part is `*` or a version we have version/kind + if versionRegex.MatchString(parts[0]) { + return "*", parts[0], parts[1], "" + } + // we have kind/subresource + return "*", "*", parts[0], parts[1] + case 3: + // if the first part is `*` or a version we have version/kind/subresource + if versionRegex.MatchString(parts[0]) { + return "*", parts[0], parts[1], parts[2] + } + // we have group/version/kind + return parts[0], parts[1], parts[2], "" + case 4: + // we have group/version/kind/subresource + return parts[0], parts[1], parts[2], parts[3] + default: + return "", "", "", "" + } +} + // GetKindFromGVK - get kind and APIVersion from GVK func GetKindFromGVK(str string) (string, string) { parts := strings.Split(str, "/") diff --git a/pkg/utils/kube/kind_test.go b/pkg/utils/kube/kind_test.go index 1ff4be874d..a0012cbb6b 100644 --- a/pkg/utils/kube/kind_test.go +++ b/pkg/utils/kube/kind_test.go @@ -108,3 +108,106 @@ func Test_GroupVersionMatches(t *testing.T) { groupVersion, serverResourceGroupVersion = "certificates.k8s.io/*", "networking.k8s.io/v1" assert.Equal(t, GroupVersionMatches(groupVersion, serverResourceGroupVersion), false) } + +func TestParseKindSelector(t *testing.T) { + type args struct { + input string + } + type want struct { + group string + version string + kind string + subresource string + } + tests := []struct { + name string + args args + want want + }{{ + args: args{"*"}, + want: want{"*", "*", "*", ""}, + }, { + args: args{"*.*"}, + want: want{"*", "*", "*", "*"}, + }, { + args: args{"*/*"}, + want: want{"*", "*", "*", "*"}, + }, { + args: args{"Pod"}, + want: want{"*", "*", "Pod", ""}, + }, { + args: args{"v1/Pod"}, + want: want{"*", "v1", "Pod", ""}, + }, { + args: args{"batch/*/CronJob"}, + want: want{"batch", "*", "CronJob", ""}, + }, { + args: args{"storage.k8s.io/v1/CSIDriver"}, + want: want{"storage.k8s.io", "v1", "CSIDriver", ""}, + }, { + args: args{"tekton.dev/v1beta1/TaskRun/status"}, + want: want{"tekton.dev", "v1beta1", "TaskRun", "status"}, + }, { + args: args{"v1/Pod.status"}, + want: want{"*", "v1", "Pod", "status"}, + }, { + args: args{"v1/Pod/status"}, + want: want{"*", "v1", "Pod", "status"}, + }, { + args: args{"Pod.status"}, + want: want{"*", "*", "Pod", "status"}, + }, { + args: args{"Pod/status"}, + want: want{"*", "*", "Pod", "status"}, + }, { + args: args{"apps/v1/Deployment/scale"}, + want: want{"apps", "v1", "Deployment", "scale"}, + }, { + args: args{"v1/ReplicationController/scale"}, + want: want{"*", "v1", "ReplicationController", "scale"}, + }, { + args: args{"*/ReplicationController/scale"}, + want: want{"*", "*", "ReplicationController", "scale"}, + }, { + args: args{"*/Deployment/scale"}, + want: want{"*", "*", "Deployment", "scale"}, + }, { + args: args{"*/Deployment.scale"}, + want: want{"*", "*", "Deployment", "scale"}, + }, { + args: args{"apps/v1/Deployment.scale"}, + want: want{"apps", "v1", "Deployment", "scale"}, + }, { + args: args{"*/scale"}, + want: want{"*", "*", "*", "scale"}, + }, { + args: args{"Pod/*"}, + want: want{"*", "*", "Pod", "*"}, + }, { + args: args{"*/*/*"}, + want: want{"*", "*", "*", "*"}, + }, { + args: args{"*/*/*/*"}, + want: want{"*", "*", "*", "*"}, + }, { + args: args{"*/*/*/*/*"}, + want: want{"", "", "", ""}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + group, version, kind, subresource := ParseKindSelector(tt.args.input) + if group != tt.want.group { + t.Errorf("ParseKindSelector() group = %v, want %v", group, tt.want.group) + } + if version != tt.want.version { + t.Errorf("ParseKindSelector() version = %v, want %v", version, tt.want.version) + } + if kind != tt.want.kind { + t.Errorf("ParseKindSelector() kind = %v, want %v", kind, tt.want.kind) + } + if subresource != tt.want.subresource { + t.Errorf("ParseKindSelector() subresource = %v, want %v", subresource, tt.want.subresource) + } + }) + } +} diff --git a/test/conformance/kuttl/webhooks/all-scale/01-policy.yaml b/test/conformance/kuttl/webhooks/all-scale/01-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/webhooks/all-scale/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/webhooks/expected-webhooks/00-webhooks.yaml b/test/conformance/kuttl/webhooks/all-scale/02-webhooks.yaml similarity index 100% rename from test/conformance/kuttl/webhooks/expected-webhooks/00-webhooks.yaml rename to test/conformance/kuttl/webhooks/all-scale/02-webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/all-scale/README.md b/test/conformance/kuttl/webhooks/all-scale/README.md new file mode 100644 index 0000000000..ab3d2f0064 --- /dev/null +++ b/test/conformance/kuttl/webhooks/all-scale/README.md @@ -0,0 +1,9 @@ +## Description + +This test verifies the resource validation webhook is configured correctly when a policy targets all `*/scale` subresources. + +## Steps + +1. - Create a policy targeting `*/scale` + - Assert policy gets ready +1. - Assert that the resource validation webhook is configured correctly diff --git a/test/conformance/kuttl/webhooks/all-scale/policy-assert.yaml b/test/conformance/kuttl/webhooks/all-scale/policy-assert.yaml new file mode 100644 index 0000000000..2993bbaa6e --- /dev/null +++ b/test/conformance/kuttl/webhooks/all-scale/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/webhooks/all-scale/policy.yaml b/test/conformance/kuttl/webhooks/all-scale/policy.yaml new file mode 100644 index 0000000000..292f5ba0b8 --- /dev/null +++ b/test/conformance/kuttl/webhooks/all-scale/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: false + rules: + - name: require-team + match: + any: + - resources: + kinds: + - '*/scale' + validate: + message: 'The label `team` is required.' + pattern: + metadata: + labels: + team: '?*' diff --git a/test/conformance/kuttl/webhooks/all-scale/webhooks.yaml b/test/conformance/kuttl/webhooks/all-scale/webhooks.yaml new file mode 100644 index 0000000000..feacde5fdb --- /dev/null +++ b/test/conformance/kuttl/webhooks/all-scale/webhooks.yaml @@ -0,0 +1,34 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + webhook.kyverno.io/managed-by: kyverno + name: kyverno-resource-validating-webhook-cfg +webhooks: +- rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - replicationcontrollers/scale + scope: '*' + - apiGroups: + - apps + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - deployments/scale + - replicasets/scale + - statefulsets/scale + scope: '*' diff --git a/test/conformance/kuttl/webhooks/expected-webhooks/01-webhooks.yaml b/test/conformance/kuttl/webhooks/expected-webhooks/01-webhooks.yaml new file mode 100644 index 0000000000..7048b639a6 --- /dev/null +++ b/test/conformance/kuttl/webhooks/expected-webhooks/01-webhooks.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +assert: +- webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/only-pod/01-policy.yaml b/test/conformance/kuttl/webhooks/only-pod/01-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/webhooks/only-pod/02-webhooks.yaml b/test/conformance/kuttl/webhooks/only-pod/02-webhooks.yaml new file mode 100644 index 0000000000..7048b639a6 --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/02-webhooks.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +assert: +- webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/only-pod/README.md b/test/conformance/kuttl/webhooks/only-pod/README.md new file mode 100644 index 0000000000..875668ed3d --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/README.md @@ -0,0 +1,9 @@ +## Description + +This test verifies the resource validation webhook is configured correctly when a policy targets `Pod`. + +## Steps + +1. - Create a policy targeting `Pod` + - Assert policy gets ready +1. - Assert that the resource validation webhook is configured correctly diff --git a/test/conformance/kuttl/webhooks/only-pod/policy-assert.yaml b/test/conformance/kuttl/webhooks/only-pod/policy-assert.yaml new file mode 100644 index 0000000000..2993bbaa6e --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/webhooks/only-pod/policy.yaml b/test/conformance/kuttl/webhooks/only-pod/policy.yaml new file mode 100644 index 0000000000..8349e314ec --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: false + rules: + - name: require-team + match: + any: + - resources: + kinds: + - Pod + validate: + message: 'The label `team` is required.' + pattern: + metadata: + labels: + team: '?*' diff --git a/test/conformance/kuttl/webhooks/only-pod/webhooks.yaml b/test/conformance/kuttl/webhooks/only-pod/webhooks.yaml new file mode 100644 index 0000000000..49a26e89be --- /dev/null +++ b/test/conformance/kuttl/webhooks/only-pod/webhooks.yaml @@ -0,0 +1,21 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + webhook.kyverno.io/managed-by: kyverno + name: kyverno-resource-validating-webhook-cfg +webhooks: +- rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - pods + - pods/ephemeralcontainers + scope: '*' diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/01-policy.yaml b/test/conformance/kuttl/webhooks/pod-all-subresources/01-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/02-webhooks.yaml b/test/conformance/kuttl/webhooks/pod-all-subresources/02-webhooks.yaml new file mode 100644 index 0000000000..7048b639a6 --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/02-webhooks.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +assert: +- webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/README.md b/test/conformance/kuttl/webhooks/pod-all-subresources/README.md new file mode 100644 index 0000000000..cc43b07a19 --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/README.md @@ -0,0 +1,9 @@ +## Description + +This test verifies the resource validation webhook is configured correctly when a policy targets all `Pod/*` subresources. + +## Steps + +1. - Create a policy targeting `Pod/*` + - Assert policy gets ready +1. - Assert that the resource validation webhook is configured correctly diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/policy-assert.yaml b/test/conformance/kuttl/webhooks/pod-all-subresources/policy-assert.yaml new file mode 100644 index 0000000000..2993bbaa6e --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/policy.yaml b/test/conformance/kuttl/webhooks/pod-all-subresources/policy.yaml new file mode 100644 index 0000000000..2faf585890 --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: false + rules: + - name: require-team + match: + any: + - resources: + kinds: + - Pod/* + validate: + message: 'The label `team` is required.' + pattern: + metadata: + labels: + team: '?*' diff --git a/test/conformance/kuttl/webhooks/pod-all-subresources/webhooks.yaml b/test/conformance/kuttl/webhooks/pod-all-subresources/webhooks.yaml new file mode 100644 index 0000000000..9766dc5d34 --- /dev/null +++ b/test/conformance/kuttl/webhooks/pod-all-subresources/webhooks.yaml @@ -0,0 +1,28 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + webhook.kyverno.io/managed-by: kyverno + name: kyverno-resource-validating-webhook-cfg +webhooks: +- rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - pods/attach + - pods/binding + - pods/ephemeralcontainers + - pods/eviction + - pods/exec + - pods/log + - pods/portforward + - pods/proxy + - pods/status + scope: '*' diff --git a/test/conformance/kuttl/webhooks/scale/01-policy.yaml b/test/conformance/kuttl/webhooks/scale/01-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/webhooks/scale/02-webhooks.yaml b/test/conformance/kuttl/webhooks/scale/02-webhooks.yaml new file mode 100644 index 0000000000..7048b639a6 --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/02-webhooks.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +assert: +- webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/scale/README.md b/test/conformance/kuttl/webhooks/scale/README.md new file mode 100644 index 0000000000..0e45391b3b --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/README.md @@ -0,0 +1,10 @@ +## Description + +This test verifies the resource validation webhook is configured correctly when a policy targets all `Scale` resource. +It should be equivalent to using `*/scale` + +## Steps + +1. - Create a policy targeting `Scale` + - Assert policy gets ready +1. - Assert that the resource validation webhook is configured correctly diff --git a/test/conformance/kuttl/webhooks/scale/policy-assert.yaml b/test/conformance/kuttl/webhooks/scale/policy-assert.yaml new file mode 100644 index 0000000000..2993bbaa6e --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/webhooks/scale/policy.yaml b/test/conformance/kuttl/webhooks/scale/policy.yaml new file mode 100644 index 0000000000..bd4a502ad9 --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: false + rules: + - name: require-team + match: + any: + - resources: + kinds: + - Scale + validate: + message: 'The label `team` is required.' + pattern: + metadata: + labels: + team: '?*' diff --git a/test/conformance/kuttl/webhooks/scale/webhooks.yaml b/test/conformance/kuttl/webhooks/scale/webhooks.yaml new file mode 100644 index 0000000000..feacde5fdb --- /dev/null +++ b/test/conformance/kuttl/webhooks/scale/webhooks.yaml @@ -0,0 +1,34 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + webhook.kyverno.io/managed-by: kyverno + name: kyverno-resource-validating-webhook-cfg +webhooks: +- rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - replicationcontrollers/scale + scope: '*' + - apiGroups: + - apps + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - deployments/scale + - replicasets/scale + - statefulsets/scale + scope: '*' diff --git a/test/conformance/kuttl/webhooks/wildcard/01-policy.yaml b/test/conformance/kuttl/webhooks/wildcard/01-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/webhooks/wildcard/02-webhooks.yaml b/test/conformance/kuttl/webhooks/wildcard/02-webhooks.yaml new file mode 100644 index 0000000000..7048b639a6 --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/02-webhooks.yaml @@ -0,0 +1,4 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +assert: +- webhooks.yaml diff --git a/test/conformance/kuttl/webhooks/wildcard/README.md b/test/conformance/kuttl/webhooks/wildcard/README.md new file mode 100644 index 0000000000..e998fbf5cc --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/README.md @@ -0,0 +1,9 @@ +## Description + +This test verifies the resource validation webhook is configured correctly when a policy targets all `*` resource. + +## Steps + +1. - Create a policy targeting `*` + - Assert policy gets ready +1. - Assert that the resource validation webhook is configured correctly diff --git a/test/conformance/kuttl/webhooks/wildcard/policy-assert.yaml b/test/conformance/kuttl/webhooks/wildcard/policy-assert.yaml new file mode 100644 index 0000000000..2993bbaa6e --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/webhooks/wildcard/policy.yaml b/test/conformance/kuttl/webhooks/wildcard/policy.yaml new file mode 100644 index 0000000000..ce9f80c1e3 --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/policy.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + pod-policies.kyverno.io/autogen-controllers: none +spec: + validationFailureAction: Audit + background: false + rules: + - name: require-team + match: + any: + - resources: + kinds: + - '*' + validate: + message: 'The label `team` is required.' + pattern: + metadata: + labels: + team: '?*' diff --git a/test/conformance/kuttl/webhooks/wildcard/webhooks.yaml b/test/conformance/kuttl/webhooks/wildcard/webhooks.yaml new file mode 100644 index 0000000000..2abb82641b --- /dev/null +++ b/test/conformance/kuttl/webhooks/wildcard/webhooks.yaml @@ -0,0 +1,35 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + webhook.kyverno.io/managed-by: kyverno + name: kyverno-resource-validating-webhook-cfg +webhooks: +- failurePolicy: Ignore + rules: + - apiGroups: + - '*' + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - '*/*' + scope: '*' +- failurePolicy: Fail + rules: + - apiGroups: + - '*' + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + - DELETE + - CONNECT + resources: + - '*/*' + scope: '*'