From f5865043edb807f8c5efff286fe0097a382fcaf0 Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Tue, 7 Mar 2023 21:50:25 +0000 Subject: [PATCH] Fallback query param verifier --- staging/src/k8s.io/cli-runtime/go.mod | 2 +- .../resource/fallback_query_param_verifier.go | 56 ++++ .../fallback_query_param_verifier_test.go | 271 ++++++++++++++++++ .../pkg/cmd/util/factory_client_access.go | 15 +- 4 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier.go create mode 100644 staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier_test.go diff --git a/staging/src/k8s.io/cli-runtime/go.mod b/staging/src/k8s.io/cli-runtime/go.mod index f57b88df673..6d39da8562e 100644 --- a/staging/src/k8s.io/cli-runtime/go.mod +++ b/staging/src/k8s.io/cli-runtime/go.mod @@ -18,6 +18,7 @@ require ( k8s.io/api v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/client-go v0.0.0 + k8s.io/klog/v2 v2.90.1 k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d k8s.io/utils v0.0.0-20230209194617-a36077c30491 sigs.k8s.io/kustomize/api v0.12.1 @@ -62,7 +63,6 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.90.1 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier.go b/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier.go new file mode 100644 index 00000000000..198d5c9d5d2 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 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 resource + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" +) + +// fallbackQueryParamVerifier encapsulates the primary Verifier that +// is invoked, and the secondary/fallback Verifier. +type fallbackQueryParamVerifier struct { + primary Verifier + secondary Verifier +} + +var _ Verifier = &fallbackQueryParamVerifier{} + +// NewFallbackQueryParamVerifier returns a new Verifier which will invoke the +// initial/primary Verifier. If the primary Verifier is "NotFound", then the +// secondary Verifier is invoked as a fallback. +func NewFallbackQueryParamVerifier(primary Verifier, secondary Verifier) Verifier { + return &fallbackQueryParamVerifier{ + primary: primary, + secondary: secondary, + } +} + +// HasSupport returns an error if the passed GVK does not support the +// query param (fieldValidation), as determined by the primary and +// secondary OpenAPI endpoints. The primary endoint is checked first, +// but if it not found, the secondary attempts to determine support. +// If the GVK supports the query param, nil is returned. +func (f *fallbackQueryParamVerifier) HasSupport(gvk schema.GroupVersionKind) error { + err := f.primary.HasSupport(gvk) + if errors.IsNotFound(err) { + klog.V(7).Infoln("openapi v3 endpoint not found...falling back to legacy") + err = f.secondary.HasSupport(gvk) + } + return err +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier_test.go b/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier_test.go new file mode 100644 index 00000000000..757e8bf3904 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/fallback_query_param_verifier_test.go @@ -0,0 +1,271 @@ +/* +Copyright 2023 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 resource + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/openapi/cached" + "k8s.io/client-go/openapi/openapitest" + "k8s.io/client-go/openapi3" +) + +func TestFallbackQueryParamVerifier_PrimaryNoFallback(t *testing.T) { + tests := map[string]struct { + crds []schema.GroupKind // CRDFinder returns these CRD's + gvk schema.GroupVersionKind // GVK whose OpenAPI spec is checked + queryParam VerifiableQueryParam // Usually "fieldValidation" + primaryError error + expectedSupports bool + }{ + "Field validation query param is supported for batch/v1/Job, primary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "Job", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation query param supported for core/v1/Namespace, primary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Namespace", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation unsupported for unknown GVK in primary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "bad", + Version: "v1", + Kind: "Uknown", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + "Unknown query param unsupported (for all GVK's) in primary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + queryParam: "UnknownQueryParam", + expectedSupports: false, + }, + "Field validation query param supported for found CRD in primary verifier": { + crds: []schema.GroupKind{ + { + Group: "example.com", + Kind: "ExampleCRD", + }, + }, + // GVK matches above CRD GroupKind + gvk: schema.GroupVersionKind{ + Group: "example.com", + Version: "v1", + Kind: "ExampleCRD", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation query param unsupported for missing CRD in primary verifier": { + crds: []schema.GroupKind{ + { + Group: "different.com", + Kind: "DifferentCRD", + }, + }, + // GVK does NOT match above CRD GroupKind + gvk: schema.GroupVersionKind{ + Group: "example.com", + Version: "v1", + Kind: "ExampleCRD", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + "List GVK is specifically unsupported in primary verfier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "List", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + } + + root := openapi3.NewRoot(cached.NewClient(openapitest.NewFileClient(t))) + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + primary := createFakeV3Verifier(tc.crds, root, tc.queryParam) + secondary := createFakeLegacyVerifier(tc.crds, &fakeSchema, tc.queryParam) + verifier := NewFallbackQueryParamVerifier(primary, secondary) + err := verifier.HasSupport(tc.gvk) + if tc.expectedSupports && err != nil { + t.Errorf("Expected supports, but returned err for GVK (%s)", tc.gvk) + } else if !tc.expectedSupports && err == nil { + t.Errorf("Expected not supports, but returned no err for GVK (%s)", tc.gvk) + } + }) + } +} + +func TestFallbackQueryParamVerifier_SecondaryFallback(t *testing.T) { + tests := map[string]struct { + crds []schema.GroupKind // CRDFinder returns these CRD's + gvk schema.GroupVersionKind // GVK whose OpenAPI spec is checked + queryParam VerifiableQueryParam // Usually "fieldValidation" + primaryError error + expectedSupports bool + }{ + "Field validation query param is supported for batch/v1/Job, secondary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "Job", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation query param supported for core/v1/Namespace, secondary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Namespace", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation unsupported for unknown GVK, secondary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "bad", + Version: "v1", + Kind: "Uknown", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + "Unknown query param unsupported (for all GVK's), secondary verifier": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + queryParam: "UnknownQueryParam", + expectedSupports: false, + }, + "Field validation query param supported for found CRD, secondary verifier": { + crds: []schema.GroupKind{ + { + Group: "example.com", + Kind: "ExampleCRD", + }, + }, + // GVK matches above CRD GroupKind + gvk: schema.GroupVersionKind{ + Group: "example.com", + Version: "v1", + Kind: "ExampleCRD", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation query param unsupported for missing CRD, secondary verifier": { + crds: []schema.GroupKind{ + { + Group: "different.com", + Kind: "DifferentCRD", + }, + }, + // GVK does NOT match above CRD GroupKind + gvk: schema.GroupVersionKind{ + Group: "example.com", + Version: "v1", + Kind: "ExampleCRD", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + "List GVK is specifically unsupported": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "List", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + } + + // Primary OpenAPI client always returns "NotFound" error, so secondary verifier is used. + fakeOpenAPIClient := openapitest.NewFakeClient() + fakeOpenAPIClient.ForcedErr = errors.NewNotFound(schema.GroupResource{}, "OpenAPI V3 endpoint not found") + root := openapi3.NewRoot(fakeOpenAPIClient) + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + primary := createFakeV3Verifier(tc.crds, root, tc.queryParam) + secondary := createFakeLegacyVerifier(tc.crds, &fakeSchema, tc.queryParam) + verifier := NewFallbackQueryParamVerifier(primary, secondary) + err := verifier.HasSupport(tc.gvk) + if tc.expectedSupports && err != nil { + t.Errorf("Expected supports, but returned err for GVK (%s)", tc.gvk) + } else if !tc.expectedSupports && err == nil { + t.Errorf("Expected not supports, but returned no err for GVK (%s)", tc.gvk) + } + }) + } +} + +// createFakeV3Verifier returns a fake OpenAPI V3 queryParamVerifierV3 struct +// filled in with passed values; implements Verifier interface. +func createFakeV3Verifier(crds []schema.GroupKind, root openapi3.Root, queryParam VerifiableQueryParam) Verifier { + return &queryParamVerifierV3{ + finder: NewCRDFinder(func() ([]schema.GroupKind, error) { + return crds, nil + }), + root: root, + queryParam: queryParam, + } +} + +// createFakeLegacyVerifier returns a fake QueryParamVerifier struct for legacy +// OpenAPI V2; implements Verifier interface. +func createFakeLegacyVerifier(crds []schema.GroupKind, fakeSchema discovery.OpenAPISchemaInterface, queryParam VerifiableQueryParam) Verifier { + return &QueryParamVerifier{ + finder: NewCRDFinder(func() ([]schema.GroupKind, error) { + return crds, nil + }), + openAPIGetter: fakeSchema, + queryParam: queryParam, + } +} 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 4340c03eee4..cf908402954 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 @@ -30,6 +30,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/openapi/cached" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/util/openapi" @@ -167,8 +168,18 @@ func (f *factoryImpl) Validator(validationDirective string) (validation.Schema, return nil, err } // Create the FieldValidationVerifier for use in the ParamVerifyingSchema. - verifier := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), resource.QueryParamFieldValidation) - return validation.NewParamVerifyingSchema(schema, verifier, string(validationDirective)), nil + discoveryClient, err := f.ToDiscoveryClient() + if err != nil { + return nil, err + } + // Memory-cache the OpenAPI V3 responses. The disk cache behavior is determined by + // the discovery client. + oapiV3Client := cached.NewClient(discoveryClient.OpenAPIV3()) + queryParam := resource.QueryParamFieldValidation + primary := 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 } // OpenAPISchema returns metadata and structural information about