graduate config.k8s.io.v1alpha1.flagz to beta

This commit is contained in:
Richa Banker 2026-02-20 17:32:55 -08:00
parent 4dd6454b02
commit adbf7dee82
14 changed files with 660 additions and 190 deletions

View file

@ -89,8 +89,9 @@ import (
version "k8s.io/apimachinery/pkg/version"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
apiv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
apiv1beta1 "k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
statuszapiv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
apiv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
statuszapiv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
configv1alpha1 "k8s.io/cloud-provider/config/v1alpha1"
@ -1318,8 +1319,9 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
auditv1.PolicyList{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyList(ref),
auditv1.PolicyRule{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyRule(ref),
apiv1alpha1.Flagz{}.OpenAPIModelName(): schema_server_flagz_api_v1alpha1_Flagz(ref),
apiv1beta1.Flagz{}.OpenAPIModelName(): schema_server_flagz_api_v1beta1_Flagz(ref),
statuszapiv1alpha1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1alpha1_Statusz(ref),
apiv1beta1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1beta1_Statusz(ref),
statuszapiv1beta1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1beta1_Statusz(ref),
clientauthenticationv1.Cluster{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_Cluster(ref),
clientauthenticationv1.ExecCredential{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_ExecCredential(ref),
clientauthenticationv1.ExecCredentialSpec{}.OpenAPIModelName(): schema_pkg_apis_clientauthentication_v1_ExecCredentialSpec(ref),
@ -63962,6 +63964,58 @@ func schema_server_flagz_api_v1alpha1_Flagz(ref common.ReferenceCallback) common
}
}
func schema_server_flagz_api_v1beta1_Flagz(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Flagz is the structured response for the /flagz endpoint.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard object's metadata.",
Default: map[string]interface{}{},
Ref: ref(metav1.ObjectMeta{}.OpenAPIModelName()),
},
},
"flags": {
SchemaProps: spec.SchemaProps{
Description: "Flags contains the command-line flags and their values. The keys are the flag names and the values are the flag values, possibly with confidential values redacted.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
metav1.ObjectMeta{}.OpenAPIModelName()},
}
}
func schema_server_statusz_api_v1alpha1_Statusz(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View file

@ -18,5 +18,5 @@ limitations under the License.
// +k8s:openapi-gen=true
// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.flagz.api.v1alpha1
// Package v1alpha1 contains API Schema definitions for the zpages v1alpha1 API group
// Package v1alpha1 contains API Schema definitions for the flagz v1alpha1 API group
package v1alpha1

View file

@ -0,0 +1,22 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.flagz.api.v1beta1
// Package v1beta1 contains API Schema definitions for the flagz v1beta1 API group
package v1beta1

View file

@ -0,0 +1,47 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
const (
GroupName = "config.k8s.io"
Version = "v1beta1"
)
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version}
var (
// SchemeBuilder initializes a scheme builder
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that adds this group's types to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Flagz{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View file

@ -0,0 +1,37 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Flagz is the structured response for the /flagz endpoint.
type Flagz struct {
// TypeMeta is the type metadata for the object.
metav1.TypeMeta `json:",inline"`
// Standard object's metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Flags contains the command-line flags and their values.
// The keys are the flag names and the values are the flag values,
// possibly with confidential values redacted.
// +optional
Flags map[string]string `json:"flags,omitempty"`
}

View file

@ -0,0 +1,59 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1beta1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Flagz) DeepCopyInto(out *Flagz) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Flags != nil {
in, out := &in.Flags, &out.Flags
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagz.
func (in *Flagz) DeepCopy() *Flagz {
if in == nil {
return nil
}
out := new(Flagz)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Flagz) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View file

@ -0,0 +1,27 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by openapi-gen. DO NOT EDIT.
package v1beta1
// OpenAPIModelName returns the OpenAPI model name for this type.
func (in Flagz) OpenAPIModelName() string {
return "io.k8s.apiserver.pkg.server.flagz.api.v1beta1.Flagz"
}

View file

@ -34,18 +34,23 @@ import (
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/apiserver/pkg/features"
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
"k8s.io/apiserver/pkg/server/flagz/negotiate"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
const (
DefaultFlagzPath = "/flagz"
Kind = "Flagz"
GroupName = "config.k8s.io"
Version = "v1alpha1"
var (
v1alpha1FlagzKind = v1alpha1.SchemeGroupVersion.WithKind("Flagz")
v1beta1FlagzKind = v1beta1.SchemeGroupVersion.WithKind("Flagz")
recognizedStructuredKinds = map[schema.GroupVersionKind]bool{
v1alpha1FlagzKind: true,
v1beta1FlagzKind: true,
}
)
const DefaultFlagzPath = "/flagz"
// 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 {
@ -61,7 +66,7 @@ type mux interface {
func Install(m mux, componentName string, flagReader Reader, opts ...Option) {
reg := &registry{
reader: flagReader,
deprecatedVersionsMap: map[string]bool{},
deprecatedVersionsMap: map[string]bool{"v1alpha1": true},
}
for _, opt := range opts {
opt(reg)
@ -69,11 +74,15 @@ func Install(m mux, componentName string, flagReader Reader, opts ...Option) {
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
filteredCodecFactory, err := newFlagzCodecFactory(scheme, componentName, reg.reader)
if err != nil {
utilruntime.HandleError(err)
}
m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, negotiate.FlagzEndpointRestrictions{}))
restrictions := negotiate.FlagzEndpointRestrictions{
RecognizedStructuredKinds: recognizedStructuredKinds,
}
m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, restrictions))
}
// newFlagzCodecFactory creates a codec factory with the standard serializers for flagz,
@ -157,10 +166,9 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia
delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp))
}()
obj := flagz(componentName, reg.reader)
acceptHeader := r.Header.Get("Accept")
if strings.TrimSpace(acceptHeader) == "" {
writePlainTextResponse(obj, serializer, w, reg)
writePlainTextResponse(v1beta1Flagz(componentName, reg.reader), serializer, w, reg)
return
}
@ -177,7 +185,6 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia
return
}
var targetGV schema.GroupVersion
switch serializerInfo.MediaType {
case "application/json", "application/yaml", "application/cbor":
if mediaType.Convert == nil {
@ -192,18 +199,15 @@ func handleFlagz(componentName string, reg *registry, serializer runtime.Negotia
)
return
}
// Set group, version, and deprecated from the negotiated target so
// the deferred MonitorRequest records the actual requested API version.
targetGV = mediaType.Convert.GroupVersion()
group = targetGV.Group
version = targetGV.Version
deprecated = reg.deprecatedVersions()[targetGV.Version]
group = mediaType.Convert.Group
version = mediaType.Convert.Version
deprecated = reg.deprecatedVersions()[version]
if deprecated {
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)
handleStructuredResponse(w, r, componentName, reg, serializer, restrictions, mediaType)
case "text/plain":
writePlainTextResponse(obj, serializer, w, reg)
writePlainTextResponse(v1beta1Flagz(componentName, reg.reader), serializer, w, reg)
default:
err := fmt.Errorf("unsupported media type: %s/%s", serializerInfo.MediaType, serializerInfo.MediaTypeSubType)
utilruntime.HandleError(err)
@ -268,12 +272,45 @@ func writeStructuredResponse(obj runtime.Object, serializer runtime.NegotiatedSe
)
}
func flagz(componentName string, flagReader Reader) *v1alpha1.Flagz {
func handleStructuredResponse(w http.ResponseWriter, r *http.Request, componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions, mediaType negotiation.MediaTypeOptions) {
switch *mediaType.Convert {
case v1alpha1FlagzKind:
writeStructuredResponse(v1alpha1Flagz(componentName, reg.reader), serializer, v1alpha1FlagzKind.GroupVersion(), restrictions, w, r)
case v1beta1FlagzKind:
writeStructuredResponse(v1beta1Flagz(componentName, reg.reader), serializer, v1beta1FlagzKind.GroupVersion(), restrictions, w, r)
default:
err := fmt.Errorf("unsupported media type: %s", mediaType.Convert.String())
utilruntime.HandleError(err)
responsewriters.ErrorNegotiated(
err,
serializer,
schema.GroupVersion{},
w,
r,
)
}
}
func v1alpha1Flagz(componentName string, flagReader Reader) *v1alpha1.Flagz {
flags := flagReader.GetFlagz()
return &v1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
Kind: v1alpha1FlagzKind.Kind,
APIVersion: v1alpha1FlagzKind.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName,
},
Flags: flags,
}
}
func v1beta1Flagz(componentName string, flagReader Reader) *v1beta1.Flagz {
flags := flagReader.GetFlagz()
return &v1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: v1beta1FlagzKind.Kind,
APIVersion: v1beta1FlagzKind.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName,

View file

@ -32,7 +32,8 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/features"
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
cliflag "k8s.io/component-base/cli/flag"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@ -68,7 +69,7 @@ func TestHandleFlagz(t *testing.T) {
registry *registry
wantStatusCode int
wantBody string
wantStructuredBody *v1alpha1.Flagz
wantStructuredBody interface{}
wantWarning bool
}{
{
@ -87,17 +88,17 @@ func TestHandleFlagz(t *testing.T) {
},
{
name: "valid request for application/json",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantStructuredBody: &v1alpha1.Flagz{
wantStructuredBody: &v1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
@ -118,8 +119,8 @@ func TestHandleFlagz(t *testing.T) {
wantStatusCode: http.StatusOK,
wantStructuredBody: &v1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
@ -132,17 +133,17 @@ func TestHandleFlagz(t *testing.T) {
},
{
name: "valid request for application/yaml",
acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantStructuredBody: &v1alpha1.Flagz{
wantStructuredBody: &v1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
@ -154,17 +155,17 @@ func TestHandleFlagz(t *testing.T) {
},
{
name: "valid request for application/cbor",
acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantStructuredBody: &v1alpha1.Flagz{
wantStructuredBody: &v1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
@ -238,7 +239,7 @@ func TestHandleFlagz(t *testing.T) {
},
{
name: "unsupported application/json with missing params",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
@ -281,9 +282,18 @@ func TestHandleFlagz(t *testing.T) {
if tt.wantStatusCode == http.StatusOK {
if tt.wantStructuredBody != nil {
var got v1alpha1.Flagz
unmarshalResponse(t, w.Header().Get("Content-Type"), w.Body.Bytes(), &got)
if diff := cmp.Diff(*tt.wantStructuredBody, got); diff != "" {
var got interface{}
switch tt.wantStructuredBody.(type) {
case *v1alpha1.Flagz:
got = &v1alpha1.Flagz{}
case *v1beta1.Flagz:
got = &v1beta1.Flagz{}
default:
t.Fatalf("unexpected type for wantStructuredBody: %T", tt.wantStructuredBody)
}
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 {
@ -299,7 +309,7 @@ func TestHandleFlagz(t *testing.T) {
}
}
func unmarshalResponse(t *testing.T, contentType string, body []byte, got *v1alpha1.Flagz) {
func unmarshalResponse(t *testing.T, contentType string, body []byte, got interface{}) {
t.Helper()
switch {
case strings.Contains(contentType, "application/json"):
@ -374,6 +384,7 @@ func TestNewFlagzCodecFactory(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true)
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
_, err := newFlagzCodecFactory(scheme, "", nil)
if err != nil {

View file

@ -20,16 +20,21 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// FlagzEndpointRestrictions implements content negotiation restrictions for the z-pages.
// FlagzEndpointRestrictions implements content negotiation restrictions for the flagz endpoint.
// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses.
type FlagzEndpointRestrictions struct{}
type FlagzEndpointRestrictions struct {
RecognizedStructuredKinds map[schema.GroupVersionKind]bool
}
// AllowsMediaTypeTransform checks if the provided GVK is supported for structured z-page responses.
func (FlagzEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
// AllowsMediaTypeTransform checks if the provided GVK is supported for structured flagz responses.
func (f FlagzEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
if mimeType == "text" && mimeSubType == "plain" {
return gvk == nil
}
return isStructured(gvk)
if gvk != nil {
return f.RecognizedStructuredKinds[*gvk]
}
return false
}
func (FlagzEndpointRestrictions) AllowsServerVersion(string) bool {
@ -39,15 +44,3 @@ func (FlagzEndpointRestrictions) AllowsServerVersion(string) bool {
func (FlagzEndpointRestrictions) AllowsStreamSchema(s string) bool {
return false
}
func isStructured(gvk *schema.GroupVersionKind) bool {
if gvk != nil {
if gvk.Group == "config.k8s.io" && gvk.Version == "v1alpha1" {
if gvk.Kind == "Flagz" {
return true
}
}
}
return false
}

View file

@ -0,0 +1,120 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package testing
import (
"encoding/json"
"strings"
"testing"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func VerifyStructuredResponse(t *testing.T, acceptHeader string, body []byte, warnings []string, want interface{}, wantDeprecationHeader bool) {
t.Helper()
unmarshal := unmarshalFunc(t, acceptHeader)
wantTypeMeta, wantName, wantFlags := wantFields(t, want)
gotTypeMeta, gotName, gotFlags := gotFields(t, unmarshal, body, wantTypeMeta.APIVersion)
if gotName != wantName {
t.Errorf("name mismatch: got %q, want %q", gotName, wantName)
}
if gotTypeMeta != wantTypeMeta {
t.Errorf("type meta mismatch: got %v, want %v", gotTypeMeta, wantTypeMeta)
}
for k, v := range wantFlags {
gotV, ok := gotFlags[k]
if !ok {
t.Errorf("missing flag %q", k)
continue
}
if gotV != v {
t.Errorf("flag %q match: got %q, want %q", k, gotV, v)
}
}
foundWarning := false
for _, w := range warnings {
if strings.Contains(w, "deprecated") {
foundWarning = true
break
}
}
if foundWarning != wantDeprecationHeader {
t.Errorf("deprecation header mismatch: got %v, want %v", foundWarning, wantDeprecationHeader)
}
}
func unmarshalFunc(t *testing.T, acceptHeader string) func([]byte, interface{}) error {
switch {
case strings.Contains(acceptHeader, "application/json"):
return json.Unmarshal
case strings.Contains(acceptHeader, "application/yaml"):
return yaml.Unmarshal
case strings.Contains(acceptHeader, "application/cbor"):
return cbor.Unmarshal
default:
t.Fatalf("unexpected Accept header: %q", acceptHeader)
}
return nil
}
func wantFields(t *testing.T, want interface{}) (metav1.TypeMeta, string, map[string]string) {
t.Helper()
switch w := want.(type) {
case *v1alpha1.Flagz:
return w.TypeMeta, w.Name, w.Flags
case *v1beta1.Flagz:
return w.TypeMeta, w.Name, w.Flags
default:
t.Fatalf("unexpected type for want: %T", want)
return metav1.TypeMeta{}, "", nil
}
}
func gotFields(t *testing.T, unmarshal func([]byte, interface{}) error, body []byte, apiVersion string) (metav1.TypeMeta, string, map[string]string) {
var gotName string
var gotTypeMeta metav1.TypeMeta
var gotFlags map[string]string
switch apiVersion {
case "config.k8s.io/v1alpha1":
var got v1alpha1.Flagz
if err := unmarshal(body, &got); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
gotName = got.Name
gotTypeMeta = got.TypeMeta
gotFlags = got.Flags
case "config.k8s.io/v1beta1":
var got v1beta1.Flagz
if err := unmarshal(body, &got); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
gotName = got.Name
gotTypeMeta = got.TypeMeta
gotFlags = got.Flags
default:
t.Fatalf("unexpected API version: %q", apiVersion)
}
return gotTypeMeta, gotName, gotFlags
}

View file

@ -28,7 +28,6 @@ import (
"time"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"sigs.k8s.io/yaml"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -37,10 +36,15 @@ 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"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
flagzv1beta1 "k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
flagztesting "k8s.io/apiserver/pkg/server/flagz/testing"
statuszv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
statuszv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
statusztesting "k8s.io/apiserver/pkg/server/statusz/testing"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@ -50,11 +54,6 @@ import (
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
statuszv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
statuszv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
statusztesting "k8s.io/apiserver/pkg/server/statusz/testing"
)
const (
@ -151,7 +150,7 @@ func TestFlagz(t *testing.T) {
}
wantBodyStr := "apiserver flagz\nWarning: This endpoint is not meant to be machine parseable"
wantBodyJSON := &flagzv1alpha1.Flagz{
wantBodyAlpha := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
@ -159,32 +158,48 @@ func TestFlagz(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "apiserver",
},
Flags: map[string]string{
"v": "2",
},
}
wantBodyBeta := &flagzv1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "apiserver",
},
Flags: map[string]string{
"v": "2",
},
}
for _, tc := range []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantStructuredBody *flagzv1alpha1.Flagz // for structured responses (JSON/YAML/CBOR)
name string
acceptHeader string
wantStatus int
wantBodyText string // for text/plain
wantBodyStructured interface{} // for structured responses (JSON/YAML/CBOR)
wantDeprecationHeader bool
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodyText: wantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantStructuredBody: wantBodyJSON,
wantBodyStructured: wantBodyBeta,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodyText: wantBodyStr,
},
{
name: "invalid accept header",
@ -198,32 +213,46 @@ func TestFlagz(t *testing.T) {
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodyText: wantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodyText: wantBodyStr,
},
{
name: "structured cbor response",
acceptHeader: "application/cbor;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantStructuredBody: wantBodyJSON,
wantBodyStructured: wantBodyBeta,
},
{
name: "structured yaml response",
acceptHeader: "application/yaml;v=v1alpha1;g=config.k8s.io;as=Flagz",
acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantStructuredBody: wantBodyJSON,
wantBodyStructured: wantBodyBeta,
},
{
name: "alpha specified before beta, should show warning",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz,application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: wantBodyAlpha,
wantDeprecationHeader: true,
},
{
name: "beta specified before alpha, no warning",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz,application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: wantBodyBeta,
wantDeprecationHeader: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
@ -240,24 +269,17 @@ func TestFlagz(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
if tc.wantStatus == http.StatusOK {
if tc.wantBodySub != "" {
if !bytes.Contains(raw, []byte(tc.wantBodySub)) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(raw))
if tc.wantBodyText != "" {
if !bytes.Contains(raw, []byte(tc.wantBodyText)) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodyText, string(raw))
}
}
if tc.wantStructuredBody != nil {
var got flagzv1alpha1.Flagz
unmarshalResponse(t, tc.acceptHeader, raw, &got)
// Only check static fields, since others are dynamic
if got.TypeMeta != tc.wantStructuredBody.TypeMeta {
t.Errorf("TypeMeta mismatch: want %+v, got %+v", tc.wantStructuredBody.TypeMeta, got.TypeMeta)
}
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"])
}
var warnings []string
for _, w := range res.Warnings() {
warnings = append(warnings, w.Text)
}
if tc.wantBodyStructured != nil {
flagztesting.VerifyStructuredResponse(t, tc.acceptHeader, raw, warnings, tc.wantBodyStructured, tc.wantDeprecationHeader)
}
}
})
@ -878,23 +900,3 @@ 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)
}
}

View file

@ -19,7 +19,6 @@ package serving
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
@ -32,6 +31,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiserverfeat "k8s.io/apiserver/pkg/features"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
flagzv1beta1 "k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
flagztesting "k8s.io/apiserver/pkg/server/flagz/testing"
statuszv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
statuszv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
statusztesting "k8s.io/apiserver/pkg/server/statusz/testing"
@ -270,7 +271,7 @@ func TestSchedulerZPages(t *testing.T) {
}
flagzWantBodyStr := "kube-scheduler flagz\nWarning: This endpoint is not meant to be machine parseable"
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
flagzWantBodyStructuredAlpha := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
@ -278,6 +279,21 @@ func TestSchedulerZPages(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "kube-scheduler",
},
Flags: map[string]string{
"leader-elect-resource-name": "kube-scheduler",
},
}
flagzWantBodyStructuredBeta := &flagzv1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-scheduler",
},
Flags: map[string]string{
"leader-elect-resource-name": "kube-scheduler",
},
}
statuszTestCases := []struct {
@ -362,29 +378,30 @@ func TestSchedulerZPages(t *testing.T) {
}
flagzTestCases := []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantJSON *flagzv1alpha1.Flagz // for structured json
name string
acceptHeader string
wantStatus int
wantBodyText string
wantBodyStructured interface{}
wantDeprecationHeader bool
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantJSON: flagzWantBodyJSON,
name: "structured json response",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyStr,
},
{
name: "invalid accept header",
@ -398,20 +415,46 @@ func TestSchedulerZPages(t *testing.T) {
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyStr,
},
{
name: "structured cbor response",
acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "structured yaml response",
acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "alpha specified before beta, should show warning",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz,application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredAlpha,
wantDeprecationHeader: true,
},
{
name: "beta specified before alpha, no warning",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz,application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
wantDeprecationHeader: false,
},
}
@ -482,23 +525,14 @@ func TestSchedulerZPages(t *testing.T) {
}
if tc.wantStatus == http.StatusOK {
if tc.wantBodySub != "" {
if !strings.Contains(string(body), tc.wantBodySub) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(body))
if tc.wantBodyText != "" {
if !strings.Contains(string(body), tc.wantBodyText) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodyText, string(body))
}
}
if tc.wantJSON != nil {
var got flagzv1alpha1.Flagz
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("error unmarshalling JSON: %v", err)
}
// 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.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name {
t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name)
}
if tc.wantBodyStructured != nil {
warnings := append([]string{}, r.Header.Values("Warning")...)
flagztesting.VerifyStructuredResponse(t, tc.acceptHeader, body, warnings, tc.wantBodyStructured, tc.wantDeprecationHeader)
}
}
})

View file

@ -20,7 +20,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net"
@ -32,16 +31,17 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
statusztesting "k8s.io/apiserver/pkg/server/statusz/testing"
"k8s.io/client-go/tools/cache"
apiserverfeat "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
flagzv1beta1 "k8s.io/apiserver/pkg/server/flagz/api/v1beta1"
flagztesting "k8s.io/apiserver/pkg/server/flagz/testing"
"k8s.io/apiserver/pkg/server/options"
statuszv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
statuszv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1"
statusztesting "k8s.io/apiserver/pkg/server/statusz/testing"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
cloudprovider "k8s.io/cloud-provider"
cloudctrlmgrtesting "k8s.io/cloud-provider/app/testing"
"k8s.io/cloud-provider/fake"
@ -381,8 +381,8 @@ users:
Paths: []string{"/configz", "/flagz", "/healthz", "/metrics"},
}
flagzWantBodyStr := "kube-controller-manager flagz\nWarning: This endpoint is not meant to be machine parseable"
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
flagzWantBodyText := "kube-controller-manager flagz\nWarning: This endpoint is not meant to be machine parseable"
flagzWantBodyStructuredAlpha := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
@ -391,6 +391,15 @@ users:
Name: "kube-controller-manager",
},
}
flagzWantBodyStructuredBeta := &flagzv1beta1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-controller-manager",
},
}
statuszTestCases := []struct {
name string
@ -474,29 +483,30 @@ users:
}
flagzTestCases := []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantJSON *flagzv1alpha1.Flagz // for structured json
name string
acceptHeader string
wantStatus int
wantBodyText string // for text/plain
wantBodyStructured interface{} // for structured json
wantDeprecationHeader bool
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyText,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantJSON: flagzWantBodyJSON,
name: "structured json response",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyText,
},
{
name: "invalid accept header",
@ -510,20 +520,46 @@ users:
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyText,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
wantBodyText: flagzWantBodyText,
},
{
name: "structured cbor response",
acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "structured yaml response",
acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
},
{
name: "alpha specified before beta, should show warning",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz,application/json;v=v1beta1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredAlpha,
wantDeprecationHeader: true,
},
{
name: "beta specified before alpha, no warning",
acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz,application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantBodyStructured: flagzWantBodyStructuredBeta,
wantDeprecationHeader: false,
},
}
@ -640,23 +676,14 @@ users:
}
if tc.wantStatus == http.StatusOK {
if tc.wantBodySub != "" {
if !strings.Contains(string(body), tc.wantBodySub) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodySub, string(body))
if tc.wantBodyText != "" {
if !strings.Contains(string(body), tc.wantBodyText) {
t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodyText, string(body))
}
}
if tc.wantJSON != nil {
var got flagzv1alpha1.Flagz
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("error unmarshalling JSON: %v", err)
}
// 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.ObjectMeta.Name != tc.wantJSON.ObjectMeta.Name {
t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", tc.wantJSON.ObjectMeta.Name, got.ObjectMeta.Name)
}
if tc.wantBodyStructured != nil {
warnings := append([]string{}, r.Header.Values("Warning")...)
flagztesting.VerifyStructuredResponse(t, tc.acceptHeader, body, warnings, tc.wantBodyStructured, tc.wantDeprecationHeader)
}
}
})