Configure JSON content type for generic webhook RESTClient.

Authorization, token authentication, imagepolicy admission, and audit webhooks configure RESTClients
that encode to JSON regardless of the ContentType of the provided rest.Config. Because this is
opaque to the RESTClient, configuring a ContentType other than "application/json" results in
requests with JSON-encoded bodies and a non-JSON media type in the Content-Type header. Webhook
servers that respect the Content-Type request header will be unable to decode an object from the
request body.

Explicitly overriding the ContentType of the provided rest.Config fixes this issue and is consistent
with how clients are constructed for conversion and admission webhooks.

Kubernetes-commit: ed07efbc57939afdf154afa80be35507d0a81d66
This commit is contained in:
Ben Luddy 2025-07-15 11:28:06 -04:00 committed by Kubernetes Publisher
parent 234cda4ae8
commit 36d7848a30
2 changed files with 60 additions and 0 deletions

View File

@ -83,6 +83,7 @@ func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFact
clientConfig := rest.CopyConfig(config)
codec := codecFactory.LegacyCodec(groupVersions...)
clientConfig.ContentType = runtime.ContentTypeJSON
clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
clientConfig.Wrap(x509metrics.NewDeprecatedCertificateRoundTripperWrapperConstructor(

View File

@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
@ -33,11 +34,15 @@ import (
"strings"
"testing"
"time"
"unicode/utf8"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/wait"
exampleinstall "k8s.io/apiserver/pkg/apis/example/install"
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
@ -927,3 +932,57 @@ func getSingleCounterValueFromRegistry(t *testing.T, r metrics.Gatherer, name st
return -1
}
func TestRESTConfigContentType(t *testing.T) {
server, err := newTestServer(clientCert, clientKey, caCert, func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Content-Type"); got != runtime.ContentTypeJSON {
t.Errorf("expected request content-type: want %q got %q", runtime.ContentTypeJSON, got)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("failed to read request body: %v", err)
return
}
if err := json.Unmarshal(body, new(any)); err != nil {
switch {
case len(body) == 0:
t.Log("empty request body")
case utf8.Valid(body):
t.Logf("request body: %s", string(body))
default:
t.Logf("request body: 0x%x", body)
}
t.Errorf("failed to unmarshal request body as json: %v", err)
}
})
if err != nil {
t.Errorf("failed to create server: %v", err)
return
}
defer server.Close()
config := &rest.Config{
ContentConfig: rest.ContentConfig{
ContentType: "foo/bar",
},
Host: server.URL,
TLSClientConfig: rest.TLSClientConfig{
CAData: caCert,
CertData: clientCert,
KeyData: clientKey,
},
}
scheme := runtime.NewScheme()
exampleinstall.Install(scheme)
codecs := serializer.NewCodecFactory(scheme)
groupVersions := []schema.GroupVersion{examplev1.SchemeGroupVersion}
wh, err := NewGenericWebhook(scheme, codecs, config, groupVersions, retryBackoff)
if err != nil {
t.Fatalf("failed to create the webhook: %v", err)
}
if err := wh.RestClient.Post().Body(&examplev1.Pod{}).Do(context.TODO()).Error(); err != nil {
t.Fatalf("failed to complete request: %v", err)
}
}