mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-17 20:00:07 +00:00
Merge pull request #135309 from richabanker/zpages
Enhance content negotiation for zpages
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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: ®istry{
|
||||
@@ -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: ®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 +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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user