Repository: kubernetes/kubernetes (language: go)

Issue:
Add statusz endpoint for kubelet
### What would you like to be added?

What would you like to be added?
Part of

https://github.com/kubernetes/enhancements/issues/4827
Add the /statuz endpoint for kubelet.

Sample response:

Started: Fri Sep  6 06:19:51 UTC 2024
Up: 0 hr 00 min 30 sec
Go version: go1.23.0
Binary version: 1.31.0-beta.0.981&#43;c6be932655a03b-dirty
Emulation version: 1.31.0-beta.0.981
Minimum Compatibility version: 1.30.0

List of useful endpoints
--------------
configz:/configz
healthz:/healthz
livez:/livez
metrics:/metrics
readyz:/readyz
sli metrics:/metrics/slis
/sig instrumentation

### Why is this needed?

Refer to [Motivation setcion](https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/4827-component-statusz/README.md#motivation) in KEP.


Discussion / hints:
This issue is currently awaiting triage.

If a SIG or subproject determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
/assign
@Henrywu573 Hi, I see you've assigned multiple components. If you don't mind, I would like to work on the `kubelet` part. Thanks~
Hi, sure. Feel free to assign this issue to yourself : ) 

cc @richabanker
/assign

Fix patch:
diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go
index cfcf6e7d5cdcf..888cc17fa0af2 100644
--- a/cmd/kubelet/app/server.go
+++ b/cmd/kubelet/app/server.go
@@ -132,14 +132,9 @@ func init() {
 	otel.SetMeterProvider(noop.NewMeterProvider())
 }
 
-const (
-	// Kubelet component name
-	componentKubelet = "kubelet"
-)
-
 // NewKubeletCommand creates a *cobra.Command object with default parameters
 func NewKubeletCommand() *cobra.Command {
-	cleanFlagSet := pflag.NewFlagSet(componentKubelet, pflag.ContinueOnError)
+	cleanFlagSet := pflag.NewFlagSet(server.ComponentKubelet, pflag.ContinueOnError)
 	cleanFlagSet.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
 	kubeletFlags := options.NewKubeletFlags()
 
@@ -151,7 +146,7 @@ func NewKubeletCommand() *cobra.Command {
 	}
 
 	cmd := &cobra.Command{
-		Use: componentKubelet,
+		Use: server.ComponentKubelet,
 		Long: `The kubelet is the primary "node agent" that runs on each
 node. It can register the node with the apiserver using one of: the hostname; a flag to
 override the hostname; or specific logic for a cloud provider.
@@ -562,7 +557,7 @@ func makeEventRecorder(ctx context.Context, kubeDeps *kubelet.Dependencies, node
 		return
 	}
 	eventBroadcaster := record.NewBroadcaster(record.WithContext(ctx))
-	kubeDeps.Recorder = eventBroadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: componentKubelet, Host: string(nodeName)})
+	kubeDeps.Recorder = eventBroadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: server.ComponentKubelet, Host: string(nodeName)})
 	eventBroadcaster.StartStructuredLogging(3)
 	if kubeDeps.EventClient != nil {
 		klog.V(4).InfoS("Sending events to api server")
@@ -1386,7 +1381,7 @@ func newTracerProvider(s *options.KubeletServer) (oteltrace.TracerProvider, erro
 	}
 	resourceOpts := []otelsdkresource.Option{
 		otelsdkresource.WithAttributes(
-			semconv.ServiceNameKey.String(componentKubelet),
+			semconv.ServiceNameKey.String(server.ComponentKubelet),
 			semconv.HostNameKey.String(hostname),
 		),
 	}
diff --git a/pkg/kubelet/server/auth.go b/pkg/kubelet/server/auth.go
index 568f78b048496..7e7fdd9643a28 100644
--- a/pkg/kubelet/server/auth.go
+++ b/pkg/kubelet/server/auth.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/apiserver/pkg/server/healthz"
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	"k8s.io/component-base/configz"
+	"k8s.io/component-base/zpages/statusz"
 	"k8s.io/klog/v2"
 	"k8s.io/kubernetes/pkg/features"
 )
@@ -68,6 +69,7 @@ func isSubpath(subpath, path string) bool {
 //	/metrics/*		=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=metrics
 //	/logs/*			=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=log
 //	/checkpoint/*	=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=checkpoint
+//	/statusz 		=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=statusz
 //	/pods/*			=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=pods,proxy
 //	/runningPods/*	=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=pods,proxy
 //	/healthz/* 		=> verb=<api verb from request>, resource=nodes, name=<node name>, subresource(s)=healthz,proxy
@@ -116,6 +118,8 @@ func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *htt
 		subresources = append(subresources, "log")
 	case isSubpath(requestPath, checkpointPath):
 		subresources = append(subresources, "checkpoint")
+	case isSubpath(requestPath, statusz.DefaultStatuszPath):
+		subresources = append(subresources, "statusz")
 	default:
 		subresources = append(subresources, "proxy")
 	}
diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go
index 46ac2403d1d60..08a9ca5bb0371 100644
--- a/pkg/kubelet/server/server.go
+++ b/pkg/kubelet/server/server.go
@@ -67,6 +67,8 @@ import (
 	metricsfeatures "k8s.io/component-base/metrics/features"
 	"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/statusz"
 	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
 	"k8s.io/cri-client/pkg/util"
 	podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"
@@ -107,6 +109,11 @@ const (
 	runningPodsPath     = "/runningpods/"
 )
 
+const (
+	// Kubelet component name
+	ComponentKubelet = "kubelet"
+)
+
 // Server is a http.Handler which exposes kubelet functionality over HTTP.
 type Server struct {
 	auth                 AuthInterface
@@ -567,6 +574,11 @@ func (s *Server) InstallDebuggingHandlers() {
 	s.addMetricsBucketMatcher("configz")
 	configz.InstallHandler(s.restfulCont)
 
+	if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) {
+		s.addMetricsBucketMatcher("statusz")
+		statusz.Install(s.restfulCont, ComponentKubelet, statusz.NewRegistry())
+	}
+
 	// The /runningpods endpoint is used for testing only.
 	s.addMetricsBucketMatcher("runningpods")
 	ws = new(restful.WebService)
diff --git a/staging/src/k8s.io/component-base/zpages/statusz/statusz.go b/staging/src/k8s.io/component-base/zpages/statusz/statusz.go
index 7d07d5ddbb60b..2c1923cb33f61 100644
--- a/staging/src/k8s.io/component-base/zpages/statusz/statusz.go
+++ b/staging/src/k8s.io/component-base/zpages/statusz/statusz.go
@@ -35,6 +35,8 @@ var (
 	errUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain")
 )
 
+const DefaultStatuszPath = "/statusz"
+
 const (
 	headerFmt = `
 %s statusz
@@ -73,7 +75,7 @@ func Install(m mux, componentName string, reg statuszRegistry) {
 		klog.Errorf("error while parsing gotemplates: %v", err)
 		return
 	}
-	m.Handle("/statusz", handleStatusz(componentName, dataTmpl, reg))
+	m.Handle(DefaultStatuszPath, handleStatusz(componentName, dataTmpl, reg))
 }
 
 func initializeTemplates() (*template.Template, error) {


Test patch:
diff --git a/pkg/kubelet/server/auth_test.go b/pkg/kubelet/server/auth_test.go
index fd0256059d2ea..e3d561d8f63f7 100644
--- a/pkg/kubelet/server/auth_test.go
+++ b/pkg/kubelet/server/auth_test.go
@@ -125,6 +125,7 @@ func AuthzTestCases(fineGrained bool) []AuthzTestCase {
 		"/attach/{podNamespace}/{podID}/{uid}/{containerName}": {"proxy"},
 		"/checkpoint/{podNamespace}/{podID}/{containerName}":   {"checkpoint"},
 		"/configz": {"proxy"},
+		"/statusz": {"statusz"},
 		"/containerLogs/{podNamespace}/{podID}/{containerName}": {"proxy"},
 		"/debug/flags/v":                                     {"proxy"},
 		"/debug/pprof/{subpath:*}":                           {"proxy"},
diff --git a/pkg/kubelet/server/server_test.go b/pkg/kubelet/server/server_test.go
index a79e71742bfea..7540a14fbded0 100644
--- a/pkg/kubelet/server/server_test.go
+++ b/pkg/kubelet/server/server_test.go
@@ -57,6 +57,7 @@ import (
 	"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/kubelet/pkg/cri/streaming"
 	"k8s.io/kubelet/pkg/cri/streaming/portforward"
 	remotecommandserver "k8s.io/kubelet/pkg/cri/streaming/remotecommand"
@@ -572,6 +573,7 @@ func TestAuthzCoverage(t *testing.T) {
 func TestAuthFilters(t *testing.T) {
 	// Enable features.ContainerCheckpoint during test
 	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerCheckpoint, true)
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
 
 	fw := newServerTest()
 	defer fw.testHTTPServer.Close()
@@ -1617,6 +1619,8 @@ func TestServePortForward(t *testing.T) {
 }
 
 func TestMetricBuckets(t *testing.T) {
+	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
+
 	tests := map[string]struct {
 		url    string
 		bucket string
@@ -1648,6 +1652,7 @@ func TestMetricBuckets(t *testing.T) {
 		"runningpods":                     {url: "/runningpods/", bucket: "runningpods"},
 		"stats":                           {url: "/stats/", bucket: "stats"},
 		"stats summary sub":               {url: "/stats/summary", bucket: "stats"},
+		"statusz":                         {url: "/statusz", bucket: "statusz"},
 		"invalid path":                    {url: "/junk", bucket: "other"},
 		"invalid path starting with good": {url: "/healthzjunk", bucket: "other"},
 	}