Merge pull request #135309 from richabanker/zpages

Enhance content negotiation for zpages
This commit is contained in:
Kubernetes Prow Robot
2026-01-22 03:17:25 +05:30
committed by GitHub
5 changed files with 434 additions and 105 deletions

View File

@@ -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,13 @@ 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
supportedMediaTypes []runtime.SerializerInfo
}
type mux interface {
Handle(path string, handler http.Handler)
}
@@ -55,10 +65,19 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) {
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
codecFactory := serializer.NewCodecFactory(
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",
@@ -68,8 +87,43 @@ 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...)
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 {
return f.supportedMediaTypes
}
func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc {
@@ -96,9 +150,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 +167,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 +180,7 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia
w,
r,
)
return
}
writeResponse(obj, serializer, targetGV, restrictions, w, r)
}
}
@@ -170,7 +221,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,

View File

@@ -27,8 +27,15 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
cliflag "k8s.io/component-base/cli/flag"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"sigs.k8s.io/yaml"
)
const wantTmpl = `
@@ -37,6 +44,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 +59,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 +83,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: &registry{
@@ -82,7 +91,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 +113,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 +127,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: &registry{
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: &registry{
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 +277,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 +296,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 := cbordirect.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"
@@ -291,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)
}
}

View File

@@ -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,12 @@ 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
supportedMediaTypes []runtime.SerializerInfo
}
type mux interface {
Handle(path string, handler http.Handler)
@@ -82,8 +90,17 @@ 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,
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}
return runtime.SerializerInfo{
@@ -95,8 +112,43 @@ 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...)
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 {
return f.supportedMediaTypes
}
func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc {
@@ -123,9 +175,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 +193,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 +206,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 +231,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,

View File

@@ -27,7 +27,14 @@ 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"
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 +65,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 +78,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 +110,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 +122,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 +258,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 +275,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 +357,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 +369,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 +378,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 {
@@ -359,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)
}
}

View File

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