Skip to content

Commit 5a8d77a

Browse files
committed
Add statusz endpoint for kube-controller-manager
1 parent 8770bd5 commit 5a8d77a

File tree

2 files changed

+152
-4
lines changed

2 files changed

+152
-4
lines changed

cmd/kube-controller-manager/app/controllermanager.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import (
6464
"k8s.io/component-base/term"
6565
utilversion "k8s.io/component-base/version"
6666
"k8s.io/component-base/version/verflag"
67+
zpagesfeatures "k8s.io/component-base/zpages/features"
68+
"k8s.io/component-base/zpages/statusz"
6769
genericcontrollermanager "k8s.io/controller-manager/app"
6870
"k8s.io/controller-manager/controller"
6971
"k8s.io/controller-manager/pkg/clientbuilder"
@@ -91,6 +93,8 @@ const (
9193
ControllerStartJitter = 1.0
9294
// ConfigzName is the name used for register kube-controller manager /configz, same with GroupName.
9395
ConfigzName = "kubecontrollermanager.config.k8s.io"
96+
// kubeControllerManager defines variable used internally when referring to cloud-controller-manager component
97+
kubeControllerManager = "kube-controller-manager"
9498
)
9599

96100
// NewControllerManagerCommand creates a *cobra.Command object with default parameters
@@ -105,7 +109,7 @@ func NewControllerManagerCommand() *cobra.Command {
105109
}
106110

107111
cmd := &cobra.Command{
108-
Use: "kube-controller-manager",
112+
Use: kubeControllerManager,
109113
Long: `The Kubernetes controller manager is a daemon that embeds
110114
the core control loops shipped with Kubernetes. In applications of robotics and
111115
automation, a control loop is a non-terminating loop that regulates the state of
@@ -213,6 +217,10 @@ func Run(ctx context.Context, c *config.CompletedConfig) error {
213217
unsecuredMux = genericcontrollermanager.NewBaseHandler(&c.ComponentConfig.Generic.Debugging, healthzHandler)
214218
slis.SLIMetricsWithReset{}.Install(unsecuredMux)
215219

220+
if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) {
221+
statusz.Install(unsecuredMux, kubeControllerManager, statusz.NewRegistry())
222+
}
223+
216224
handler := genericcontrollermanager.BuildHandlerChain(unsecuredMux, &c.Authorization, &c.Authentication)
217225
// TODO: handle stoppedCh and listenerStoppedCh returned by c.SecureServing.Serve
218226
if _, _, err := c.SecureServing.Serve(handler, 0, stopCh); err != nil {
@@ -267,7 +275,7 @@ func Run(ctx context.Context, c *config.CompletedConfig) error {
267275
logger.Info("starting leader migration")
268276

269277
leaderMigrator = leadermigration.NewLeaderMigrator(&c.ComponentConfig.Generic.LeaderMigration,
270-
"kube-controller-manager")
278+
kubeControllerManager)
271279

272280
// startSATokenControllerInit is the original InitFunc.
273281
startSATokenControllerInit := saTokenControllerDescriptor.GetInitFunc()
@@ -295,7 +303,7 @@ func Run(ctx context.Context, c *config.CompletedConfig) error {
295303
c.Client,
296304
"kube-system",
297305
id,
298-
"kube-controller-manager",
306+
kubeControllerManager,
299307
binaryVersion.FinalizeVersion(),
300308
emulationVersion.FinalizeVersion(),
301309
coordinationv1.OldestEmulationVersion,
@@ -634,7 +642,7 @@ func CreateControllerContext(ctx context.Context, s *config.CompletedConfig, roo
634642
RESTMapper: restMapper,
635643
InformersStarted: make(chan struct{}),
636644
ResyncPeriod: ResyncPeriod(s),
637-
ControllerManagerMetrics: controllersmetrics.NewControllerManagerMetrics("kube-controller-manager"),
645+
ControllerManagerMetrics: controllersmetrics.NewControllerManagerMetrics(kubeControllerManager),
638646
}
639647

640648
if controllerContext.ComponentConfig.GarbageCollectorController.EnableGarbageCollector &&

test/integration/serving/serving_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ import (
3030

3131
"k8s.io/apiserver/pkg/server"
3232
"k8s.io/apiserver/pkg/server/options"
33+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3334
cloudprovider "k8s.io/cloud-provider"
3435
cloudctrlmgrtesting "k8s.io/cloud-provider/app/testing"
3536
"k8s.io/cloud-provider/fake"
37+
featuregatetesting "k8s.io/component-base/featuregate/testing"
38+
zpagesfeatures "k8s.io/component-base/zpages/features"
3639
"k8s.io/klog/v2/ktesting"
3740
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
3841
kubectrlmgrtesting "k8s.io/kubernetes/cmd/kube-controller-manager/app/testing"
@@ -289,3 +292,140 @@ func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) {
289292
DisableRoutes: true, // disable routes for server tests, otherwise --cluster-cidr is required
290293
}, nil
291294
}
295+
296+
func TestKubeControllerManagerServingStatusz(t *testing.T) {
297+
298+
// authenticate to apiserver via bearer token
299+
token := "flwqkenfjasasdfmwerasd" // Fake token for testing.
300+
tokenFile, err := os.CreateTemp("", "kubeconfig")
301+
if err != nil {
302+
t.Fatal(err)
303+
}
304+
if _, err = tokenFile.WriteString(fmt.Sprintf(`
305+
%s,system:kube-controller-manager,system:kube-controller-manager,""
306+
`, token)); err != nil {
307+
t.Fatal(err)
308+
}
309+
if err = tokenFile.Close(); err != nil {
310+
t.Fatal(err)
311+
}
312+
313+
// start apiserver
314+
server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{
315+
"--token-auth-file", tokenFile.Name(),
316+
"--authorization-mode", "RBAC",
317+
}, framework.SharedEtcd())
318+
defer server.TearDownFn()
319+
320+
// create kubeconfig for the apiserver
321+
apiserverConfig, err := os.CreateTemp("", "kubeconfig")
322+
if err != nil {
323+
t.Fatal(err)
324+
}
325+
if _, err = apiserverConfig.WriteString(fmt.Sprintf(`
326+
apiVersion: v1
327+
kind: Config
328+
clusters:
329+
- cluster:
330+
server: %s
331+
certificate-authority: %s
332+
name: integration
333+
contexts:
334+
- context:
335+
cluster: integration
336+
user: controller-manager
337+
name: default-context
338+
current-context: default-context
339+
users:
340+
- name: controller-manager
341+
user:
342+
token: %s
343+
`, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token)); err != nil {
344+
t.Fatal(err)
345+
}
346+
if err = apiserverConfig.Close(); err != nil {
347+
t.Fatal(err)
348+
}
349+
350+
tests := []struct {
351+
name string
352+
flags []string
353+
path string
354+
anonymous bool // to use the token or not
355+
wantErr bool
356+
wantSecureCode *int
357+
}{
358+
{"serving /statusz", []string{
359+
"--authentication-skip-lookup", // to survive inaccessible extensions-apiserver-authentication configmap
360+
"--authentication-kubeconfig", apiserverConfig.Name(),
361+
"--authorization-kubeconfig", apiserverConfig.Name(),
362+
"--authorization-always-allow-paths", "/statusz",
363+
"--kubeconfig", apiserverConfig.Name(),
364+
"--leader-elect=false",
365+
}, "/statusz", false, false, intPtr(http.StatusOK)},
366+
}
367+
for _, tt := range tests {
368+
t.Run(tt.name, func(t *testing.T) {
369+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true)
370+
_, ctx := ktesting.NewTestContext(t)
371+
secureOptions, secureInfo, tearDownFn, err := kubeControllerManagerTester{}.StartTestServer(ctx, append(append([]string{}, tt.flags...), []string{}...))
372+
if tearDownFn != nil {
373+
defer tearDownFn()
374+
}
375+
if (err != nil) != tt.wantErr {
376+
t.Fatalf("StartTestServer() error = %v, wantErr %v", err, tt.wantErr)
377+
}
378+
if err != nil {
379+
return
380+
}
381+
382+
if want, got := tt.wantSecureCode != nil, secureInfo != nil; want != got {
383+
t.Errorf("SecureServing enabled: expected=%v got=%v", want, got)
384+
} else if want {
385+
url := fmt.Sprintf("https://%s%s", secureInfo.Listener.Addr().String(), tt.path)
386+
url = strings.ReplaceAll(url, "[::]", "127.0.0.1") // switch to IPv4 because the self-signed cert does not support [::]
387+
388+
// read self-signed server cert disk
389+
pool := x509.NewCertPool()
390+
serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt")
391+
serverCert, err := os.ReadFile(serverCertPath)
392+
if err != nil {
393+
t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err)
394+
}
395+
pool.AppendCertsFromPEM(serverCert)
396+
tr := &http.Transport{
397+
TLSClientConfig: &tls.Config{
398+
RootCAs: pool,
399+
},
400+
}
401+
402+
client := &http.Client{Transport: tr}
403+
req, err := http.NewRequest(http.MethodGet, url, nil)
404+
req.Header.Set("Accept", "text/plain")
405+
if err != nil {
406+
t.Fatal(err)
407+
}
408+
if !tt.anonymous {
409+
req.Header.Add("Authorization", fmt.Sprintf("Token %s", token))
410+
}
411+
r, err := client.Do(req)
412+
if err != nil {
413+
t.Fatalf("failed to GET %s from component: %v", tt.path, err)
414+
}
415+
416+
if _, err = io.ReadAll(r.Body); err != nil {
417+
t.Fatalf("failed to read response body: %v", err)
418+
}
419+
defer func() {
420+
if err := r.Body.Close(); err != nil {
421+
t.Fatalf("Error closing response body: %v", err)
422+
}
423+
}()
424+
425+
if got, expected := r.StatusCode, *tt.wantSecureCode; got != expected {
426+
t.Fatalf("expected http %d at %s of component, got: %d", expected, tt.path, got)
427+
}
428+
}
429+
})
430+
}
431+
}

0 commit comments

Comments
 (0)