Merge pull request #116392 from seans3/fallback-verifier

Fallback query param verifier
This commit is contained in:
Kubernetes Prow Robot 2023-03-08 23:06:00 -08:00 committed by GitHub
commit f5ddaa152e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 341 additions and 3 deletions

View File

@ -18,6 +18,7 @@ require (
k8s.io/api v0.0.0 k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0 k8s.io/apimachinery v0.0.0
k8s.io/client-go 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/kube-openapi v0.0.0-20230303024457-afdc3dddf62d
k8s.io/utils v0.0.0-20230209194617-a36077c30491 k8s.io/utils v0.0.0-20230209194617-a36077c30491
sigs.k8s.io/kustomize/api v0.12.1 sigs.k8s.io/kustomize/api v0.12.1
@ -62,7 +63,6 @@ require (
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.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/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
) )

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -30,6 +30,7 @@ import (
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/openapi/cached"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/openapi"
@ -167,8 +168,18 @@ func (f *factoryImpl) Validator(validationDirective string) (validation.Schema,
return nil, err return nil, err
} }
// Create the FieldValidationVerifier for use in the ParamVerifyingSchema. // Create the FieldValidationVerifier for use in the ParamVerifyingSchema.
verifier := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), resource.QueryParamFieldValidation) discoveryClient, err := f.ToDiscoveryClient()
return validation.NewParamVerifyingSchema(schema, verifier, string(validationDirective)), nil 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 // OpenAPISchema returns metadata and structural information about