Merge pull request #132960 from benluddy/webhook-client-content-type

Configure JSON content type for generic webhook RESTClient.
This commit is contained in:
Kubernetes Prow Robot
2025-09-02 08:01:15 -07:00
committed by GitHub
3 changed files with 268 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
/*
Copyright 2025 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 app
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"k8s.io/apiserver/pkg/authorization/authorizer"
authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1"
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/rest"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
)
func TestAuthzWebhookRequestEncoding(t *testing.T) {
testCases := []struct {
name string
ContentType string
ExpectContentType string
ExpectRequestBodyPrefix []byte
}{
{
name: "json",
ContentType: "application/json",
ExpectContentType: "application/json",
ExpectRequestBodyPrefix: []byte(`{`),
},
{
name: "empty",
ContentType: "",
ExpectContentType: "application/json",
ExpectRequestBodyPrefix: []byte(`{`),
},
{
name: "protobuf",
ContentType: "application/vnd.kubernetes.protobuf",
ExpectContentType: "application/vnd.kubernetes.protobuf",
ExpectRequestBodyPrefix: []byte("\x6b\x38\x73\x00"), // k8s protobuf magic number
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handlerInvoked := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer close(handlerInvoked)
if got := r.Header.Get("Content-Type"); got != tc.ExpectContentType {
t.Errorf("unexpected Content-Type: got %q, want %q", got, tc.ExpectContentType)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
if !bytes.HasPrefix(body, tc.ExpectRequestBodyPrefix) {
t.Errorf("request body should have prefix %q, but got %q", tc.ExpectRequestBodyPrefix, body)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"kind":"SubjectAccessReview","apiVersion":"authorization.k8s.io/v1","status":{"allowed":true}}`)); err != nil {
t.Fatalf("unexpected response write failure: %v", err)
}
}))
defer server.Close()
cfg := &rest.Config{
Host: server.URL,
ContentConfig: rest.ContentConfig{
ContentType: tc.ContentType,
},
}
authzClient, err := authorizationv1client.NewForConfigAndClient(cfg, server.Client())
if err != nil {
t.Fatalf("failed to create authorization client: %v", err)
}
authz, err := BuildAuthz(authzClient, kubeletconfig.KubeletAuthorization{Mode: kubeletconfig.KubeletAuthorizationModeWebhook})
if err != nil {
t.Fatalf("failed to build authorizer: %v", err)
}
if _, _, err := authz.Authorize(context.Background(), &authorizer.AttributesRecord{}); err != nil {
t.Fatalf("Authorize failed: %v", err)
}
select {
case <-handlerInvoked:
default:
t.Fatal("webhook handler not invoked")
}
})
}
}
func TestAuthnWebhookRequestEncoding(t *testing.T) {
testCases := []struct {
name string
ContentType string
ExpectContentType string
ExpectRequestBodyPrefix []byte
}{
{
name: "json",
ContentType: "application/json",
ExpectContentType: "application/json",
ExpectRequestBodyPrefix: []byte(`{`),
},
{
name: "empty",
ContentType: "",
ExpectContentType: "application/json",
ExpectRequestBodyPrefix: []byte(`{`),
},
{
name: "protobuf",
ContentType: "application/vnd.kubernetes.protobuf",
ExpectContentType: "application/vnd.kubernetes.protobuf",
ExpectRequestBodyPrefix: []byte("\x6b\x38\x73\x00"), // k8s protobuf magic number
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handlerInvoked := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer close(handlerInvoked)
if got := r.Header.Get("Content-Type"); got != tc.ExpectContentType {
t.Errorf("unexpected Content-Type: got %q, want %q", got, tc.ExpectContentType)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
if !bytes.HasPrefix(body, tc.ExpectRequestBodyPrefix) {
t.Errorf("request body should have prefix %q, but got %q", tc.ExpectRequestBodyPrefix, body)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"kind":"TokenReview","apiVersion":"authentication.k8s.io/v1","status":{"authenticated":true}}`)); err != nil {
t.Fatalf("unexpected response write failure: %v", err)
}
}))
defer server.Close()
cfg := &rest.Config{
Host: server.URL,
ContentConfig: rest.ContentConfig{
ContentType: tc.ContentType,
},
}
authnClient, err := authenticationv1client.NewForConfigAndClient(cfg, server.Client())
if err != nil {
t.Fatalf("failed to create authentication client: %v", err)
}
authn, _, err := BuildAuthn(authnClient, kubeletconfig.KubeletAuthentication{
Webhook: kubeletconfig.KubeletWebhookAuthentication{
Enabled: true,
},
})
if err != nil {
t.Fatalf("failed to build authenticator: %v", err)
}
request, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "/fooz", nil)
if err != nil {
t.Fatalf("failed to build test request: %v", err)
}
request.Header.Set("Authorization", "Bearer foo")
if _, _, err := authn.AuthenticateRequest(request); err != nil {
t.Fatalf("AuthenticateToken failed: %v", err)
}
select {
case <-handlerInvoked:
default:
t.Fatal("webhook handler not invoked")
}
})
}
}

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