From 90d4a961b615481b1eecd0e189997cf217b5f97a Mon Sep 17 00:00:00 2001 From: Mohd Kamaal <102820439+Mohdcode@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:13:51 +0530 Subject: [PATCH] Updated application logic to treat an empty 'group' string as the core API group for GlobalContextEntry (#10572) * Updated application logic to treat an empty 'group' string as the core API group for GlobalContextEntry Signed-off-by: Mohdcode * Updated application logic to treat an empty 'group' string as the core API group for GlobalContextEntry Signed-off-by: Mohdcode * Updated application logic to treat an empty 'group' string as the core API group for GlobalContextEntry Signed-off-by: Mohdcode * Update global_context_entry_types.go Signed-off-by: Mohd Kamaal <102820439+Mohdcode@users.noreply.github.com> --------- Signed-off-by: Mohdcode Signed-off-by: Mohd Kamaal <102820439+Mohdcode@users.noreply.github.com> Co-authored-by: shuting --- .../v2alpha1/global_context_entry_types.go | 7 +- .../global_context_entry_types_test.go | 246 ++++++++++++++++++ .../kyverno.io_globalcontextentries.yaml | 1 - .../kyverno.io_globalcontextentries.yaml | 1 - config/install-latest-testing.yaml | 1 - 5 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 api/kyverno/v2alpha1/global_context_entry_types_test.go diff --git a/api/kyverno/v2alpha1/global_context_entry_types.go b/api/kyverno/v2alpha1/global_context_entry_types.go index 65c81383f2d5..81e70e15d63c 100644 --- a/api/kyverno/v2alpha1/global_context_entry_types.go +++ b/api/kyverno/v2alpha1/global_context_entry_types.go @@ -118,8 +118,8 @@ type GlobalContextEntryList struct { // KubernetesResource stores infos about kubernetes resource that should be cached type KubernetesResource struct { // Group defines the group of the resource. - // +kubebuilder:validation:Required - Group string `json:"group"` + // +kubebuilder:validation:Optional + Group string `json:"group,omitempty"` // Version defines the version of the resource. // +kubebuilder:validation:Required Version string `json:"version"` @@ -136,7 +136,8 @@ type KubernetesResource struct { // Validate implements programmatic validation func (k *KubernetesResource) Validate(path *field.Path) (errs field.ErrorList) { - if k.Group == "" { + isCoreGroup := k.Group == "" && k.Version == "v1" + if k.Group == "" && !isCoreGroup { errs = append(errs, field.Required(path.Child("group"), "A Resource entry requires a group")) } if k.Version == "" { diff --git a/api/kyverno/v2alpha1/global_context_entry_types_test.go b/api/kyverno/v2alpha1/global_context_entry_types_test.go new file mode 100644 index 000000000000..d3654dc8f00f --- /dev/null +++ b/api/kyverno/v2alpha1/global_context_entry_types_test.go @@ -0,0 +1,246 @@ +/* +Copyright 2022 The Kubernetes authors. + +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. +*/ + +package v2alpha1 + +import ( + "math/rand" + "strconv" + "testing" + "time" + + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestGlobalContextEntrySpecValidate(t *testing.T) { + tests := []struct { + name string + spec GlobalContextEntrySpec + wantErr bool + }{ + { + name: "valid KubernetesResource", + spec: GlobalContextEntrySpec{ + KubernetesResource: &KubernetesResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }, + }, + wantErr: false, + }, + { + name: "valid APICall", + spec: GlobalContextEntrySpec{ + APICall: &ExternalAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces", + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + }, + wantErr: false, + }, + { + name: "both KubernetesResource and APICall", + spec: GlobalContextEntrySpec{ + KubernetesResource: &KubernetesResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }, + APICall: &ExternalAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces", + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + }, + wantErr: true, + }, + { + name: "neither KubernetesResource nor APICall", + spec: GlobalContextEntrySpec{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := tt.spec.Validate(field.NewPath("spec")) + if (len(errs) > 0) != tt.wantErr { + t.Errorf("GlobalContextEntrySpec.Validate() error = %v, wantErr %v", errs, tt.wantErr) + } + }) + } +} + +func TestKubernetesResourceValidate(t *testing.T) { + tests := []struct { + name string + resource KubernetesResource + wantErr bool + }{ + { + name: "valid KubernetesResource", + resource: KubernetesResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }, + wantErr: false, + }, + { + name: "missing group only if version is v1 or CoreGroup", + resource: KubernetesResource{ + Group: "", + Version: "v1", + Resource: "deployments", + }, + wantErr: false, + }, + { + name: "missing group with random version", + resource: KubernetesResource{ + Group: "", + Version: generateRandomVersion(), + Resource: "deployments", + }, + wantErr: true, + }, + + { + name: "missing version", + resource: KubernetesResource{ + Group: "app", + Resource: "deployments", + }, + wantErr: true, + }, + { + name: "missing resource", + resource: KubernetesResource{ + Group: "apps", + Version: "v1", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := tt.resource.Validate(field.NewPath("resource")) + if (len(errs) > 0) != tt.wantErr { + t.Errorf("KubernetesResource.Validate() error = %v, wantErr %v", errs, tt.wantErr) + } + }) + } +} + +func TestExternalAPICallValidate(t *testing.T) { + tests := []struct { + name string + apiCall ExternalAPICall + wantErr bool + }{ + { + name: "valid ExternalAPICall", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces", + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + wantErr: false, + }, + { + name: "missing RefreshInterval", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces", + }, + RefreshInterval: &metav1.Duration{Duration: 0 * time.Second}, + }, + wantErr: true, + }, + { + name: "both Service and URLPath", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{ + Service: &kyvernov1.ServiceCall{}, + URLPath: "/api/v1/namespaces", + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + wantErr: true, + }, + { + name: "missing Service and URLPath", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{}, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + wantErr: true, + }, + { + name: "POST method without data", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{ + Method: "POST", + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + wantErr: true, + }, + + { + name: "non-POST method with data", + apiCall: ExternalAPICall{ + APICall: kyvernov1.APICall{ + Method: "GET", + Data: []kyvernov1.RequestData{ + {Key: "example-key", Value: &apiextv1.JSON{Raw: []byte(`{"field": "value"}`)}}, + }, + }, + RefreshInterval: &metav1.Duration{Duration: 10 * time.Minute}, + }, + + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := tt.apiCall.Validate(field.NewPath("apiCall")) + if (len(errs) > 0) != tt.wantErr { + t.Errorf("ExternalAPICall.Validate() error = %v, wantErr %v", errs, tt.wantErr) + } + }) + } +} +func generateRandomVersion() string { + rand.NewSource(time.Now().UnixNano()) + for { + version := "v" + strconv.Itoa(rand.Intn(9)+2) // Generates a number between 2 and 10 (inclusive) + if version != "v1" { + return version + } + } +} diff --git a/charts/kyverno/charts/crds/templates/kyverno.io/kyverno.io_globalcontextentries.yaml b/charts/kyverno/charts/crds/templates/kyverno.io/kyverno.io_globalcontextentries.yaml index d3a19ffb9d91..2af691846a9e 100644 --- a/charts/kyverno/charts/crds/templates/kyverno.io/kyverno.io_globalcontextentries.yaml +++ b/charts/kyverno/charts/crds/templates/kyverno.io/kyverno.io_globalcontextentries.yaml @@ -156,7 +156,6 @@ spec: description: Version defines the version of the resource. type: string required: - - group - resource - version type: object diff --git a/config/crds/kyverno/kyverno.io_globalcontextentries.yaml b/config/crds/kyverno/kyverno.io_globalcontextentries.yaml index c4c743acb294..ad2fb3ea027b 100644 --- a/config/crds/kyverno/kyverno.io_globalcontextentries.yaml +++ b/config/crds/kyverno/kyverno.io_globalcontextentries.yaml @@ -150,7 +150,6 @@ spec: description: Version defines the version of the resource. type: string required: - - group - resource - version type: object diff --git a/config/install-latest-testing.yaml b/config/install-latest-testing.yaml index 65b752f2ac50..f5e0bd937a63 100644 --- a/config/install-latest-testing.yaml +++ b/config/install-latest-testing.yaml @@ -22659,7 +22659,6 @@ spec: description: Version defines the version of the resource. type: string required: - - group - resource - version type: object