diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3.go b/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3.go new file mode 100644 index 00000000000..3d08c202f5e --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3.go @@ -0,0 +1,126 @@ +/* +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/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/openapi" + "k8s.io/client-go/openapi3" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +var _ Verifier = &queryParamVerifierV3{} + +// NewQueryParamVerifierV3 returns a pointer to the created queryParamVerifier3 struct, +// which implements the Verifier interface. The caching characteristics of the +// OpenAPI V3 specs are determined by the passed oapiClient. For memory caching, the +// client should be wrapped beforehand as: cached.NewClient(oapiClient). The disk +// caching is determined by the discovery client the oapiClient is created from. +func NewQueryParamVerifierV3(dynamicClient dynamic.Interface, oapiClient openapi.Client, queryParam VerifiableQueryParam) Verifier { + return &queryParamVerifierV3{ + finder: NewCRDFinder(CRDFromDynamic(dynamicClient)), + root: openapi3.NewRoot(oapiClient), + queryParam: queryParam, + } +} + +// queryParamVerifierV3 encapsulates info necessary to determine if +// the queryParam is a parameter for the Patch endpoint for a +// passed GVK. +type queryParamVerifierV3 struct { + finder CRDFinder + root openapi3.Root + queryParam VerifiableQueryParam +} + +var namespaceGVK = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"} + +// HasSupport returns nil error if the passed GVK supports the parameter +// (stored in struct; usually "fieldValidation") for Patch endpoint. +// Returns an error if the passed GVK does not support the query param, +// or if another error occurred. If the Open API V3 spec for a CRD is not +// found, then the spec for Namespace is checked for query param support instead. +func (v *queryParamVerifierV3) HasSupport(gvk schema.GroupVersionKind) error { + gvSpec, err := v.root.GVSpec(gvk.GroupVersion()) + if err == nil { + if supports := supportsQueryParamV3(gvSpec, gvk, v.queryParam); supports { + return nil + } + return NewParamUnsupportedError(gvk, v.queryParam) + } + if _, isErr := err.(*openapi3.GroupVersionNotFoundError); !isErr { + return err + } + // If the spec for the passed GVK is not found, then check if it is a CRD. + // For CRD's substitute Namespace OpenAPI V3 spec to check if query param is supported. + if found, _ := v.finder.HasCRD(gvk.GroupKind()); found { + namespaceSpec, err := v.root.GVSpec(namespaceGVK.GroupVersion()) + if err != nil { + // If error retrieving Namespace spec, propagate error. + return err + } + if supports := supportsQueryParamV3(namespaceSpec, namespaceGVK, v.queryParam); supports { + return nil + } + } + return NewParamUnsupportedError(gvk, v.queryParam) +} + +// hasGVKExtensionV3 returns true if the passed OpenAPI extensions map contains +// the passed GVK; false otherwise. +func hasGVKExtensionV3(extensions spec.Extensions, gvk schema.GroupVersionKind) bool { + var oapiGVK map[string]string + err := extensions.GetObject("x-kubernetes-group-version-kind", &oapiGVK) + if err != nil { + return false + } + if oapiGVK["group"] == gvk.Group && + oapiGVK["version"] == gvk.Version && + oapiGVK["kind"] == gvk.Kind { + return true + } + return false +} + +// supportsQueryParam is a method that let's us look in the OpenAPI if the +// specific group-version-kind supports the specific query parameter for +// the PATCH end-point. Returns true if the query param is supported by the +// spec for the passed GVK; false otherwise. +func supportsQueryParamV3(doc *spec3.OpenAPI, gvk schema.GroupVersionKind, queryParam VerifiableQueryParam) bool { + for _, path := range doc.Paths.Paths { + // If operation is not PATCH, then continue. + op := path.PathProps.Patch + if op == nil { + continue + } + // Is this PATCH operation for the passed GVK? + if !hasGVKExtensionV3(op.VendorExtensible.Extensions, gvk) { + continue + } + // Now look for the query parameter among the parameters + // for the PATCH operation. + for _, param := range op.OperationProps.Parameters { + if param.ParameterProps.Name == string(queryParam) { + return true + } + } + return false + } + return false +} diff --git a/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3_test.go b/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3_test.go new file mode 100644 index 00000000000..603189e85d5 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/pkg/resource/query_param_verifier_v3_test.go @@ -0,0 +1,127 @@ +/* +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/runtime/schema" + "k8s.io/client-go/openapi/cached" + "k8s.io/client-go/openapi/openapitest" + "k8s.io/client-go/openapi3" +) + +func TestV3SupportsQueryParamBatchV1(t *testing.T) { + tests := map[string]struct { + crds []schema.GroupKind // CRDFinder returns these CRD's + gvk schema.GroupVersionKind // GVK whose OpenAPI V3 spec is checked + queryParam VerifiableQueryParam // Usually "fieldValidation" + expectedSupports bool + }{ + "Field validation query param is supported for batch/v1/Job": { + 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": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Namespace", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: true, + }, + "Field validation unsupported for unknown GVK": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "bad", + Version: "v1", + Kind: "Uknown", + }, + queryParam: QueryParamFieldValidation, + expectedSupports: false, + }, + "Unknown query param unsupported (for all GVK's)": { + crds: []schema.GroupKind{}, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + queryParam: "UnknownQueryParam", + expectedSupports: false, + }, + "Field validation query param supported for found CRD": { + 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": { + 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, + }, + } + + root := openapi3.NewRoot(cached.NewClient(openapitest.NewFileClient(t))) + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + verifier := &queryParamVerifierV3{ + finder: NewCRDFinder(func() ([]schema.GroupKind, error) { + return tc.crds, nil + }), + root: root, + queryParam: tc.queryParam, + } + 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) + } + }) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 14b696d8460..7604e8c1cbb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1892,6 +1892,7 @@ k8s.io/client-go/metadata/metadatainformer k8s.io/client-go/metadata/metadatalister k8s.io/client-go/openapi k8s.io/client-go/openapi/cached +k8s.io/client-go/openapi3 k8s.io/client-go/pkg/apis/clientauthentication k8s.io/client-go/pkg/apis/clientauthentication/install k8s.io/client-go/pkg/apis/clientauthentication/v1