diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 047b78f070d..f2c1bee80a4 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -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{ diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go index ab7a48c2106..42d5fcef536 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/doc.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/doc.go new file mode 100644 index 00000000000..bc8cffc0a0e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/doc.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/register.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/register.go new file mode 100644 index 00000000000..f66acc7dd6c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/register.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/types.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/types.go new file mode 100644 index 00000000000..403436fdcf3 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/types.go @@ -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"` +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..1d5e52605cc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go new file mode 100644 index 00000000000..d475a0aa64a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go @@ -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" +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go index 95d38744add..cb6df7b5d64 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go @@ -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 := ®istry{ 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, diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go index 26489dc28fe..6c34848dd24 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go @@ -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: ®istry{ 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: ®istry{ 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: ®istry{ 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: ®istry{ 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 { diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go index f3edc8f3c1a..ec2d28e67c1 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/testing/testing.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/testing/testing.go new file mode 100644 index 00000000000..8b3edd65b32 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/testing/testing.go @@ -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 +} diff --git a/test/integration/controlplane/kube_apiserver_test.go b/test/integration/controlplane/kube_apiserver_test.go index bde20d19004..c994b7cdf32 100644 --- a/test/integration/controlplane/kube_apiserver_test.go +++ b/test/integration/controlplane/kube_apiserver_test.go @@ -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) - } -} diff --git a/test/integration/scheduler/serving/endpoints_test.go b/test/integration/scheduler/serving/endpoints_test.go index d3365ca1179..9260dc73192 100644 --- a/test/integration/scheduler/serving/endpoints_test.go +++ b/test/integration/scheduler/serving/endpoints_test.go @@ -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) } } }) diff --git a/test/integration/serving/serving_test.go b/test/integration/serving/serving_test.go index 53a56454c7d..38a3e8d440f 100644 --- a/test/integration/serving/serving_test.go +++ b/test/integration/serving/serving_test.go @@ -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) } } })