Merge pull request #134995 from yongruilin/flagz-kk-structure

[KEP-4828] Flagz versioned structured response
This commit is contained in:
Kubernetes Prow Robot 2025-11-04 19:02:04 -08:00 committed by GitHub
commit 5fd9cefd95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1311 additions and 447 deletions

View file

@ -34,6 +34,7 @@ import (
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/egressselector"
"k8s.io/apiserver/pkg/server/flagz"
serverstorage "k8s.io/apiserver/pkg/server/storage"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/notfoundhandler"
@ -51,7 +52,6 @@ import (
"k8s.io/component-base/term"
utilversion "k8s.io/component-base/version"
"k8s.io/component-base/version/verflag"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"

View file

@ -45,6 +45,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/flagz"
serveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/apiserver/pkg/storageversion"
@ -60,7 +61,6 @@ import (
featuregatetesting "k8s.io/component-base/featuregate/testing"
logsapi "k8s.io/component-base/logs/api/v1"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
"k8s.io/kube-aggregator/pkg/apiserver"
"k8s.io/kubernetes/pkg/features"

View file

@ -18,11 +18,11 @@ package config
import (
apiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/flagz"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
basecompatibility "k8s.io/component-base/compatibility"
"k8s.io/component-base/zpages/flagz"
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
"time"
)

View file

@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/mux"
"k8s.io/apiserver/pkg/server/statusz"
@ -67,7 +68,6 @@ import (
utilversion "k8s.io/component-base/version"
"k8s.io/component-base/version/verflag"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
genericcontrollermanager "k8s.io/controller-manager/app"
"k8s.io/controller-manager/controller"
"k8s.io/controller-manager/pkg/clientbuilder"

View file

@ -27,6 +27,7 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/server/flagz"
apiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/util/compatibility"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -45,7 +46,6 @@ import (
"k8s.io/component-base/logs"
logsapi "k8s.io/component-base/logs/api/v1"
"k8s.io/component-base/metrics"
"k8s.io/component-base/zpages/flagz"
cmoptions "k8s.io/controller-manager/options"
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
kubecontrollerconfig "k8s.io/kubernetes/cmd/kube-controller-manager/app/config"

View file

@ -28,11 +28,11 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/server/flagz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
cliflag "k8s.io/component-base/cli/flag"
logsapi "k8s.io/component-base/logs/api/v1"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
"k8s.io/kube-proxy/config/v1alpha1"
"k8s.io/kubernetes/pkg/cluster/ports"

View file

@ -38,6 +38,7 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/mux"
"k8s.io/apiserver/pkg/server/routes"
@ -62,7 +63,6 @@ import (
"k8s.io/component-base/version"
"k8s.io/component-base/version/verflag"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
nodeutil "k8s.io/component-helpers/node/util"
"k8s.io/klog/v2"
api "k8s.io/kubernetes/pkg/apis/core"

View file

@ -20,6 +20,7 @@ import (
"time"
apiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
@ -27,7 +28,6 @@ import (
"k8s.io/client-go/tools/events"
"k8s.io/client-go/tools/leaderelection"
basecompatibility "k8s.io/component-base/compatibility"
"k8s.io/component-base/zpages/flagz"
kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
)

View file

@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apiserver/pkg/server/flagz"
apiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/util/compatibility"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -47,7 +48,6 @@ import (
logsapi "k8s.io/component-base/logs/api/v1"
"k8s.io/component-base/metrics"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
schedulerappconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
"k8s.io/kubernetes/pkg/scheduler"

View file

@ -37,6 +37,7 @@ import (
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server"
genericfilters "k8s.io/apiserver/pkg/server/filters"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/mux"
"k8s.io/apiserver/pkg/server/routes"
@ -61,7 +62,6 @@ import (
utilversion "k8s.io/component-base/version"
"k8s.io/component-base/version/verflag"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"

View file

@ -65,6 +65,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/wait"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes"
@ -87,7 +88,6 @@ import (
"k8s.io/component-base/version"
"k8s.io/component-base/version/verflag"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
nodeutil "k8s.io/component-helpers/node/util"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1"

View file

@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
"k8s.io/apiserver/pkg/server/flagz"
genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/client-go/util/keyutil"
@ -34,7 +35,6 @@ import (
"k8s.io/component-base/logs"
logsapi "k8s.io/component-base/logs/api/v1"
"k8s.io/component-base/metrics"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
netutil "k8s.io/utils/net"

View file

@ -36,6 +36,7 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes"
@ -43,7 +44,6 @@ import (
cliflag "k8s.io/component-base/cli/flag"
logsapi "k8s.io/component-base/logs/api/v1"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options"
"k8s.io/kubernetes/test/utils/ktesting"

View file

@ -32,12 +32,12 @@ import (
genericregistry "k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/apiserver/pkg/server/flagz"
serverstorage "k8s.io/apiserver/pkg/server/storage"
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientgoinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/component-helpers/apimachinery/lease"
"k8s.io/klog/v2"
"k8s.io/utils/clock"

View file

@ -90,7 +90,8 @@ import (
intstr "k8s.io/apimachinery/pkg/util/intstr"
version "k8s.io/apimachinery/pkg/version"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
apiv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
apiv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
statuszapiv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
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"
@ -1339,7 +1340,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
auditv1.Policy{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_Policy(ref),
auditv1.PolicyList{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyList(ref),
auditv1.PolicyRule{}.OpenAPIModelName(): schema_pkg_apis_audit_v1_PolicyRule(ref),
apiv1alpha1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1alpha1_Statusz(ref),
apiv1alpha1.Flagz{}.OpenAPIModelName(): schema_server_flagz_api_v1alpha1_Flagz(ref),
statuszapiv1alpha1.Statusz{}.OpenAPIModelName(): schema_server_statusz_api_v1alpha1_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),
@ -64936,6 +64938,58 @@ func schema_pkg_apis_audit_v1_PolicyRule(ref common.ReferenceCallback) common.Op
}
}
func schema_server_flagz_api_v1alpha1_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

@ -59,6 +59,7 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/flagz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
coreinformersv1 "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
@ -69,7 +70,6 @@ import (
"k8s.io/client-go/util/certificate"
"k8s.io/client-go/util/flowcontrol"
cloudprovider "k8s.io/cloud-provider"
"k8s.io/component-base/zpages/flagz"
"k8s.io/component-helpers/apimachinery/lease"
resourcehelper "k8s.io/component-helpers/resource"
internalapi "k8s.io/cri-api/pkg/apis"

View file

@ -25,11 +25,11 @@ import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/statusz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/configz"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/features"
)

View file

@ -56,6 +56,7 @@ import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/httplog"
"k8s.io/apiserver/pkg/server/routes"
@ -70,7 +71,6 @@ import (
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/prometheus/slis"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/cri-client/pkg/util"
podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"

View file

@ -57,11 +57,11 @@ import (
"k8s.io/kubernetes/test/utils/ktesting"
// Do some initialization to decode the query parameters correctly.
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
zpagesfeatures "k8s.io/component-base/zpages/features"
"k8s.io/component-base/zpages/flagz"
"k8s.io/kubelet/pkg/cri/streaming"
"k8s.io/kubelet/pkg/cri/streaming/portforward"
remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"

View file

@ -63,6 +63,7 @@ import (
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/apiserver/pkg/server/egressselector"
genericfilters "k8s.io/apiserver/pkg/server/filters"
"k8s.io/apiserver/pkg/server/flagz"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/routes"
"k8s.io/apiserver/pkg/server/routine"
@ -80,7 +81,6 @@ import (
"k8s.io/component-base/metrics/features"
"k8s.io/component-base/metrics/prometheus/slis"
"k8s.io/component-base/tracing"
"k8s.io/component-base/zpages/flagz"
"k8s.io/klog/v2"
openapicommon "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"

View file

@ -0,0 +1,7 @@
approvers:
- sig-instrumentation-approvers
- sig-api-machinery-approvers
reviewers:
- sig-instrumentation-reviewers
labels:
- sig/instrumentation

View file

@ -0,0 +1,11 @@
# Disable inheritance as this is an api owners file
options:
no_parent_owners: true
approvers:
- api-approvers
reviewers:
- api-reviewers
- sig-instrumentation-reviewers
labels:
- kind/api-change
- sig/instrumentation

View file

@ -0,0 +1,22 @@
/*
Copyright 2025 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.v1alpha1
// Package v1alpha1 contains API Schema definitions for the zpages v1alpha1 API group
package v1alpha1

View file

@ -0,0 +1,47 @@
/*
Copyright 2025 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 v1alpha1
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 = "v1alpha1"
)
// 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 2025 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 v1alpha1
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 v1alpha1
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 v1alpha1
// OpenAPIModelName returns the OpenAPI model name for this type.
func (in Flagz) OpenAPIModelName() string {
return "io.k8s.apiserver.pkg.server.flagz.api.v1alpha1.Flagz"
}

View file

@ -0,0 +1,198 @@
/*
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 flagz
import (
"fmt"
"net/http"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/flagz/negotiate"
)
const (
DefaultFlagzPath = "/flagz"
Kind = "Flagz"
GroupName = "config.k8s.io"
Version = "v1alpha1"
)
type mux interface {
Handle(path string, handler http.Handler)
}
// Install installs the flagz endpoint to the given mux.
func Install(m mux, componentName string, flagReader Reader, opts ...Option) {
reg := &registry{
reader: flagReader,
deprecatedVersionsMap: map[string]bool{},
}
for _, opt := range opts {
opt(reg)
}
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
codecFactory := serializer.NewCodecFactory(
scheme,
serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo {
textSerializer := flagzTextSerializer{componentName, reg.reader}
return runtime.SerializerInfo{
MediaType: "text/plain",
MediaTypeType: "text",
MediaTypeSubType: "plain",
EncodesAsText: true,
Serializer: textSerializer,
PrettySerializer: textSerializer,
}
}),
)
m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, codecFactory, negotiate.FlagzEndpointRestrictions{}))
}
func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
obj := flagz(componentName, reg.reader)
acceptHeader := r.Header.Get("Accept")
if strings.TrimSpace(acceptHeader) == "" {
writePlainTextResponse(obj, serializer, w, reg)
return
}
mediaType, serializerInfo, err := negotiation.NegotiateOutputMediaType(r, serializer, restrictions)
if err != nil {
utilruntime.HandleError(err)
responsewriters.ErrorNegotiated(
err,
serializer,
schema.GroupVersion{},
w,
r,
)
return
}
var targetGV schema.GroupVersion
switch serializerInfo.MediaType {
case "application/json":
if mediaType.Convert == nil {
err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for application/json")
utilruntime.HandleError(err)
responsewriters.ErrorNegotiated(
err,
serializer,
schema.GroupVersion{},
w,
r,
)
return
}
targetGV = mediaType.Convert.GroupVersion()
if reg.deprecatedVersions()[targetGV.Version] {
w.Header().Set("Warning", `299 - "This version of the flagz endpoint is deprecated. Please use a newer version."`)
}
case "text/plain":
writePlainTextResponse(obj, serializer, w, reg)
return
default:
err = fmt.Errorf("content negotiation failed: unsupported media type '%s'", serializerInfo.MediaType)
utilruntime.HandleError(err)
responsewriters.ErrorNegotiated(
err,
serializer,
schema.GroupVersion{},
w,
r,
)
return
}
writeResponse(obj, serializer, targetGV, restrictions, w, r)
}
}
func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter, reg *registry) {
reg.cachedPlainTextResponseLock.Lock()
defer reg.cachedPlainTextResponseLock.Unlock()
if reg.cachedPlainTextResponse != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if _, err := w.Write(reg.cachedPlainTextResponse); err != nil {
utilruntime.HandleError(fmt.Errorf("error writing cached flagz as text/plain: %w", err))
}
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
var textSerializer runtime.Serializer
for _, info := range serializer.SupportedMediaTypes() {
if info.MediaType == "text/plain" {
textSerializer = info.Serializer
break
}
}
if textSerializer == nil {
utilruntime.HandleError(fmt.Errorf("text/plain serializer not available"))
w.WriteHeader(http.StatusInternalServerError)
return
}
var buf strings.Builder
if err := textSerializer.Encode(obj, &buf); err != nil {
utilruntime.HandleError(fmt.Errorf("error encoding flagz as text/plain: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
reg.cachedPlainTextResponse = []byte(buf.String())
if _, err := w.Write(reg.cachedPlainTextResponse); err != nil {
utilruntime.HandleError(fmt.Errorf("error writing flagz as text/plain: %w", err))
}
}
func writeResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.FlagzEndpointRestrictions, w http.ResponseWriter, r *http.Request) {
responsewriters.WriteObjectNegotiated(
serializer,
restrictions,
targetGV,
w,
r,
http.StatusOK,
obj,
true,
)
}
func flagz(componentName string, flagReader Reader) *v1alpha1.Flagz {
flags := flagReader.GetFlagz()
return &v1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: Kind,
APIVersion: fmt.Sprintf("%s/%s", GroupName, Version),
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName,
},
Flags: flags,
}
}

View file

@ -0,0 +1,293 @@
/*
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 flagz
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
cliflag "k8s.io/component-base/cli/flag"
)
const wantTmpl = `
%s flagz
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
`
func TestHandleFlagz(t *testing.T) {
fakeFlagName := "test-flag"
fakeFlagValue := "test-value"
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
fs.String(fakeFlagName, fakeFlagValue, "usage")
fakeReader := NamedFlagSetsReader{
FlagSets: cliflag.NamedFlagSets{
FlagSets: map[string]*pflag.FlagSet{
"test": fs,
},
},
}
tests := []struct {
name string
acceptHeader string
componentName string
registry *registry
wantStatusCode int
wantBody string
wantJSONBody *v1alpha1.Flagz
wantWarning bool
}{
{
name: "valid request for text/plain",
acceptHeader: "text/plain",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmpl,
"test-server",
),
},
{
name: "valid request for v1alpha1",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantJSONBody: &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: "deprecated version request",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{"v1alpha1": true},
},
wantStatusCode: http.StatusOK,
wantJSONBody: &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,
},
},
wantWarning: true,
},
{
name: "no accept header falls back to text/plain",
acceptHeader: "",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmpl,
"test-server",
),
},
{
name: "wildcard accept header falls back to text/plain",
acceptHeader: "*/*",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmpl,
"test-server",
),
},
{
name: "bad json header falls back to text/plain with wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusOK,
wantBody: fmt.Sprintf(
wantTmpl,
"test-server",
),
},
{
name: "unsupported accept header",
acceptHeader: "application/xml",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusNotAcceptable,
},
{
name: "unsupported application/json without params",
acceptHeader: "application/json",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusNotAcceptable,
},
{
name: "unsupported application/json with missing params",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
componentName: "test-server",
registry: &registry{
reader: fakeReader,
deprecatedVersionsMap: map[string]bool{},
},
wantStatusCode: http.StatusNotAcceptable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mux := http.NewServeMux()
var opts []Option
var capturedReg *registry
opts = append(opts, func(reg *registry) {
capturedReg = reg
if tt.registry != nil {
reg.deprecatedVersionsMap = tt.registry.deprecatedVersionsMap
}
})
Install(mux, tt.componentName, fakeReader, opts...)
// Assign the captured registry to tt.registry for consistency with existing test logic
tt.registry = capturedReg
path := "/flagz"
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil)
if err != nil {
t.Fatalf("unexpected error while creating request: %v", err)
}
if tt.acceptHeader != "" {
req.Header.Set("Accept", tt.acceptHeader)
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != tt.wantStatusCode {
t.Fatalf("want status code: %v, got: %v", tt.wantStatusCode, w.Code)
}
if tt.wantStatusCode == http.StatusOK {
if tt.wantJSONBody != 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 != "" {
t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff)
}
if tt.wantWarning {
if !strings.Contains(w.Header().Get("Warning"), "deprecated") {
t.Errorf("expected deprecation warning in header, but got: %s", w.Header().Get("Warning"))
}
}
} else if !strings.Contains(string(w.Body.String()), tt.wantBody) {
t.Errorf("Unexpected response body:\n- want: %s\n- got: %s", tt.wantBody, string(w.Body.String()))
}
}
})
}
}
func TestCache(t *testing.T) {
fakeFlagName := "test-flag"
fakeFlagValue := "test-value"
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
fs.String(fakeFlagName, fakeFlagValue, "usage")
fakeReader := NamedFlagSetsReader{
FlagSets: cliflag.NamedFlagSets{
FlagSets: map[string]*pflag.FlagSet{
"test": fs,
},
},
}
mux := http.NewServeMux()
var capturedReg *registry
Install(mux, "test-server", fakeReader, func(reg *registry) {
capturedReg = reg
})
path := "/flagz"
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil)
if err != nil {
t.Fatalf("unexpected error while creating request: %v", err)
}
req.Header.Set("Accept", "text/plain")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code)
}
if capturedReg.cachedPlainTextResponse == nil {
t.Fatalf("cached response should not be nil")
}
cached := capturedReg.cachedPlainTextResponse
fs.String("new-flag", "new-value", "usage")
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code)
}
if diff := cmp.Diff(cached, capturedReg.cachedPlainTextResponse); diff != "" {
t.Errorf("Unexpected diff on cached response (-want,+got):\n%s", diff)
}
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2025 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 negotiate
import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// FlagzEndpointRestrictions implements content negotiation restrictions for the z-pages.
// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses.
type FlagzEndpointRestrictions struct{}
// AllowsMediaTypeTransform checks if the provided GVK is supported for structured z-page responses.
func (FlagzEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
if mimeType == "text" && mimeSubType == "plain" {
return gvk == nil
}
return isStructured(gvk)
}
func (FlagzEndpointRestrictions) AllowsServerVersion(string) bool {
return false
}
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,39 @@
/*
Copyright 2024 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 flagz
import (
"sync"
)
type registry struct {
// reader is a Reader where we can get the flags.
reader Reader
// deprecatedVersionsMap is a map of deprecated flagz versions.
deprecatedVersionsMap map[string]bool
// cachedPlainTextResponse is a cached response of the flagz endpoint.
cachedPlainTextResponse []byte
// cachedPlainTextResponseLock is a lock for the cachedPlainTextResponse.
cachedPlainTextResponseLock sync.Mutex
}
// Option is a function to configure registry.
type Option func(reg *registry)
func (r *registry) deprecatedVersions() map[string]bool {
return r.deprecatedVersionsMap
}

View file

@ -0,0 +1,77 @@
/*
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 flagz
import (
"fmt"
"io"
"math/rand"
"sort"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
delimiters = []string{":", ": ", "=", " "}
)
const headerFmt = `
%s flagz
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
`
// flagzTextSerializer implements runtime.Serializer for text/plain output.
type flagzTextSerializer struct {
componentName string
flagReader Reader
}
// Encode writes the flagz information in plain text format to the given writer, using the provided obj.
func (s flagzTextSerializer) Encode(obj runtime.Object, w io.Writer) error {
if _, err := fmt.Fprintf(w, headerFmt, s.componentName); err != nil {
return err
}
randomIndex := rand.Intn(len(delimiters))
separator := delimiters[randomIndex]
flags := s.flagReader.GetFlagz()
var sortedKeys []string
for key := range flags {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
for _, key := range sortedKeys {
if _, err := fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key]); err != nil {
return err
}
}
return nil
}
// Decode is not supported for text/plain serialization.
func (s flagzTextSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
return nil, nil, fmt.Errorf("decode not supported for text/plain")
}
// Identifier returns a unique identifier for this serializer.
func (s flagzTextSerializer) Identifier() runtime.Identifier {
return runtime.Identifier("flagzTextSerializer")
}

View file

@ -43,7 +43,6 @@ func (StatuszEndpointRestrictions) AllowsStreamSchema(s string) bool {
func isStructured(gvk *schema.GroupVersionKind) bool {
if gvk != nil {
if gvk.Group == "config.k8s.io" && gvk.Version == "v1alpha1" {
// TODO: extend this to Flagz once we have a structured Flagz type.
if gvk.Kind == "Statusz" {
return true
}

View file

@ -12,7 +12,6 @@ require (
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.7.0
github.com/moby/term v0.5.0
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.66.1
@ -59,6 +58,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect

View file

@ -1,102 +0,0 @@
/*
Copyright 2024 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 flagz
import (
"bytes"
"fmt"
"io"
"math/rand"
"net/http"
"sort"
"sync"
"k8s.io/component-base/zpages/httputil"
"k8s.io/klog/v2"
)
const (
DefaultFlagzPath = "/flagz"
flagzHeaderFmt = `
%s flags
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
`
)
var (
delimiters = []string{":", ": ", "=", " "}
)
type registry struct {
response bytes.Buffer
once sync.Once
}
type mux interface {
Handle(path string, handler http.Handler)
}
func Install(m mux, componentName string, flagReader Reader) {
var reg registry
reg.installHandler(m, componentName, flagReader)
}
func (reg *registry) installHandler(m mux, componentName string, flagReader Reader) {
m.Handle(DefaultFlagzPath, reg.handleFlags(componentName, flagReader))
}
func (reg *registry) handleFlags(componentName string, flagReader Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !httputil.AcceptableMediaType(r) {
http.Error(w, httputil.ErrUnsupportedMediaType.Error(), http.StatusNotAcceptable)
return
}
reg.once.Do(func() {
fmt.Fprintf(&reg.response, flagzHeaderFmt, componentName)
if flagReader == nil {
klog.Error("received nil flagReader")
return
}
randomIndex := rand.Intn(len(delimiters))
separator := delimiters[randomIndex]
// Randomize the delimiter for printing to prevent scraping of the response.
printSortedFlags(&reg.response, flagReader.GetFlagz(), separator)
})
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, err := w.Write(reg.response.Bytes())
if err != nil {
klog.Errorf("error writing response: %v", err)
http.Error(w, "error writing response", http.StatusInternalServerError)
}
}
}
func printSortedFlags(w io.Writer, flags map[string]string, separator string) {
var sortedKeys []string
for key := range flags {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
for _, key := range sortedKeys {
fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key])
}
}

View file

@ -1,126 +0,0 @@
/*
Copyright 2024 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 flagz
import (
"fmt"
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
cliflag "k8s.io/component-base/cli/flag"
)
const wantTmpl = `%s flags
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.
`
func TestFlagz(t *testing.T) {
componentName := "test-server"
delimiters = []string{"="}
wantHeaderLines := strings.Split(fmt.Sprintf(wantTmpl, componentName), "\n")
tests := []struct {
name string
header string
flagzReader Reader
wantStatus int
wantResp []string
}{
{
name: "nil flags",
wantStatus: http.StatusOK,
wantResp: wantHeaderLines,
},
{
name: "unaccepted header",
header: "some header",
wantStatus: http.StatusNotAcceptable,
},
{
name: "test flags",
flagzReader: NamedFlagSetsReader{
FlagSets: cliflag.NamedFlagSets{
FlagSets: map[string]*pflag.FlagSet{
"test": flagSet(t, map[string]flagValue{
"test-flag-bar": {
value: "test-value-bar",
sensitive: false,
},
"test-flag-foo": {
value: "test-value-foo",
sensitive: false,
},
}),
},
},
},
wantStatus: http.StatusOK,
wantResp: append(wantHeaderLines,
"test-flag-bar=test-value-bar",
"test-flag-foo=test-value-foo",
),
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
mux := http.NewServeMux()
Install(mux, componentName, test.flagzReader)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil)
if err != nil {
t.Fatalf("case[%d] Unexpected error: %v", i, err)
}
req.Header.Set("Accept", "text/plain; charset=utf-8")
if test.header != "" {
req.Header.Set("Accept", test.header)
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
assert.Equal(t, test.wantStatus, w.Code, "case[%s] Expected status code %d, got %d", test.name, test.wantStatus, w.Code)
if test.wantStatus == http.StatusOK {
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"), "case[%s] Incorrect Content-Type header", test.name)
gotLines := strings.Split(w.Body.String(), "\n")
gotLines = trimEmptyLines(gotLines)
sort.Strings(gotLines)
sort.Strings(test.wantResp)
wantLines := trimEmptyLines(test.wantResp)
assert.Equal(t, wantLines, gotLines, "case[%s] Response body mismatch", test.name)
}
})
}
}
func trimEmptyLines(lines []string) []string {
var result []string
for _, line := range lines {
if line != "" {
result = append(result, line)
}
}
return result
}

View file

@ -1,54 +0,0 @@
/*
Copyright 2024 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 httputil
import (
"fmt"
"net/http"
"strings"
"github.com/munnerz/goautoneg"
)
// ErrUnsupportedMediaType is the error returned when the request's
// Accept header does not contain "text/plain".
var ErrUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain")
// AcceptableMediaType checks if the request's Accept header contains
// a supported media type with optional "charset=utf-8" parameter.
func AcceptableMediaType(r *http.Request) bool {
accepts := goautoneg.ParseAccept(r.Header.Get("Accept"))
for _, accept := range accepts {
if !mediaTypeMatches(accept) {
continue
}
if len(accept.Params) == 0 {
return true
}
if len(accept.Params) == 1 {
if charset, ok := accept.Params["charset"]; ok && strings.EqualFold(charset, "utf-8") {
return true
}
}
}
return false
}
func mediaTypeMatches(a goautoneg.Accept) bool {
return (a.Type == "text" || a.Type == "*") &&
(a.SubType == "plain" || a.SubType == "*")
}

View file

@ -1,74 +0,0 @@
/*
Copyright 2024 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 httputil
import (
"net/http"
"testing"
)
func TestAcceptableMediaTypes(t *testing.T) {
tests := []struct {
name string
reqHeader string
want bool
}{
{
name: "valid text/plain header",
reqHeader: "text/plain",
want: true,
},
{
name: "valid text/* header",
reqHeader: "text/*",
want: true,
},
{
name: "valid */plain header",
reqHeader: "*/plain",
want: true,
},
{
name: "valid accept args",
reqHeader: "text/plain; charset=utf-8",
want: true,
},
{
name: "invalid text/foo header",
reqHeader: "text/foo",
want: false,
},
{
name: "invalid text/plain params",
reqHeader: "text/plain; foo=bar",
want: false,
},
}
for _, tt := range tests {
req, err := http.NewRequest(http.MethodGet, "http://example.com/statusz", nil)
if err != nil {
t.Fatalf("Unexpected error while creating request: %v", err)
}
req.Header.Set("Accept", tt.reqHeader)
got := AcceptableMediaType(req)
if got != tt.want {
t.Errorf("Unexpected response from AcceptableMediaType(), want %v, got = %v", tt.want, got)
}
}
}

View file

@ -49,6 +49,7 @@ import (
"k8s.io/kubernetes/test/integration/etcd"
"k8s.io/kubernetes/test/integration/framework"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
)
@ -135,7 +136,7 @@ func TestLivezAndReadyz(t *testing.T) {
func TestFlagz(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ComponentFlagz, true)
testServerFlags := append(framework.DefaultTestServerFlags(), "--emulated-version=1.32")
testServerFlags := append(framework.DefaultTestServerFlags(), "--v=2")
server := kubeapiservertesting.StartTestServerOrDie(t, nil, testServerFlags, framework.SharedEtcd())
defer server.TearDownFn()
@ -144,33 +145,107 @@ func TestFlagz(t *testing.T) {
t.Fatalf("Unexpected error: %v", err)
}
res := client.CoreV1().RESTClient().Get().RequestURI("/flagz").Do(context.TODO())
var status int
res.StatusCode(&status)
if status != http.StatusOK {
t.Fatalf("flagz/ should be healthy, got %v", status)
wantBodyStr := "kube-apiserver flagz\nWarning: This endpoint is not meant to be machine parseable"
wantBodyJSON := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-apiserver",
},
}
expectedHeader := `
kube-apiserver flags
Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only.`
raw, err := res.Raw()
if err != nil {
t.Fatal(err)
}
if !bytes.HasPrefix(raw, []byte(expectedHeader)) {
t.Fatalf("Header mismatch!\nExpected:\n%s\n\nGot:\n%s", expectedHeader, string(raw))
}
found := false
for _, line := range strings.Split(string(raw), "\n") {
if strings.Contains(line, "emulated-version") && strings.Contains(line, "1.32") {
found = true
break
}
}
if !found {
t.Fatalf("Expected flag --emulated-version=[1.32] to be reflected in /flagz output, got:\n%s", string(raw))
for _, tc := range []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantJSON *flagzv1alpha1.Flagz // for application/json
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantJSON: wantBodyJSON,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
},
{
name: "invalid accept header",
acceptHeader: "application/xml",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json without params",
acceptHeader: "application/json",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
},
} {
t.Run(tc.name, func(t *testing.T) {
req := client.CoreV1().RESTClient().Get().RequestURI("/flagz")
req.SetHeader("Accept", tc.acceptHeader)
res := req.Do(context.TODO())
var status int
res.StatusCode(&status)
if status != tc.wantStatus {
t.Fatalf("want status %d, got %d", tc.wantStatus, status)
}
raw, err := res.Raw()
if err != nil && tc.wantStatus == http.StatusOK {
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.wantJSON != nil {
var got flagzv1alpha1.Flagz
if err := json.Unmarshal(raw, &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 got.Flags["v"] != "2" {
t.Errorf("v mismatch: want %q, got %q", "2", got.Flags["v"])
}
}
}
})
}
}

View file

@ -31,6 +31,7 @@ import (
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/util/retry"
@ -43,10 +44,6 @@ import (
)
func TestEndpointHandlers(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.ComponentFlagz: true,
})
server, configStr, _, err := startTestAPIServer(t)
if err != nil {
t.Fatalf("Failed to start kube-apiserver server: %v", err)
@ -126,16 +123,6 @@ func TestEndpointHandlers(t *testing.T) {
useBrokenConfig: true,
wantResponseCode: http.StatusInternalServerError,
},
{
name: "/flagz",
path: "/flagz",
requestHeader: map[string]string{"Accept": "text/plain"},
wantResponseCode: http.StatusOK,
wantResponseBodyRegx: `^\n` +
`kube-scheduler flags\n` +
`Warning: This endpoint is not meant to be machine parseable, ` +
`has no formatting compatibility guarantees and is for debugging purposes only.`,
},
}
for _, tt := range tests {
@ -217,9 +204,10 @@ func TestEndpointHandlers(t *testing.T) {
}
}
func TestSchedulerStatusz(t *testing.T) {
func TestSchedulerZPages(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.ComponentStatusz: true,
features.ComponentFlagz: true,
})
server, configStr, _, err := startTestAPIServer(t)
@ -242,7 +230,7 @@ func TestSchedulerStatusz(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
result, err := kubeschedulertesting.StartTestServer(
t, ctx,
[]string{"--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--authorization-always-allow-paths=/statusz"},
[]string{"--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--authorization-always-allow-paths=/statusz,/flagz"},
)
if err != nil {
t.Fatalf("Failed to start kube-scheduler server: %v", err)
@ -256,8 +244,8 @@ func TestSchedulerStatusz(t *testing.T) {
t.Fatalf("Failed to get client from test server: %v", err)
}
wantBodyStr := "kube-scheduler statusz\nWarning: This endpoint is not meant to be machine parseable"
wantBodyJSON := &v1alpha1.Statusz{
statuszWantBodyStr := "kube-scheduler statusz\nWarning: This endpoint is not meant to be machine parseable"
statuszWantBodyJSON := &v1alpha1.Statusz{
TypeMeta: metav1.TypeMeta{
Kind: "Statusz",
APIVersion: "config.k8s.io/v1alpha1",
@ -265,10 +253,21 @@ func TestSchedulerStatusz(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "kube-scheduler",
},
Paths: []string{"/configz", "/healthz", "/livez", "/metrics", "/readyz"},
Paths: []string{"/configz", "/flagz", "/healthz", "/livez", "/metrics", "/readyz"},
}
for _, tc := range []struct {
flagzWantBodyStr := "kube-scheduler flagz\nWarning: This endpoint is not meant to be machine parseable"
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-scheduler",
},
}
statuszTestCases := []struct {
name string
acceptHeader string
wantStatus int
@ -279,19 +278,19 @@ func TestSchedulerStatusz(t *testing.T) {
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
wantStatus: http.StatusOK,
wantJSON: wantBodyJSON,
wantJSON: statuszWantBodyJSON,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "invalid accept header",
@ -312,16 +311,72 @@ func TestSchedulerStatusz(t *testing.T) {
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
} {
t.Run(tc.name, func(t *testing.T) {
}
flagzTestCases := []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantJSON *flagzv1alpha1.Flagz // for structured json
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantJSON: flagzWantBodyJSON,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "invalid accept header",
acceptHeader: "application/xml",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json without params",
acceptHeader: "application/json",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
}
for _, tc := range statuszTestCases {
t.Run("statusz_"+tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, base+"/statusz", nil)
if err != nil {
t.Fatalf("failed to request: %v", err)
@ -371,6 +426,55 @@ func TestSchedulerStatusz(t *testing.T) {
}
})
}
for _, tc := range flagzTestCases {
t.Run("flagz_"+tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, base+"/flagz", nil)
if err != nil {
t.Fatalf("failed to request: %v", err)
}
req.Header.Set("Accept", tc.acceptHeader)
r, err := client.Do(req)
if err != nil {
t.Fatalf("failed to GET /flagz: %v", err)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
if err = r.Body.Close(); err != nil {
t.Fatalf("failed to close response body: %v", err)
}
if r.StatusCode != tc.wantStatus {
t.Fatalf("want status %d, got %d", tc.wantStatus, r.StatusCode)
}
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.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)
}
}
}
})
}
}
// TODO: Make this a util function once there is a unified way to start a testing apiserver so that we can reuse it.

View file

@ -33,6 +33,7 @@ import (
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1"
"k8s.io/apiserver/pkg/server/statusz/api/v1alpha1"
"k8s.io/apiserver/pkg/server"
@ -299,7 +300,7 @@ func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) {
}, nil
}
func TestKubeControllerManagerServingStatusz(t *testing.T) {
func TestKubeControllerManagerServingZPages(t *testing.T) {
// authenticate to apiserver via bearer token
token := "flwqkenfjasasdfmwerasd" // Fake token for testing.
tokenFile, err := os.CreateTemp("", "kubeconfig")
@ -352,8 +353,8 @@ users:
t.Fatal(err)
}
wantBodyStr := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable"
wantBodyJSON := &v1alpha1.Statusz{
statuszWantBodyStr := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable"
statuszWantBodyJSON := &v1alpha1.Statusz{
TypeMeta: metav1.TypeMeta{
Kind: "Statusz",
APIVersion: "config.k8s.io/v1alpha1",
@ -361,10 +362,21 @@ users:
ObjectMeta: metav1.ObjectMeta{
Name: "kube-controller-manager",
},
Paths: []string{"/configz", "/healthz", "/metrics"},
Paths: []string{"/configz", "/flagz", "/healthz", "/metrics"},
}
testCases := []struct {
flagzWantBodyStr := "kube-controller-manager flagz\nWarning: This endpoint is not meant to be machine parseable"
flagzWantBodyJSON := &flagzv1alpha1.Flagz{
TypeMeta: metav1.TypeMeta{
Kind: "Flagz",
APIVersion: "config.k8s.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "kube-controller-manager",
},
}
statuszTestCases := []struct {
name string
acceptHeader string
wantStatus int
@ -375,19 +387,19 @@ users:
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz",
wantStatus: http.StatusOK,
wantJSON: wantBodyJSON,
wantJSON: statuszWantBodyJSON,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "invalid accept header",
@ -408,23 +420,78 @@ users:
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*",
wantStatus: http.StatusOK,
wantBodySub: wantBodyStr,
wantBodySub: statuszWantBodyStr,
},
}
flagzTestCases := []struct {
name string
acceptHeader string
wantStatus int
wantBodySub string // for text/plain
wantJSON *flagzv1alpha1.Flagz // for structured json
}{
{
name: "text plain response",
acceptHeader: "text/plain",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "structured json response",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz",
wantStatus: http.StatusOK,
wantJSON: flagzWantBodyJSON,
},
{
name: "no accept header (defaults to text)",
acceptHeader: "",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "invalid accept header",
acceptHeader: "application/xml",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json without params",
acceptHeader: "application/json",
wantStatus: http.StatusNotAcceptable,
},
{
name: "application/json with missing as",
acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io",
wantStatus: http.StatusNotAcceptable,
},
{
name: "wildcard accept header",
acceptHeader: "*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
{
name: "bad json header fall back wildcard",
acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*",
wantStatus: http.StatusOK,
wantBodySub: flagzWantBodyStr,
},
}
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true)
_, ctx := ktesting.NewTestContext(t)
flags := []string{
"--authentication-skip-lookup",
"--authentication-kubeconfig", apiserverConfig.Name(),
"--authorization-kubeconfig", apiserverConfig.Name(),
"--authorization-always-allow-paths", "/statusz",
"--authorization-always-allow-paths", "/statusz,/flagz",
"--kubeconfig", apiserverConfig.Name(),
"--leader-elect=false",
}
@ -442,7 +509,8 @@ users:
if err != nil {
t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err)
}
url := fmt.Sprintf("https://127.0.0.1:%s/statusz", port)
statuszURL := fmt.Sprintf("https://127.0.0.1:%s/statusz", port)
flagzURL := fmt.Sprintf("https://127.0.0.1:%s/flagz", port)
// read self-signed server cert disk
pool := x509.NewCertPool()
@ -459,9 +527,9 @@ users:
}
client := &http.Client{Transport: tr}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, url, nil)
for _, tc := range statuszTestCases {
t.Run("statusz_"+tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, statuszURL, nil)
if err != nil {
t.Fatal(err)
}
@ -511,4 +579,54 @@ users:
}
})
}
for _, tc := range flagzTestCases {
t.Run("flagz_"+tc.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, flagzURL, nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", tc.acceptHeader)
req.Header.Add("Authorization", fmt.Sprintf("Token %s", token))
r, err := client.Do(req)
if err != nil {
t.Fatalf("failed to GET /flagz: %v", err)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
if err = r.Body.Close(); err != nil {
t.Fatalf("failed to close response body: %v", err)
}
if r.StatusCode != tc.wantStatus {
t.Fatalf("want status %d, got %d", tc.wantStatus, r.StatusCode)
}
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.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)
}
}
}
})
}
}