From 2422bc0bb8cb241a4026f86ee3b7d89b26f5809b Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 31 Oct 2025 05:14:49 +0000 Subject: [PATCH 1/5] feat: Implement structured /flagz endpoint --- cmd/kube-apiserver/app/server.go | 2 +- cmd/kube-apiserver/app/testing/testserver.go | 2 +- .../app/config/config.go | 2 +- .../app/controllermanager.go | 2 +- .../app/options/options.go | 2 +- cmd/kube-proxy/app/options.go | 2 +- cmd/kube-proxy/app/server.go | 2 +- cmd/kube-scheduler/app/config/config.go | 2 +- cmd/kube-scheduler/app/options/options.go | 2 +- cmd/kube-scheduler/app/server.go | 2 +- cmd/kubelet/app/server.go | 2 +- pkg/controlplane/apiserver/options/options.go | 2 +- .../generic/server/testing/testserver.go | 2 +- pkg/controlplane/apiserver/server.go | 2 +- pkg/generated/openapi/zz_generated.openapi.go | 58 +++- pkg/kubelet/kubelet.go | 2 +- pkg/kubelet/server/auth.go | 2 +- pkg/kubelet/server/server.go | 2 +- pkg/kubelet/server/server_test.go | 2 +- .../src/k8s.io/apiserver/pkg/server/config.go | 2 +- .../pkg/server/flagz/api/v1alpha1/doc.go | 22 ++ .../pkg/server/flagz/api/v1alpha1/register.go | 47 +++ .../pkg/server/flagz/api/v1alpha1/types.go | 37 +++ .../api/v1alpha1/zz_generated.deepcopy.go | 59 ++++ .../api/v1alpha1/zz_generated.model_name.go | 27 ++ .../pkg/server}/flagz/flagreader.go | 0 .../pkg/server}/flagz/flagreader_test.go | 0 .../apiserver/pkg/server/flagz/flagz.go | 198 ++++++++++++ .../apiserver/pkg/server/flagz/flagz_test.go | 293 ++++++++++++++++++ .../pkg/server/flagz/negotiate/negotiation.go | 53 ++++ .../apiserver/pkg/server/flagz/registry.go | 39 +++ .../pkg/server/flagz/textserializer.go | 77 +++++ .../server/statusz/negotiate/negotiation.go | 1 - staging/src/k8s.io/component-base/go.mod | 2 +- .../component-base/zpages/flagz/flagz.go | 102 ------ .../component-base/zpages/flagz/flagz_test.go | 126 -------- .../zpages/httputil/httputil.go | 54 ---- .../zpages/httputil/httputil_test.go | 74 ----- 38 files changed, 928 insertions(+), 379 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/register.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/types.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go rename staging/src/k8s.io/{component-base/zpages => apiserver/pkg/server}/flagz/flagreader.go (100%) rename staging/src/k8s.io/{component-base/zpages => apiserver/pkg/server}/flagz/flagreader_test.go (100%) create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/textserializer.go delete mode 100644 staging/src/k8s.io/component-base/zpages/flagz/flagz.go delete mode 100644 staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go delete mode 100644 staging/src/k8s.io/component-base/zpages/httputil/httputil.go delete mode 100644 staging/src/k8s.io/component-base/zpages/httputil/httputil_test.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 71ebb317461..a0ba37e0103 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -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" diff --git a/cmd/kube-apiserver/app/testing/testserver.go b/cmd/kube-apiserver/app/testing/testserver.go index aa8a4d11d49..46506cc2a10 100644 --- a/cmd/kube-apiserver/app/testing/testserver.go +++ b/cmd/kube-apiserver/app/testing/testserver.go @@ -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" diff --git a/cmd/kube-controller-manager/app/config/config.go b/cmd/kube-controller-manager/app/config/config.go index af7ae4b3dda..77c6699a502 100644 --- a/cmd/kube-controller-manager/app/config/config.go +++ b/cmd/kube-controller-manager/app/config/config.go @@ -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" ) diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index ddfb135b201..873d5e0bbef 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -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" diff --git a/cmd/kube-controller-manager/app/options/options.go b/cmd/kube-controller-manager/app/options/options.go index c599ff58bd6..c447c0cc01c 100644 --- a/cmd/kube-controller-manager/app/options/options.go +++ b/cmd/kube-controller-manager/app/options/options.go @@ -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" diff --git a/cmd/kube-proxy/app/options.go b/cmd/kube-proxy/app/options.go index 0f33a0ac0bd..ff7d349d2e9 100644 --- a/cmd/kube-proxy/app/options.go +++ b/cmd/kube-proxy/app/options.go @@ -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" diff --git a/cmd/kube-proxy/app/server.go b/cmd/kube-proxy/app/server.go index 0100183a959..b4e48c578fb 100644 --- a/cmd/kube-proxy/app/server.go +++ b/cmd/kube-proxy/app/server.go @@ -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" diff --git a/cmd/kube-scheduler/app/config/config.go b/cmd/kube-scheduler/app/config/config.go index ef0e7ff4fec..e1ddc8afb7f 100644 --- a/cmd/kube-scheduler/app/config/config.go +++ b/cmd/kube-scheduler/app/config/config.go @@ -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" ) diff --git a/cmd/kube-scheduler/app/options/options.go b/cmd/kube-scheduler/app/options/options.go index 59dfaa4a51b..80f6eeab695 100644 --- a/cmd/kube-scheduler/app/options/options.go +++ b/cmd/kube-scheduler/app/options/options.go @@ -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" diff --git a/cmd/kube-scheduler/app/server.go b/cmd/kube-scheduler/app/server.go index 633417189d5..834213e337f 100644 --- a/cmd/kube-scheduler/app/server.go +++ b/cmd/kube-scheduler/app/server.go @@ -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" diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index b084405e0c3..54ff8d6a26b 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -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" diff --git a/pkg/controlplane/apiserver/options/options.go b/pkg/controlplane/apiserver/options/options.go index b409f595808..cce8f3e77a7 100644 --- a/pkg/controlplane/apiserver/options/options.go +++ b/pkg/controlplane/apiserver/options/options.go @@ -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" diff --git a/pkg/controlplane/apiserver/samples/generic/server/testing/testserver.go b/pkg/controlplane/apiserver/samples/generic/server/testing/testserver.go index 16a0667711b..8b2600988a4 100644 --- a/pkg/controlplane/apiserver/samples/generic/server/testing/testserver.go +++ b/pkg/controlplane/apiserver/samples/generic/server/testing/testserver.go @@ -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" diff --git a/pkg/controlplane/apiserver/server.go b/pkg/controlplane/apiserver/server.go index ba4e0f45dc3..7ef44b28531 100644 --- a/pkg/controlplane/apiserver/server.go +++ b/pkg/controlplane/apiserver/server.go @@ -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" diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 4d2cbf79d2c..d873144dc76 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -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" @@ -1337,7 +1338,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), @@ -64858,6 +64860,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{ diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 0c7b08af147..e66d1252140 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -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" diff --git a/pkg/kubelet/server/auth.go b/pkg/kubelet/server/auth.go index 6f5fb75096d..b5d8255d4ca 100644 --- a/pkg/kubelet/server/auth.go +++ b/pkg/kubelet/server/auth.go @@ -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" ) diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go index 8cd47f46281..31ecfe65672 100644 --- a/pkg/kubelet/server/server.go +++ b/pkg/kubelet/server/server.go @@ -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" diff --git a/pkg/kubelet/server/server_test.go b/pkg/kubelet/server/server_test.go index bd80ac92d40..97091722ef5 100644 --- a/pkg/kubelet/server/server_test.go +++ b/pkg/kubelet/server/server_test.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index 3e8a6b8c068..6e77812ec9e 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -62,6 +62,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" @@ -79,7 +80,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" 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 new file mode 100644 index 00000000000..ab7a48c2106 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/doc.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/register.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/register.go new file mode 100644 index 00000000000..d2a1f08bce9 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/register.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/types.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/types.go new file mode 100644 index 00000000000..c0ce9736994 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/types.go @@ -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"` +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..b32aabef6ae --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/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 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go new file mode 100644 index 00000000000..d953fff3cbc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/v1alpha1/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 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" +} diff --git a/staging/src/k8s.io/component-base/zpages/flagz/flagreader.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagreader.go similarity index 100% rename from staging/src/k8s.io/component-base/zpages/flagz/flagreader.go rename to staging/src/k8s.io/apiserver/pkg/server/flagz/flagreader.go diff --git a/staging/src/k8s.io/component-base/zpages/flagz/flagreader_test.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagreader_test.go similarity index 100% rename from staging/src/k8s.io/component-base/zpages/flagz/flagreader_test.go rename to staging/src/k8s.io/apiserver/pkg/server/flagz/flagreader_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go new file mode 100644 index 00000000000..de5dfe79fee --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz.go @@ -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 := ®istry{ + 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, + } +} 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 new file mode 100644 index 00000000000..4f6e6148492 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/flagz_test.go @@ -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: ®istry{ + 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: ®istry{ + 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: ®istry{ + 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: ®istry{ + 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: ®istry{ + 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: ®istry{ + 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: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "unsupported application/json without params", + acceptHeader: "application/json", + componentName: "test-server", + registry: ®istry{ + 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: ®istry{ + 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) + } +} 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 new file mode 100644 index 00000000000..f3edc8f3c1a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/negotiate/negotiation.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go new file mode 100644 index 00000000000..7fe489a183e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/registry.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/textserializer.go b/staging/src/k8s.io/apiserver/pkg/server/flagz/textserializer.go new file mode 100644 index 00000000000..ee9f72edd63 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/textserializer.go @@ -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") +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/statusz/negotiate/negotiation.go b/staging/src/k8s.io/apiserver/pkg/server/statusz/negotiate/negotiation.go index 1925c20c35e..04c6943774f 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/statusz/negotiate/negotiation.go +++ b/staging/src/k8s.io/apiserver/pkg/server/statusz/negotiate/negotiation.go @@ -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 } diff --git a/staging/src/k8s.io/component-base/go.mod b/staging/src/k8s.io/component-base/go.mod index 1399ecd839f..08d74d52c3e 100644 --- a/staging/src/k8s.io/component-base/go.mod +++ b/staging/src/k8s.io/component-base/go.mod @@ -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 diff --git a/staging/src/k8s.io/component-base/zpages/flagz/flagz.go b/staging/src/k8s.io/component-base/zpages/flagz/flagz.go deleted file mode 100644 index 99a2f44824d..00000000000 --- a/staging/src/k8s.io/component-base/zpages/flagz/flagz.go +++ /dev/null @@ -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(®.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(®.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]) - } -} diff --git a/staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go b/staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go deleted file mode 100644 index c8568c8b527..00000000000 --- a/staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/component-base/zpages/httputil/httputil.go b/staging/src/k8s.io/component-base/zpages/httputil/httputil.go deleted file mode 100644 index da49474ba89..00000000000 --- a/staging/src/k8s.io/component-base/zpages/httputil/httputil.go +++ /dev/null @@ -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 == "*") -} diff --git a/staging/src/k8s.io/component-base/zpages/httputil/httputil_test.go b/staging/src/k8s.io/component-base/zpages/httputil/httputil_test.go deleted file mode 100644 index ab15cd27fc5..00000000000 --- a/staging/src/k8s.io/component-base/zpages/httputil/httputil_test.go +++ /dev/null @@ -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) - } - } -} From 16db8e2375fa2019ffbabb76a261cebb92e7b287 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 31 Oct 2025 05:21:40 +0000 Subject: [PATCH 2/5] feat: add integration test for /flagz kube-apiserver endpoint --- .../controlplane/kube_apiserver_test.go | 127 ++++++++++++++---- 1 file changed, 101 insertions(+), 26 deletions(-) diff --git a/test/integration/controlplane/kube_apiserver_test.go b/test/integration/controlplane/kube_apiserver_test.go index e9d684e2c5d..ff633c65e9c 100644 --- a/test/integration/controlplane/kube_apiserver_test.go +++ b/test/integration/controlplane/kube_apiserver_test.go @@ -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"]) + } + } + } + }) } } From ba059168c356451962a9140bae58d554033b7477 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 31 Oct 2025 05:30:54 +0000 Subject: [PATCH 3/5] test: add integration test for /flagz endpoint in KCM --- test/integration/serving/serving_test.go | 148 ++++++++++++++++++++--- 1 file changed, 133 insertions(+), 15 deletions(-) diff --git a/test/integration/serving/serving_test.go b/test/integration/serving/serving_test.go index 1f6b197f0ee..12d9a35f734 100644 --- a/test/integration/serving/serving_test.go +++ b/test/integration/serving/serving_test.go @@ -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) + } + } + } + }) + } } From 09dfeeb9881ec46400c48ed6e6dc0001112840ac Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 31 Oct 2025 05:43:01 +0000 Subject: [PATCH 4/5] test: add integration test for /flagz endpoint in scheduler --- .../scheduler/serving/endpoints_test.go | 158 +++++++++++++++--- 1 file changed, 131 insertions(+), 27 deletions(-) diff --git a/test/integration/scheduler/serving/endpoints_test.go b/test/integration/scheduler/serving/endpoints_test.go index 1e2f005b7ea..06d444fd33d 100644 --- a/test/integration/scheduler/serving/endpoints_test.go +++ b/test/integration/scheduler/serving/endpoints_test.go @@ -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. From 256591520a552cda01b75eca08b1715a5f35e398 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Fri, 31 Oct 2025 18:22:37 +0000 Subject: [PATCH 5/5] feat: add OWNERS files for flagz server and api with approvers and reviewers --- staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS | 7 +++++++ .../src/k8s.io/apiserver/pkg/server/flagz/api/OWNERS | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS create mode 100644 staging/src/k8s.io/apiserver/pkg/server/flagz/api/OWNERS diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS b/staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS new file mode 100644 index 00000000000..01225cdf85d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/OWNERS @@ -0,0 +1,7 @@ +approvers: + - sig-instrumentation-approvers + - sig-api-machinery-approvers +reviewers: + - sig-instrumentation-reviewers +labels: + - sig/instrumentation diff --git a/staging/src/k8s.io/apiserver/pkg/server/flagz/api/OWNERS b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/OWNERS new file mode 100644 index 00000000000..0cd610188ae --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/flagz/api/OWNERS @@ -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