From 77401d7073b275ea50b4f6c156298395a567b2ef Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Thu, 24 Oct 2024 10:27:03 -0400 Subject: [PATCH] Add CBOR variant of admission webhook integration test. The existing admission webhook integration test provides good coverage of serving built-in resources and custom resources, including subresources. Serialization concerns, including roundtrippability, of built-in types have existing test coverage; the CBOR variant of the admission webhook integration test additionally exercises client and server codec wiring. --- .../admissionwebhook/admission_test.go | 28 +++++--- test/integration/framework/cbor.go | 69 +++++++++++++++++++ 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/test/integration/apiserver/admissionwebhook/admission_test.go b/test/integration/apiserver/admissionwebhook/admission_test.go index 3ab0e8f5916..a29fc0b5e1f 100644 --- a/test/integration/apiserver/admissionwebhook/admission_test.go +++ b/test/integration/apiserver/admissionwebhook/admission_test.go @@ -20,7 +20,6 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/json" "fmt" "io" "net/http" @@ -49,6 +48,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" @@ -446,16 +446,25 @@ func (w *warningHandler) HandleWarningHeader(code int, agent string, message str // TestWebhookAdmissionWithWatchCache tests communication between API server and webhook process. func TestWebhookAdmissionWithWatchCache(t *testing.T) { - testWebhookAdmission(t, true) + testWebhookAdmission(t, true, func(testing.TB, *rest.Config) {}) } // TestWebhookAdmissionWithoutWatchCache tests communication between API server and webhook process. func TestWebhookAdmissionWithoutWatchCache(t *testing.T) { - testWebhookAdmission(t, false) + testWebhookAdmission(t, false, func(testing.TB, *rest.Config) {}) +} + +func TestWebhookAdmissionWithCBOR(t *testing.T) { + framework.EnableCBORServingAndStorageForTest(t) + framework.SetTestOnlyCBORClientFeatureGatesForTest(t, true, true) + testWebhookAdmission(t, false, func(t testing.TB, config *rest.Config) { + config.Wrap(framework.AssertRequestResponseAsCBOR(t)) + }) } // testWebhookAdmission tests communication between API server and webhook process. -func testWebhookAdmission(t *testing.T, watchCache bool) { +func testWebhookAdmission(t *testing.T, watchCache bool, reconfigureClient func(testing.TB, *rest.Config)) { + // holder communicates expectations to webhooks, and results from webhooks holder := &holder{ t: t, @@ -528,10 +537,6 @@ func testWebhookAdmission(t *testing.T, watchCache bool) { } // gather resources to test - dynamicClient, err := dynamic.NewForConfig(clientConfig) - if err != nil { - t.Fatal(err) - } _, resources, err := client.Discovery().ServerGroupsAndResources() if err != nil { t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err) @@ -640,6 +645,13 @@ func testWebhookAdmission(t *testing.T, watchCache bool) { for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} { if shouldTestResourceVerb(gvr, resource, verb) { t.Run(verb, func(t *testing.T) { + clientConfig := rest.CopyConfig(clientConfig) + reconfigureClient(t, clientConfig) + dynamicClient, err := dynamic.NewForConfig(clientConfig) + if err != nil { + t.Fatal(err) + } + count++ holder.reset(t) testFunc := getTestFunc(gvr, verb) diff --git a/test/integration/framework/cbor.go b/test/integration/framework/cbor.go index 3ad7e498a99..8bbb003bc59 100644 --- a/test/integration/framework/cbor.go +++ b/test/integration/framework/cbor.go @@ -17,6 +17,9 @@ limitations under the License. package framework import ( + "bytes" + "io" + "net/http" "testing" apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" @@ -24,9 +27,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer/cbor" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" clientfeatures "k8s.io/client-go/features" + "k8s.io/client-go/transport" featuregatetesting "k8s.io/component-base/featuregate/testing" aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" "k8s.io/kubernetes/pkg/api/legacyscheme" @@ -102,3 +107,67 @@ func EnableCBORServingAndStorageForTest(tb testing.TB) { *codecs[scheme] = serializer.NewCodecFactory(scheme, serializer.WithSerializer(newCBORSerializerInfo)) } } + +// AssertRequestResponseAsCBOR returns a transport.WrapperFunc that will report a test error if a +// non-empty request or response body contains data that does not appear to be CBOR-encoded. +func AssertRequestResponseAsCBOR(t testing.TB) transport.WrapperFunc { + recognizer := cbor.NewSerializer(runtime.NewScheme(), runtime.NewScheme()) + + unsupportedPatchContentTypes := sets.New( + "application/json-patch+json", + "application/merge-patch+json", + "application/strategic-merge-patch+json", + ) + + return func(rt http.RoundTripper) http.RoundTripper { + return roundTripperFunc(func(request *http.Request) (*http.Response, error) { + if request.Body != nil && !unsupportedPatchContentTypes.Has(request.Header.Get("Content-Type")) { + requestbody, err := io.ReadAll(request.Body) + if err != nil { + t.Error(err) + } + recognized, _, err := recognizer.RecognizesData(requestbody) + if err != nil { + t.Error(err) + } + if len(requestbody) > 0 && !recognized { + t.Errorf("non-cbor request: 0x%x", requestbody) + } + request.Body = io.NopCloser(bytes.NewReader(requestbody)) + } + + response, rterr := rt.RoundTrip(request) + if rterr != nil { + return response, rterr + } + + // We can't synchronously inspect streaming responses, so tee to a buffer + // and inspect it at the end of the test. + var buf bytes.Buffer + response.Body = struct { + io.Reader + io.Closer + }{ + Reader: io.TeeReader(response.Body, &buf), + Closer: response.Body, + } + t.Cleanup(func() { + recognized, _, err := recognizer.RecognizesData(buf.Bytes()) + if err != nil { + t.Error(err) + } + if buf.Len() > 0 && !recognized { + t.Errorf("non-cbor response: 0x%x", buf.Bytes()) + } + }) + + return response, rterr + }) + } +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +}