From e179f38cb8707512331660ffea9fff1bd025af90 Mon Sep 17 00:00:00 2001 From: Richa Banker Date: Fri, 14 Nov 2025 12:59:21 -0800 Subject: [PATCH 1/2] zpages - add proper handling of the application/yaml Accept Header --- .../apiserver/pkg/server/flagz/flagz.go | 49 +++++-- .../apiserver/pkg/server/flagz/flagz_test.go | 101 +++++++++++--- .../apiserver/pkg/server/statusz/statusz.go | 54 +++++--- .../pkg/server/statusz/statusz_test.go | 120 ++++++++++++++--- .../controlplane/kube_apiserver_test.go | 127 ++++++++++++------ 5 files changed, 347 insertions(+), 104 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go index de5dfe79fee..235b8bbcd8e 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go @@ -25,11 +25,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/features" v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" "k8s.io/apiserver/pkg/server/flagz/negotiate" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) const ( @@ -39,6 +42,12 @@ const ( Version = "v1alpha1" ) +// flagzCodecFactory wraps a CodecFactory to filter out unsupported media types (like protobuf) +// from the supported media types list, so error messages only show actually supported types. +type flagzCodecFactory struct { + serializer.CodecFactory +} + type mux interface { Handle(path string, handler http.Handler) } @@ -55,8 +64,7 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) { scheme := runtime.NewScheme() utilruntime.Must(v1alpha1.AddToScheme(scheme)) - codecFactory := serializer.NewCodecFactory( - scheme, + codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { textSerializer := flagzTextSerializer{componentName, reg.reader} return runtime.SerializerInfo{ @@ -68,8 +76,28 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) { PrettySerializer: textSerializer, } }), - ) - m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, codecFactory, negotiate.FlagzEndpointRestrictions{})) + } + // TODO: remove this explicit check when https://github.com/kubernetes/enhancements/pull/5740 is implemented. + if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) { + codecFactoryOpts = append(codecFactoryOpts, serializer.WithSerializer(cbor.NewSerializerInfo)) + } + + codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) + // Wrap to filter out unsupported media types (e.g., protobuf) + filteredCodecFactory := &flagzCodecFactory{codecFactory} + m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, negotiate.FlagzEndpointRestrictions{})) +} + +func (f *flagzCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { + allTypes := f.CodecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + for _, info := range allTypes { + if info.MediaType == runtime.ContentTypeProtobuf { + continue + } + filtered = append(filtered, info) + } + return filtered } func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc { @@ -96,9 +124,9 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia var targetGV schema.GroupVersion switch serializerInfo.MediaType { - case "application/json": + case "application/json", "application/yaml", "application/cbor": if mediaType.Convert == nil { - err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for application/json") + err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for %s", serializerInfo.MediaType) utilruntime.HandleError(err) responsewriters.ErrorNegotiated( err, @@ -113,11 +141,11 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia if reg.deprecatedVersions()[targetGV.Version] { w.Header().Set("Warning", `299 - "This version of the flagz endpoint is deprecated. Please use a newer version."`) } + writeStructuredResponse(obj, serializer, targetGV, restrictions, w, r) case "text/plain": writePlainTextResponse(obj, serializer, w, reg) - return default: - err = fmt.Errorf("content negotiation failed: unsupported media type '%s'", serializerInfo.MediaType) + err := fmt.Errorf("unsupported media type: %s/%s", serializerInfo.MediaType, serializerInfo.MediaTypeSubType) utilruntime.HandleError(err) responsewriters.ErrorNegotiated( err, @@ -126,10 +154,7 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia w, r, ) - return } - - writeResponse(obj, serializer, targetGV, restrictions, w, r) } } @@ -170,7 +195,7 @@ func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSer } } -func writeResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.FlagzEndpointRestrictions, w http.ResponseWriter, r *http.Request) { +func writeStructuredResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.FlagzEndpointRestrictions, w http.ResponseWriter, r *http.Request) { responsewriters.WriteObjectNegotiated( serializer, restrictions, diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go index 4f6e6148492..68b303ff712 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go @@ -27,8 +27,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + "k8s.io/apiserver/pkg/features" v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" + utilfeature "k8s.io/apiserver/pkg/util/feature" cliflag "k8s.io/component-base/cli/flag" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "sigs.k8s.io/yaml" ) const wantTmpl = ` @@ -37,6 +42,8 @@ Warning: This endpoint is not meant to be machine parseable, has no formatting c ` func TestHandleFlagz(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + fakeFlagName := "test-flag" fakeFlagValue := "test-value" fs := pflag.NewFlagSet("test", pflag.ContinueOnError) @@ -50,14 +57,14 @@ func TestHandleFlagz(t *testing.T) { } tests := []struct { - name string - acceptHeader string - componentName string - registry *registry - wantStatusCode int - wantBody string - wantJSONBody *v1alpha1.Flagz - wantWarning bool + name string + acceptHeader string + componentName string + registry *registry + wantStatusCode int + wantBody string + wantStructuredBody *v1alpha1.Flagz + wantWarning bool }{ { name: "valid request for text/plain", @@ -74,7 +81,7 @@ func TestHandleFlagz(t *testing.T) { ), }, { - name: "valid request for v1alpha1", + name: "valid request for application/json", acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz", componentName: "test-server", registry: ®istry{ @@ -82,7 +89,7 @@ func TestHandleFlagz(t *testing.T) { deprecatedVersionsMap: map[string]bool{}, }, wantStatusCode: http.StatusOK, - wantJSONBody: &v1alpha1.Flagz{ + wantStructuredBody: &v1alpha1.Flagz{ TypeMeta: metav1.TypeMeta{ Kind: Kind, APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), @@ -104,7 +111,7 @@ func TestHandleFlagz(t *testing.T) { deprecatedVersionsMap: map[string]bool{"v1alpha1": true}, }, wantStatusCode: http.StatusOK, - wantJSONBody: &v1alpha1.Flagz{ + wantStructuredBody: &v1alpha1.Flagz{ TypeMeta: metav1.TypeMeta{ Kind: Kind, APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), @@ -118,6 +125,50 @@ func TestHandleFlagz(t *testing.T) { }, wantWarning: true, }, + { + name: "valid request for application/yaml", + acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind, + APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + }, + { + name: "valid request for application/cbor", + acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind, + APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + }, { name: "no accept header falls back to text/plain", acceptHeader: "", @@ -224,12 +275,10 @@ func TestHandleFlagz(t *testing.T) { } if tt.wantStatusCode == http.StatusOK { - if tt.wantJSONBody != nil { + if tt.wantStructuredBody != nil { var got v1alpha1.Flagz - if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { - t.Fatalf("unexpected error while unmarshalling response: %v", err) - } - if diff := cmp.Diff(*tt.wantJSONBody, got); diff != "" { + unmarshalResponse(t, w.Header().Get("Content-Type"), w.Body.Bytes(), &got) + if diff := cmp.Diff(*tt.wantStructuredBody, got); diff != "" { t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) } if tt.wantWarning { @@ -245,6 +294,26 @@ func TestHandleFlagz(t *testing.T) { } } +func unmarshalResponse(t *testing.T, contentType string, body []byte, got *v1alpha1.Flagz) { + t.Helper() + switch { + case strings.Contains(contentType, "application/json"): + if err := json.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling JSON response: %v", err) + } + case strings.Contains(contentType, "application/yaml"): + if err := yaml.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling YAML response: %v", err) + } + case strings.Contains(contentType, "application/cbor"): + if err := cbor.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling CBOR response: %v", err) + } + default: + t.Fatalf("unexpected content type: %s", contentType) + } +} + func TestCache(t *testing.T) { fakeFlagName := "test-flag" fakeFlagValue := "test-value" diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go index f9f24dfbd7f..ef4f05da5b4 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go @@ -26,15 +26,18 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor" "k8s.io/component-base/compatibility" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/statusz/negotiate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) var ( @@ -60,7 +63,11 @@ const headerFmt = ` Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. ` -var schemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} +// statuszCodecFactory wraps a CodecFactory to filter out unsupported media types (like protobuf) +// from the supported media types list, so error messages only show actually supported types. +type statuszCodecFactory struct { + serializer.CodecFactory +} type mux interface { Handle(path string, handler http.Handler) @@ -82,8 +89,7 @@ func NewRegistry(effectiveVersion compatibility.EffectiveVersion, opts ...Option func Install(m mux, componentName string, reg statuszRegistry) { scheme := runtime.NewScheme() utilruntime.Must(v1alpha1.AddToScheme(scheme)) - codecFactory := serializer.NewCodecFactory( - scheme, + codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { textSerializer := statuszTextSerializer{componentName, reg} return runtime.SerializerInfo{ @@ -95,8 +101,28 @@ func Install(m mux, componentName string, reg statuszRegistry) { PrettySerializer: textSerializer, } }), - ) - m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, codecFactory, negotiate.StatuszEndpointRestrictions{})) + } + // TODO: remove this explicit check when https://github.com/kubernetes/enhancements/pull/5740 is implemented. + if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) { + codecFactoryOpts = append(codecFactoryOpts, serializer.WithSerializer(cbor.NewSerializerInfo)) + } + + codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) + // Wrap to filter out unsupported media types (e.g., protobuf) + filteredCodecFactory := &statuszCodecFactory{codecFactory} + m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, filteredCodecFactory, negotiate.StatuszEndpointRestrictions{})) +} + +func (f *statuszCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { + allTypes := f.CodecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + for _, info := range allTypes { + if info.MediaType == runtime.ContentTypeProtobuf { + continue + } + filtered = append(filtered, info) + } + return filtered } func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc { @@ -123,9 +149,9 @@ func handleStatusz(componentName string, reg statuszRegistry, serializer runtime var targetGV schema.GroupVersion switch serializerInfo.MediaType { - case "application/json": + case "application/json", "application/yaml", "application/cbor": if mediaType.Convert == nil { - err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for application/json") + err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for %s", serializerInfo.MediaType) utilruntime.HandleError(err) responsewriters.ErrorNegotiated( err, @@ -141,13 +167,11 @@ func handleStatusz(componentName string, reg statuszRegistry, serializer runtime if deprecated { w.Header().Set("Warning", `299 - "This version of the statusz endpoint is deprecated. Please use a newer version."`) } + writeStructuredResponse(obj, serializer, targetGV, restrictions, w, r) case "text/plain": - // Even though text/plain serialization does not use the group/version, - // the serialization machinery expects a non-zero schema.GroupVersion to be passed. - // Passing the zero value can cause errors or unexpected behavior in the negotiation logic. - targetGV = schemeGroupVersion + writePlainTextResponse(obj, serializer, w) default: - err = fmt.Errorf("content negotiation failed: unsupported media type '%s'", serializerInfo.MediaType) + err := fmt.Errorf("unsupported media type: %s/%s", serializerInfo.MediaType, serializerInfo.MediaTypeSubType) utilruntime.HandleError(err) responsewriters.ErrorNegotiated( err, @@ -156,14 +180,10 @@ func handleStatusz(componentName string, reg statuszRegistry, serializer runtime w, r, ) - return } - - writeResponse(obj, serializer, targetGV, restrictions, w, r) } } -// writePlainTextResponse writes the statusz response as text/plain using the registered serializer. func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") // Find the text/plain serializer @@ -185,7 +205,7 @@ func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSer } } -func writeResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.StatuszEndpointRestrictions, w http.ResponseWriter, r *http.Request) { +func writeStructuredResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.StatuszEndpointRestrictions, w http.ResponseWriter, r *http.Request) { responsewriters.WriteObjectNegotiated( serializer, restrictions, diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go index cd0b2a17d45..77938027819 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go @@ -27,7 +27,12 @@ import ( "github.com/google/go-cmp/cmp" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" @@ -58,6 +63,9 @@ Paths: /livez /readyz ` func TestHandleStatusz(t *testing.T) { + // Enable CBOR feature gate for CBOR test case + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + delimiters = []string{":"} fakeStartTime := time.Now() fakeUptime := uptime(fakeStartTime) @@ -68,14 +76,14 @@ func TestHandleStatusz(t *testing.T) { fakeEmulationVersion := parseVersion(t, fakeEvStr) fakeListedPaths := []string{"/livez/poststarthook/peer-discovery-cache-sync", "/livez/post", "/readyz/informer-sync", "/readyz/log", "/readyz/ping"} tests := []struct { - name string - acceptHeader string - componentName string - registry fakeRegistry - wantStatusCode int - wantBody string - wantJSONBody *v1alpha1.Statusz - wantWarning bool + name string + acceptHeader string + componentName string + registry fakeRegistry + wantStatusCode int + wantBody string + wantStructuredBody *v1alpha1.Statusz + wantWarning bool }{ { name: "valid request for text/plain", @@ -100,7 +108,7 @@ func TestHandleStatusz(t *testing.T) { ), }, { - name: "valid request for v1alpha1", + name: "valid request for application/json", acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz", componentName: "test-server", registry: fakeRegistry{ @@ -112,7 +120,7 @@ func TestHandleStatusz(t *testing.T) { deprecated: map[string]bool{}, }, wantStatusCode: http.StatusOK, - wantJSONBody: &v1alpha1.Statusz{ + wantStructuredBody: &v1alpha1.Statusz{ TypeMeta: metav1.TypeMeta{ Kind: Kind, APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), @@ -248,7 +256,7 @@ func TestHandleStatusz(t *testing.T) { }, }, wantStatusCode: http.StatusOK, - wantJSONBody: &v1alpha1.Statusz{ + wantStructuredBody: &v1alpha1.Statusz{ TypeMeta: metav1.TypeMeta{ Kind: Kind, APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), @@ -265,6 +273,64 @@ func TestHandleStatusz(t *testing.T) { }, wantWarning: true, }, + { + name: "valid request for application/yaml", + acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind, + APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + }, + { + name: "valid request for application/cbor", + acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind, + APIVersion: fmt.Sprintf("%s/%s", GroupName, Version), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + }, } for _, tt := range tests { @@ -289,12 +355,10 @@ func TestHandleStatusz(t *testing.T) { } if tt.wantStatusCode == http.StatusOK { - if tt.wantJSONBody != nil { + if tt.wantStructuredBody != nil { var got v1alpha1.Statusz - if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { - t.Fatalf("unexpected error while unmarshalling response: %v", err) - } - if diff := cmp.Diff(*tt.wantJSONBody, got, timeEqual()); diff != "" { + unmarshalResponse(t, w.Header().Get("Content-Type"), w.Body.Bytes(), &got) + if diff := cmp.Diff(*tt.wantStructuredBody, got, timeEqual()); diff != "" { t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) } if tt.wantWarning { @@ -303,8 +367,8 @@ func TestHandleStatusz(t *testing.T) { } } } else { - if diff := cmp.Diff(tt.wantBody, string(w.Body.String())); diff != "" { - t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) + if !strings.Contains(string(w.Body.String()), tt.wantBody) { + t.Errorf("Unexpected response body:\n- want to contain: %s\n- got: %s", tt.wantBody, string(w.Body.String())) } } } @@ -312,6 +376,26 @@ func TestHandleStatusz(t *testing.T) { } } +func unmarshalResponse(t *testing.T, contentType string, body []byte, got *v1alpha1.Statusz) { + t.Helper() + switch { + case strings.Contains(contentType, "application/json"): + if err := json.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling JSON response: %v", err) + } + case strings.Contains(contentType, "application/cbor"): + if err := cbor.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling CBOR response: %v", err) + } + case strings.Contains(contentType, "application/yaml"): + if err := yaml.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling YAML response: %v", err) + } + default: + t.Fatalf("unexpected content type: %s", contentType) + } +} + func parseVersion(t *testing.T, v string) *version.Version { parsed, err := version.ParseMajorMinor(v) if err != nil { diff --git a/test/integration/controlplane/kube_apiserver_test.go b/test/integration/controlplane/kube_apiserver_test.go index ff633c65e9c..654321e1233 100644 --- a/test/integration/controlplane/kube_apiserver_test.go +++ b/test/integration/controlplane/kube_apiserver_test.go @@ -29,6 +29,7 @@ import ( "github.com/google/go-cmp/cmp" "k8s.io/apiextensions-apiserver/test/integration/fixtures" + "sigs.k8s.io/yaml" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -37,8 +38,10 @@ import ( apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/wait" + apiserverfeat "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes" featuregatetesting "k8s.io/component-base/featuregate/testing" @@ -136,6 +139,7 @@ func TestLivezAndReadyz(t *testing.T) { func TestFlagz(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ComponentFlagz, true) + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiserverfeat.CBORServingAndStorage, true) testServerFlags := append(framework.DefaultTestServerFlags(), "--v=2") server := kubeapiservertesting.StartTestServerOrDie(t, nil, testServerFlags, framework.SharedEtcd()) defer server.TearDownFn() @@ -157,11 +161,11 @@ func TestFlagz(t *testing.T) { } for _, tc := range []struct { - name string - acceptHeader string - wantStatus int - wantBodySub string // for text/plain - wantJSON *flagzv1alpha1.Flagz // for application/json + name string + acceptHeader string + wantStatus int + wantBodySub string // for text/plain + wantStructuredBody *flagzv1alpha1.Flagz // for structured responses (JSON/YAML/CBOR) }{ { name: "text plain response", @@ -170,10 +174,10 @@ func TestFlagz(t *testing.T) { wantBodySub: wantBodyStr, }, { - name: "structured json response", - acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz", - wantStatus: http.StatusOK, - wantJSON: wantBodyJSON, + name: "structured json response", + acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyJSON, }, { name: "no accept header (defaults to text)", @@ -208,6 +212,18 @@ func TestFlagz(t *testing.T) { wantStatus: http.StatusOK, wantBodySub: wantBodyStr, }, + { + name: "structured cbor response", + acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Flagz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyJSON, + }, + { + name: "structured yaml response", + acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Flagz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyJSON, + }, } { t.Run(tc.name, func(t *testing.T) { req := client.CoreV1().RESTClient().Get().RequestURI("/flagz") @@ -228,17 +244,15 @@ func TestFlagz(t *testing.T) { t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(raw)) } } - if tc.wantJSON != nil { + if tc.wantStructuredBody != nil { var got flagzv1alpha1.Flagz - if err := json.Unmarshal(raw, &got); err != nil { - t.Fatalf("error unmarshalling JSON: %v", err) - } + unmarshalResponse(t, tc.acceptHeader, raw, &got) // Only check static fields, since others are dynamic - if got.TypeMeta != tc.wantJSON.TypeMeta { - t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantJSON.TypeMeta, got.TypeMeta) + if got.TypeMeta != tc.wantStructuredBody.TypeMeta { + t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantStructuredBody.TypeMeta, got.TypeMeta) } - if got.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name { - t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name) + if got.ObjectMeta.Name != tc.wantStructuredBody.ObjectMeta.Name { + t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantStructuredBody.ObjectMeta.Name, got.ObjectMeta.Name) } if got.Flags["v"] != "2" { t.Errorf("v mismatch: want %q, got %q", "2", got.Flags["v"]) @@ -251,6 +265,7 @@ func TestFlagz(t *testing.T) { func TestStatusz(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ComponentStatusz, true) + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiserverfeat.CBORServingAndStorage, true) server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd()) defer server.TearDownFn() @@ -259,8 +274,8 @@ func TestStatusz(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } - wantBodyStr := "statusz\nWarning: This endpoint is not meant to be machine parseable" - wantBodyJSON := &v1alpha1.Statusz{ + wantBodyString := "statusz\nWarning: This endpoint is not meant to be machine parseable" + wantBodyStructured := &v1alpha1.Statusz{ // StartTime, UptimeSeconds, GoVersion, BinaryVersion, // EmulationVersion, Paths are dynamic, so we only check // static fields @@ -275,29 +290,29 @@ func TestStatusz(t *testing.T) { } for _, tc := range []struct { - name string - acceptHeader string - wantStatus int - wantBodySub string // for text/plain responses - wantJSON *v1alpha1.Statusz // for structured response + name string + acceptHeader string + wantStatus int + wantBodySub string // for text/plain responses + wantStructuredBody *v1alpha1.Statusz // for structured responses (JSON/YAML/CBOR) }{ { name: "text plain response", acceptHeader: "text/plain", wantStatus: http.StatusOK, - wantBodySub: wantBodyStr, + wantBodySub: wantBodyString, }, { - name: "structured json response", - acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz", - wantStatus: http.StatusOK, - wantJSON: wantBodyJSON, + name: "structured json response", + acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyStructured, }, { name: "no accept header (defaults to text)", acceptHeader: "", wantStatus: http.StatusOK, - wantBodySub: wantBodyStr, + wantBodySub: wantBodyString, }, { name: "invalid accept header", @@ -318,13 +333,25 @@ func TestStatusz(t *testing.T) { name: "wildcard accept header", acceptHeader: "*/*", wantStatus: http.StatusOK, - wantBodySub: wantBodyStr, + wantBodySub: wantBodyString, }, { name: "bad json header fall back wildcard", acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*", wantStatus: http.StatusOK, - wantBodySub: wantBodyStr, + wantBodySub: wantBodyString, + }, + { + name: "structured yaml response", + acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Statusz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyStructured, + }, + { + name: "structured cbor response", + acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Statusz", + wantStatus: http.StatusOK, + wantStructuredBody: wantBodyStructured, }, } { t.Run(tc.name, func(t *testing.T) { @@ -346,19 +373,17 @@ func TestStatusz(t *testing.T) { t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(raw)) } } - if tc.wantJSON != nil { + if tc.wantStructuredBody != nil { var got v1alpha1.Statusz - if err := json.Unmarshal(raw, &got); err != nil { - t.Fatalf("error unmarshalling JSON: %v", err) - } + unmarshalResponse(t, tc.acceptHeader, raw, &got) // Only check static fields, since others are dynamic - if got.TypeMeta != tc.wantJSON.TypeMeta { - t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantJSON.TypeMeta, got.TypeMeta) + if got.TypeMeta != tc.wantStructuredBody.TypeMeta { + t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantStructuredBody.TypeMeta, got.TypeMeta) } - if got.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name { - t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name) + if got.ObjectMeta.Name != tc.wantStructuredBody.ObjectMeta.Name { + t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantStructuredBody.ObjectMeta.Name, got.ObjectMeta.Name) } - if diff := cmp.Diff(tc.wantJSON.Paths, got.Paths); diff != "" { + if diff := cmp.Diff(tc.wantStructuredBody.Paths, got.Paths); diff != "" { t.Errorf("Paths mismatch (-want,+got):\n%s", diff) } } @@ -835,3 +860,23 @@ func TestMultiAPIServerNodePortAllocation(t *testing.T) { } } + +func unmarshalResponse(t *testing.T, acceptHeader string, raw []byte, got interface{}) { + t.Helper() + switch { + case strings.Contains(acceptHeader, "application/json"): + if err := json.Unmarshal(raw, got); err != nil { + t.Fatalf("error unmarshalling JSON: %v", err) + } + case strings.Contains(acceptHeader, "application/yaml"): + if err := yaml.Unmarshal(raw, got); err != nil { + t.Fatalf("error unmarshalling YAML: %v", err) + } + case strings.Contains(acceptHeader, "application/cbor"): + if err := cbor.Unmarshal(raw, got); err != nil { + t.Fatalf("error unmarshalling CBOR: %v", err) + } + default: + t.Fatalf("unexpected accept header for structured body: %s", acceptHeader) + } +} From a9dec0cfddef552eb5f0a41a6a012d370839f80b Mon Sep 17 00:00:00 2001 From: Richa Banker Date: Tue, 6 Jan 2026 20:00:58 -0800 Subject: [PATCH 2/2] Explicit checking for supported and unsupported types, add test --- .../apiserver/pkg/server/flagz/flagz.go | 52 ++++++++++++++----- .../apiserver/pkg/server/flagz/flagz_test.go | 21 +++++++- .../apiserver/pkg/server/statusz/statusz.go | 50 +++++++++++++----- .../pkg/server/statusz/statusz_test.go | 17 ++++++ 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go index 235b8bbcd8e..dcdfc682753 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go @@ -46,6 +46,7 @@ const ( // from the supported media types list, so error messages only show actually supported types. type flagzCodecFactory struct { serializer.CodecFactory + supportedMediaTypes []runtime.SerializerInfo } type mux interface { @@ -64,9 +65,19 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) { scheme := runtime.NewScheme() utilruntime.Must(v1alpha1.AddToScheme(scheme)) + filteredCodecFactory, err := newFlagzCodecFactory(scheme, componentName, reg.reader) + if err != nil { + utilruntime.HandleError(err) + } + m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, negotiate.FlagzEndpointRestrictions{})) +} + +// newFlagzCodecFactory creates a codec factory with the standard serializers for flagz, +// filtering out unsupported media types (e.g., protobuf). +func newFlagzCodecFactory(scheme *runtime.Scheme, componentName string, flagReader Reader) (*flagzCodecFactory, error) { codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { - textSerializer := flagzTextSerializer{componentName, reg.reader} + textSerializer := flagzTextSerializer{componentName, flagReader} return runtime.SerializerInfo{ MediaType: "text/plain", MediaTypeType: "text", @@ -83,21 +94,36 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) { } codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) - // Wrap to filter out unsupported media types (e.g., protobuf) - filteredCodecFactory := &flagzCodecFactory{codecFactory} - m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, negotiate.FlagzEndpointRestrictions{})) + allTypes := codecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + + var unknownTypes []string + for _, info := range allTypes { + switch info.MediaType { + // Supported media types + case "text/plain", runtime.ContentTypeJSON, runtime.ContentTypeYAML, runtime.ContentTypeCBOR: + filtered = append(filtered, info) + // Unsupported media types + case runtime.ContentTypeProtobuf: + continue + default: + unknownTypes = append(unknownTypes, info.MediaType) + } + } + + var err error + if len(unknownTypes) > 0 { + err = fmt.Errorf("flagz: unknown media type(s) %v, excluding from supported types", unknownTypes) + } + + return &flagzCodecFactory{ + CodecFactory: codecFactory, + supportedMediaTypes: filtered, + }, err } func (f *flagzCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { - allTypes := f.CodecFactory.SupportedMediaTypes() - filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) - for _, info := range allTypes { - if info.MediaType == runtime.ContentTypeProtobuf { - continue - } - filtered = append(filtered, info) - } - return filtered + return f.supportedMediaTypes } func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc { diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go index 68b303ff712..075f32b4296 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go @@ -27,7 +27,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + "k8s.io/apimachinery/pkg/runtime" + cbordirect "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/features" v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -306,7 +308,7 @@ func unmarshalResponse(t *testing.T, contentType string, body []byte, got *v1alp t.Fatalf("unexpected error while unmarshalling YAML response: %v", err) } case strings.Contains(contentType, "application/cbor"): - if err := cbor.Unmarshal(body, got); err != nil { + if err := cbordirect.Unmarshal(body, got); err != nil { t.Fatalf("unexpected error while unmarshalling CBOR response: %v", err) } default: @@ -360,3 +362,18 @@ func TestCache(t *testing.T) { t.Errorf("Unexpected diff on cached response (-want,+got):\n%s", diff) } } + +// TestNewFlagzCodecFactory ensures all media types in the codec factory +// are explicitly handled. If this test fails, a new media type was added +// to the codec factory and needs to be explicitly added to the supported +// or unsupported list in newFlagzCodecFactory. +func TestNewFlagzCodecFactory(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + + _, err := newFlagzCodecFactory(scheme, "", nil) + if err != nil { + t.Fatalf("unknown media type(s) detected - update newFlagzCodecFactory to explicitly handle them: %v", err) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go index ef4f05da5b4..aed4bf6a20a 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz.go @@ -67,6 +67,7 @@ Warning: This endpoint is not meant to be machine parseable, has no formatting c // from the supported media types list, so error messages only show actually supported types. type statuszCodecFactory struct { serializer.CodecFactory + supportedMediaTypes []runtime.SerializerInfo } type mux interface { @@ -89,6 +90,16 @@ func NewRegistry(effectiveVersion compatibility.EffectiveVersion, opts ...Option func Install(m mux, componentName string, reg statuszRegistry) { scheme := runtime.NewScheme() utilruntime.Must(v1alpha1.AddToScheme(scheme)) + filteredCodecFactory, err := newStatuszCodecFactory(scheme, componentName, reg) + if err != nil { + utilruntime.HandleError(err) + } + m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, filteredCodecFactory, negotiate.StatuszEndpointRestrictions{})) +} + +// newStatuszCodecFactory creates a codec factory with the standard serializers for statusz, +// filtering out unsupported media types (e.g., protobuf). +func newStatuszCodecFactory(scheme *runtime.Scheme, componentName string, reg statuszRegistry) (*statuszCodecFactory, error) { codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { textSerializer := statuszTextSerializer{componentName, reg} @@ -108,21 +119,36 @@ func Install(m mux, componentName string, reg statuszRegistry) { } codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) - // Wrap to filter out unsupported media types (e.g., protobuf) - filteredCodecFactory := &statuszCodecFactory{codecFactory} - m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, filteredCodecFactory, negotiate.StatuszEndpointRestrictions{})) + allTypes := codecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + + var unknownTypes []string + for _, info := range allTypes { + switch info.MediaType { + // Supported media types + case "text/plain", runtime.ContentTypeJSON, runtime.ContentTypeYAML, runtime.ContentTypeCBOR: + filtered = append(filtered, info) + // Unsupported media types + case runtime.ContentTypeProtobuf: + continue + default: + unknownTypes = append(unknownTypes, info.MediaType) + } + } + + var err error + if len(unknownTypes) > 0 { + err = fmt.Errorf("statusz: unknown media type(s) %v, excluding from supported types", unknownTypes) + } + + return &statuszCodecFactory{ + CodecFactory: codecFactory, + supportedMediaTypes: filtered, + }, err } func (f *statuszCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { - allTypes := f.CodecFactory.SupportedMediaTypes() - filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) - for _, info := range allTypes { - if info.MediaType == runtime.ContentTypeProtobuf { - continue - } - filtered = append(filtered, info) - } - return filtered + return f.supportedMediaTypes } func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc { diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go index 77938027819..9b0b5515e2b 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/statusz_test.go @@ -27,7 +27,9 @@ import ( "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/runtime" cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apiserver/pkg/features" @@ -443,3 +445,18 @@ func timeEqual() cmp.Option { return expectedTime.Truncate(time.Second).Equal(actualTime.Truncate(time.Second)) }) } + +// TestNewStatuszCodecFactory ensures all media types in the codec factory +// are explicitly handled. If this test fails, a new media type was added +// to the codec factory and needs to be explicitly added to the supported +// or unsupported list in newStatuszCodecFactory. +func TestNewStatuszCodecFactory(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + + _, err := newStatuszCodecFactory(scheme, "", nil) + if err != nil { + t.Fatalf("unknown media type(s) detected - update newStatuszCodecFactory to explicitly handle them: %v", err) + } +}