diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index 6d4bf492038..8121eca8374 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -86,6 +86,8 @@ import ( "k8s.io/component-base/tracing" "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" @@ -267,6 +269,13 @@ is checked every 20 seconds (also configurable with a flag).`, return fmt.Errorf("failed to construct kubelet dependencies: %w", err) } + if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentFlagz) { + if cleanFlagSet != nil { + namedFlagSet := map[string]*pflag.FlagSet{server.ComponentKubelet: cleanFlagSet} + kubeletDeps.Flagz = flagz.NamedFlagSetsReader{FlagSets: cliflag.NamedFlagSets{FlagSets: namedFlagSet}} + } + } + if err := checkPermissions(); err != nil { klog.ErrorS(err, "kubelet running with insufficient permissions") } diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 4a182c9a8ec..7812692d70a 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -68,6 +68,7 @@ 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" internalapi "k8s.io/cri-api/pkg/apis" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" @@ -292,6 +293,7 @@ type Dependencies struct { Options []Option // Injected Dependencies + Flagz flagz.Reader Auth server.AuthInterface CAdvisorInterface cadvisor.Interface Cloud cloudprovider.Interface @@ -616,6 +618,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, nodeStatusMaxImages: nodeStatusMaxImages, tracer: tracer, nodeStartupLatencyTracker: kubeDeps.NodeStartupLatencyTracker, + flagz: kubeDeps.Flagz, } if klet.cloud != nil { @@ -1433,6 +1436,9 @@ type Kubelet struct { // Health check kubelet healthChecker watchdog.HealthChecker + + // flagz is the Reader interface to get flags for flagz page. + flagz flagz.Reader } // ListPodStats is delegated to StatsProvider, which implements stats.Provider interface @@ -3084,12 +3090,12 @@ func (kl *Kubelet) BirthCry() { // ListenAndServe runs the kubelet HTTP server. func (kl *Kubelet) ListenAndServe(kubeCfg *kubeletconfiginternal.KubeletConfiguration, tlsOptions *server.TLSOptions, auth server.AuthInterface, tp trace.TracerProvider) { - server.ListenAndServeKubeletServer(kl, kl.resourceAnalyzer, kl.containerManager.GetHealthCheckers(), kubeCfg, tlsOptions, auth, tp) + server.ListenAndServeKubeletServer(kl, kl.resourceAnalyzer, kl.containerManager.GetHealthCheckers(), kl.flagz, kubeCfg, tlsOptions, auth, tp) } // ListenAndServeReadOnly runs the kubelet HTTP server in read-only mode. func (kl *Kubelet) ListenAndServeReadOnly(address net.IP, port uint, tp trace.TracerProvider) { - server.ListenAndServeKubeletReadOnlyServer(kl, kl.resourceAnalyzer, kl.containerManager.GetHealthCheckers(), address, port, tp) + server.ListenAndServeKubeletReadOnlyServer(kl, kl.resourceAnalyzer, kl.containerManager.GetHealthCheckers(), kl.flagz, address, port, tp) } // ListenAndServePodResources runs the kubelet podresources grpc service diff --git a/pkg/kubelet/server/auth.go b/pkg/kubelet/server/auth.go index 7e7fdd9643a..176f6a95379 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/flagz" "k8s.io/component-base/zpages/statusz" "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/features" @@ -74,6 +75,7 @@ func isSubpath(subpath, path string) bool { // /runningPods/* => verb=, resource=nodes, name=, subresource(s)=pods,proxy // /healthz/* => verb=, resource=nodes, name=, subresource(s)=healthz,proxy // /configz => verb=, resource=nodes, name=, subresource(s)=configz,proxy +// /flagz => verb=, resource=nodes, name=, subresource(s)=configz,proxy func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) []authorizer.Attributes { apiVerb := "" @@ -120,6 +122,8 @@ func (n nodeAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *htt subresources = append(subresources, "checkpoint") case isSubpath(requestPath, statusz.DefaultStatuszPath): subresources = append(subresources, "statusz") + case isSubpath(requestPath, flagz.DefaultFlagzPath): + subresources = append(subresources, "configz") default: subresources = append(subresources, "proxy") } diff --git a/pkg/kubelet/server/auth_test.go b/pkg/kubelet/server/auth_test.go index e3d561d8f63..05874015bc9 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"}, + "/flagz": {"configz"}, "/statusz": {"statusz"}, "/containerLogs/{podNamespace}/{podID}/{containerName}": {"proxy"}, "/debug/flags/v": {"proxy"}, diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go index 35f93f161e6..c88e1d60d81 100644 --- a/pkg/kubelet/server/server.go +++ b/pkg/kubelet/server/server.go @@ -69,6 +69,7 @@ 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" "k8s.io/component-base/zpages/statusz" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" "k8s.io/cri-client/pkg/util" @@ -117,6 +118,7 @@ const ( // Server is a http.Handler which exposes kubelet functionality over HTTP. type Server struct { + flagz flagz.Reader auth AuthInterface host HostInterface restfulCont containerInterface @@ -167,6 +169,7 @@ func ListenAndServeKubeletServer( host HostInterface, resourceAnalyzer stats.ResourceAnalyzer, checkers []healthz.HealthChecker, + flagz flagz.Reader, kubeCfg *kubeletconfiginternal.KubeletConfiguration, tlsOptions *TLSOptions, auth AuthInterface, @@ -175,7 +178,7 @@ func ListenAndServeKubeletServer( address := netutils.ParseIPSloppy(kubeCfg.Address) port := uint(kubeCfg.Port) klog.InfoS("Starting to listen", "address", address, "port", port) - handler := NewServer(host, resourceAnalyzer, checkers, auth, kubeCfg) + handler := NewServer(host, resourceAnalyzer, checkers, flagz, auth, kubeCfg) if utilfeature.DefaultFeatureGate.Enabled(features.KubeletTracing) { handler.InstallTracingFilter(tp) @@ -210,11 +213,12 @@ func ListenAndServeKubeletReadOnlyServer( host HostInterface, resourceAnalyzer stats.ResourceAnalyzer, checkers []healthz.HealthChecker, + flagz flagz.Reader, address net.IP, port uint, tp oteltrace.TracerProvider) { klog.InfoS("Starting to listen read-only", "address", address, "port", port) - s := NewServer(host, resourceAnalyzer, checkers, nil, nil) + s := NewServer(host, resourceAnalyzer, checkers, nil, nil, nil) if utilfeature.DefaultFeatureGate.Enabled(features.KubeletTracing) { s.InstallTracingFilter(tp, otelrestful.WithPublicEndpoint()) @@ -291,10 +295,12 @@ func NewServer( host HostInterface, resourceAnalyzer stats.ResourceAnalyzer, checkers []healthz.HealthChecker, + flagz flagz.Reader, auth AuthInterface, kubeCfg *kubeletconfiginternal.KubeletConfiguration) Server { server := Server{ + flagz: flagz, host: host, resourceAnalyzer: resourceAnalyzer, auth: auth, @@ -575,6 +581,13 @@ func (s *Server) InstallAuthRequiredHandlers() { statusz.Install(s.restfulCont, ComponentKubelet, statusz.NewRegistry(compatibility.DefaultBuildEffectiveVersion())) } + if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentFlagz) { + if s.flagz != nil { + s.addMetricsBucketMatcher("flagz") + flagz.Install(s.restfulCont, ComponentKubelet, s.flagz) + } + } + // The /runningpods endpoint is used for testing only. s.addMetricsBucketMatcher("runningpods") ws = new(restful.WebService) diff --git a/pkg/kubelet/server/server_test.go b/pkg/kubelet/server/server_test.go index fe58760dd4e..8a9c2f5d1c9 100644 --- a/pkg/kubelet/server/server_test.go +++ b/pkg/kubelet/server/server_test.go @@ -59,6 +59,7 @@ import ( 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" @@ -373,6 +374,7 @@ func newServerTestWithDebuggingHandlers(kubeCfg *kubeletconfiginternal.KubeletCo fw.fakeKubelet, stats.NewResourceAnalyzer(fw.fakeKubelet, time.Minute, &record.FakeRecorder{}), []healthz.HealthChecker{}, + flagz.NamedFlagSetsReader{}, fw.fakeAuth, kubeCfg, ) @@ -646,6 +648,7 @@ 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) + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true) fw := newServerTest() defer fw.testHTTPServer.Close() @@ -1729,9 +1732,11 @@ func TestMetricBuckets(t *testing.T) { "stats": {url: "/stats/", bucket: "stats"}, "stats summary sub": {url: "/stats/summary", bucket: "stats"}, "statusz": {url: "/statusz", bucket: "statusz"}, + "/flagz": {url: "/flagz", bucket: "flagz"}, "invalid path": {url: "/junk", bucket: "other"}, "invalid path starting with good": {url: "/healthzjunk", bucket: "other"}, } + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true) fw := newServerTest() defer fw.testHTTPServer.Close() @@ -1960,8 +1965,8 @@ func TestNewServerRegistersMetricsSLIsEndpointTwice(t *testing.T) { } resourceAnalyzer := stats.NewResourceAnalyzer(nil, time.Minute, &record.FakeRecorder{}) - server1 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, nil, nil) - server2 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, nil, nil) + server1 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, flagz.NamedFlagSetsReader{}, nil, nil) + server2 := NewServer(host, resourceAnalyzer, []healthz.HealthChecker{}, flagz.NamedFlagSetsReader{}, nil, nil) // Check if both servers registered the /metrics/slis endpoint assert.Contains(t, server1.restfulCont.RegisteredHandlePaths(), "/metrics/slis", "First server should register /metrics/slis") diff --git a/staging/src/k8s.io/component-base/zpages/flagz/flagz.go b/staging/src/k8s.io/component-base/zpages/flagz/flagz.go index 0db77d4a78a..99a2f44824d 100644 --- a/staging/src/k8s.io/component-base/zpages/flagz/flagz.go +++ b/staging/src/k8s.io/component-base/zpages/flagz/flagz.go @@ -30,6 +30,8 @@ import ( ) 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. @@ -56,7 +58,7 @@ func Install(m mux, componentName string, flagReader Reader) { } func (reg *registry) installHandler(m mux, componentName string, flagReader Reader) { - m.Handle("/flagz", reg.handleFlags(componentName, flagReader)) + m.Handle(DefaultFlagzPath, reg.handleFlags(componentName, flagReader)) } func (reg *registry) handleFlags(componentName string, flagReader Reader) http.HandlerFunc { 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 index 09d22d82f9c..c8568c8b527 100644 --- a/staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go +++ b/staging/src/k8s.io/component-base/zpages/flagz/flagz_test.go @@ -85,7 +85,7 @@ func TestFlagz(t *testing.T) { mux := http.NewServeMux() Install(mux, componentName, test.flagzReader) - req, err := http.NewRequest(http.MethodGet, "http://example.com/flagz", nil) + 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) }