diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier.go new file mode 100644 index 00000000000..ea111223f21 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier.go @@ -0,0 +1,59 @@ +/* +Copyright 2025 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 util + +import ( + "sync" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" +) + +// cachingVerifier wraps the given Verifier to cache return values forever. +type cachingVerifier struct { + cache map[schema.GroupVersionKind]error + mu sync.RWMutex + next resource.Verifier +} + +// newCachingVerifier creates a new cache using the given underlying verifier. +func newCachingVerifier(next resource.Verifier) *cachingVerifier { + return &cachingVerifier{ + cache: make(map[schema.GroupVersionKind]error), + next: next, + } +} + +// HasSupport implements resource.Verifier. It cached return values from the underlying verifier forever. +func (cv *cachingVerifier) HasSupport(gvk schema.GroupVersionKind) error { + // Try to get the cached value. + cv.mu.RLock() + err, ok := cv.cache[gvk] + cv.mu.RUnlock() + if ok { + return err + } + + // Cache miss. Get the actual result. + err = cv.next.HasSupport(gvk) + + // Update the cache. + cv.mu.Lock() + cv.cache[gvk] = err + cv.mu.Unlock() + return err +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier_test.go new file mode 100644 index 00000000000..4587a9d1bc6 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/caching_verifier_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2025 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 util + +import ( + "errors" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type verifierCall struct { + GVK schema.GroupVersionKind + Err error +} + +type mockVerifier struct { + t *testing.T + expectedCalls map[schema.GroupVersionKind]error +} + +func (m *mockVerifier) CheckExpectations() { + if len(m.expectedCalls) != 0 { + m.t.Errorf("Expected calls remaining: %v", m.expectedCalls) + } +} + +func (m *mockVerifier) HasSupport(gvk schema.GroupVersionKind) error { + returnErr, ok := m.expectedCalls[gvk] + if !ok { + m.t.Errorf("Unexpected HasSupport call with GVK=%v", gvk) + } + delete(m.expectedCalls, gvk) + return returnErr +} + +func TestCachingVerifier(t *testing.T) { + gvk1 := schema.GroupVersionKind{ + Group: "group", + Version: "version", + Kind: "kind", + } + gvk2 := schema.GroupVersionKind{ + Group: "group2", + Version: "version2", + Kind: "kind2", + } + + err1 := errors.New("some error") + + testCases := []struct { + name string + calls []verifierCall + expectedUnderlyingCalls map[schema.GroupVersionKind]error + }{ + { + name: "return value is cached", + calls: []verifierCall{ + {GVK: gvk1, Err: nil}, + {GVK: gvk1, Err: nil}, + {GVK: gvk1, Err: nil}, + {GVK: gvk2, Err: err1}, + {GVK: gvk2, Err: err1}, + {GVK: gvk2, Err: err1}, + }, + expectedUnderlyingCalls: map[schema.GroupVersionKind]error{ + gvk1: nil, + gvk2: err1, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &mockVerifier{ + t: t, + expectedCalls: tc.expectedUnderlyingCalls, + } + verifier := newCachingVerifier(m) + + for _, call := range tc.calls { + err := verifier.HasSupport(call.GVK) + if !errors.Is(err, call.Err) { + t.Errorf("Expected error: %v, got: %v", call.Err, err) + } + } + + m.CheckExpectations() + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go index 6a1646b844e..c2a06cf9280 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go @@ -172,7 +172,7 @@ func (f *factoryImpl) Validator(validationDirective string) (validation.Schema, // the discovery client. oapiV3Client := cached.NewClient(discoveryClient.OpenAPIV3()) queryParam := resource.QueryParamFieldValidation - primary := resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam) + primary := newCachingVerifier(resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam)) secondary := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), queryParam) fallback := resource.NewFallbackQueryParamVerifier(primary, secondary) return validation.NewParamVerifyingSchema(schema, fallback, string(validationDirective)), nil