/
githubmirror
/
kubernetes
Обзор
Документация
Войти
/
githubmirror
/
kubernetes
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/integration/serving/serving_test.go
1 039 строк
33 KB
Sergey Kanzhelev
integration tests for configz endpoints
11 мар 2026, 03:25
11 мар 2026, 03:25
4396474
Код
Авторство
О чём код?
/* Copyright 2018 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 serving import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "net" "net/http" "os" "path" "slices" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiserverfeat "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server" flagzv1alpha1 "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" flagzv1beta1 "k8s.io/apiserver/pkg/server/flagz/api/v1beta1" flagztesting "k8s.io/apiserver/pkg/server/flagz/testing" "k8s.io/apiserver/pkg/server/options" statuszv1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" statuszv1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1" statusztesting "k8s.io/apiserver/pkg/server/statusz/testing" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" cloudprovider "k8s.io/cloud-provider" cloudctrlmgrtesting "k8s.io/cloud-provider/app/testing" cloudproviderconfigv1alpha1 "k8s.io/cloud-provider/config/v1alpha1" "k8s.io/cloud-provider/fake" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/prometheus/clientgo/fifo" "k8s.io/component-base/metrics/testutil" zpagesfeatures "k8s.io/component-base/zpages/features" "k8s.io/klog/v2/ktesting" kubecontrollermanagerconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" kubectrlmgrtesting "k8s.io/kubernetes/cmd/kube-controller-manager/app/testing" kubeschedulertesting "k8s.io/kubernetes/cmd/kube-scheduler/app/testing" "k8s.io/kubernetes/test/integration/framework" "k8s.io/utils/ptr" ) type componentTester interface { StartTestServer(t *testing.T, ctx context.Context, customFlags []string) (*options.SecureServingOptions, *server.SecureServingInfo, func(), error) } type kubeControllerManagerTester struct{} func (kubeControllerManagerTester) StartTestServer(t *testing.T, ctx context.Context, customFlags []string) (*options.SecureServingOptions, *server.SecureServingInfo, func(), error) { // avoid starting any controller loops, we're just testing serving customFlags = append([]string{"--controllers="}, customFlags...) gotResult, err := kubectrlmgrtesting.StartTestServer(t, ctx, customFlags) if err != nil { return nil, nil, nil, err } return gotResult.Options.SecureServing, gotResult.Config.SecureServing, gotResult.TearDownFn, err } type cloudControllerManagerTester struct{} func (cloudControllerManagerTester) StartTestServer(t *testing.T, ctx context.Context, customFlags []string) (*options.SecureServingOptions, *server.SecureServingInfo, func(), error) { gotResult, err := cloudctrlmgrtesting.StartTestServer(t, ctx, customFlags) if err != nil { return nil, nil, nil, err } return gotResult.Options.SecureServing.SecureServingOptions, gotResult.Config.SecureServing, gotResult.TearDownFn, err } type kubeSchedulerTester struct{} func (kubeSchedulerTester) StartTestServer(t *testing.T, ctx context.Context, customFlags []string) (*options.SecureServingOptions, *server.SecureServingInfo, func(), error) { gotResult, err := kubeschedulertesting.StartTestServer(t, ctx, customFlags) if err != nil { return nil, nil, nil, err } return gotResult.Options.SecureServing, gotResult.Config.SecureServing, gotResult.TearDownFn, err } func TestComponentSecureServingAndAuth(t *testing.T) { if !cloudprovider.IsCloudProvider("fake") { cloudprovider.RegisterCloudProvider("fake", fakeCloudProviderFactory) } // Insulate this test from picking up in-cluster config when run inside a pod // We can't assume we have permissions to write to /var/run/secrets/... from a unit test to mock in-cluster config for testing if len(os.Getenv("KUBERNETES_SERVICE_HOST")) > 0 { t.Setenv("KUBERNETES_SERVICE_HOST", "") } // authenticate to apiserver via bearer token token := "flwqkenfjasasdfmwerasd" // Fake token for testing. tokenFile, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } tokenFile.WriteString(fmt.Sprintf(` %s,system:kube-controller-manager,system:kube-controller-manager,"" `, token)) tokenFile.Close() // start apiserver server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ "--token-auth-file", tokenFile.Name(), "--authorization-mode", "RBAC", }, framework.SharedEtcd()) defer server.TearDownFn() // create kubeconfig for the apiserver apiserverConfig, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } apiserverConfig.WriteString(fmt.Sprintf(` apiVersion: v1 kind: Config clusters: - cluster: server: %s certificate-authority: %s name: integration contexts: - context: cluster: integration user: controller-manager name: default-context current-context: default-context users: - name: controller-manager user: token: %s `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token)) apiserverConfig.Close() // create BROKEN kubeconfig for the apiserver brokenApiserverConfig, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } brokenApiserverConfig.WriteString(fmt.Sprintf(` apiVersion: v1 kind: Config clusters: - cluster: server: %s certificate-authority: %s name: integration contexts: - context: cluster: integration user: controller-manager name: default-context current-context: default-context users: - name: controller-manager user: token: WRONGTOKEN `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile)) brokenApiserverConfig.Close() tests := []struct { name string tester componentTester extraFlags []string }{ {"kube-controller-manager", kubeControllerManagerTester{}, nil}, {"cloud-controller-manager", cloudControllerManagerTester{}, []string{"--cloud-provider=fake", "--webhook-secure-port=0"}}, {"kube-scheduler", kubeSchedulerTester{}, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testComponentWithSecureServing(t, tt.tester, apiserverConfig.Name(), brokenApiserverConfig.Name(), token, tt.extraFlags) }) } } func testComponentWithSecureServing(t *testing.T, tester componentTester, kubeconfig, brokenKubeconfig, token string, extraFlags []string) { tests := []struct { name string flags []string path string anonymous bool // to use the token or not wantErr bool wantSecureCode *int }{ {"no-flags", nil, "/healthz", false, true, nil}, {"/healthz without authn/authz", []string{ "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/healthz", true, false, ptr.To(http.StatusOK)}, {"/metrics without authn/authz", []string{ "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/metrics", true, false, ptr.To(http.StatusForbidden)}, {"authorization skipped for /healthz with authn/authz", []string{ "--authentication-kubeconfig", kubeconfig, "--authorization-kubeconfig", kubeconfig, "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/healthz", false, false, ptr.To(http.StatusOK)}, {"authorization skipped for /healthz with BROKEN authn/authz", []string{ "--authentication-skip-lookup", // to survive inaccessible extensions-apiserver-authentication configmap "--authentication-kubeconfig", brokenKubeconfig, "--authorization-kubeconfig", brokenKubeconfig, "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/healthz", false, false, ptr.To(http.StatusOK)}, {"not authorized /metrics with BROKEN authn/authz", []string{ "--authentication-kubeconfig", kubeconfig, "--authorization-kubeconfig", brokenKubeconfig, "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/metrics", false, false, ptr.To(http.StatusInternalServerError)}, {"always-allowed /metrics with BROKEN authn/authz", []string{ "--authentication-skip-lookup", // to survive inaccessible extensions-apiserver-authentication configmap "--authentication-kubeconfig", brokenKubeconfig, "--authorization-kubeconfig", brokenKubeconfig, "--authorization-always-allow-paths", "/healthz,/metrics", "--kubeconfig", kubeconfig, "--leader-elect=false", }, "/metrics", false, false, ptr.To(http.StatusOK)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, ctx := ktesting.NewTestContext(t) secureOptions, secureInfo, tearDownFn, err := tester.StartTestServer(t, ctx, slices.Concat(tt.flags, extraFlags)) if tearDownFn != nil { defer tearDownFn() } if (err != nil) != tt.wantErr { t.Fatalf("StartTestServer() error = %v, wantErr %v", err, tt.wantErr) } if err != nil { return } if want, got := tt.wantSecureCode != nil, secureInfo != nil; want != got { t.Errorf("SecureServing enabled: expected=%v got=%v", want, got) } else if want { url := fmt.Sprintf("https://%s%s", secureInfo.Listener.Addr().String(), tt.path) url = strings.Replace(url, "[::]", "127.0.0.1", -1) // switch to IPv4 because the self-signed cert does not support [::] // read self-signed server cert disk pool := x509.NewCertPool() serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt") serverCert, err := os.ReadFile(serverCertPath) if err != nil { t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err) } pool.AppendCertsFromPEM(serverCert) tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, }, } client := &http.Client{Transport: tr} req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatal(err) } if !tt.anonymous { req.Header.Add("Authorization", fmt.Sprintf("Token %s", token)) } r, err := client.Do(req) if err != nil { t.Fatalf("failed to GET %s from component: %v", tt.path, err) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } defer r.Body.Close() if got, expected := r.StatusCode, *tt.wantSecureCode; got != expected { t.Fatalf("expected http %d at %s of component, got: %d %q", expected, tt.path, got, string(body)) } } }) } } func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) { return &fake.Cloud{ DisableRoutes: true, // disable routes for server tests, otherwise --cluster-cidr is required }, nil } func TestKubeControllerManagerServingZPages(t *testing.T) { // authenticate to apiserver via bearer token token := "flwqkenfjasasdfmwerasd" // Fake token for testing. tokenFile, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = tokenFile.WriteString(fmt.Sprintf(` %s,system:kube-controller-manager,system:kube-controller-manager,"" `, token)); err != nil { t.Fatal(err) } if err = tokenFile.Close(); err != nil { t.Fatal(err) } // start apiserver server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ "--token-auth-file", tokenFile.Name(), "--authorization-mode", "RBAC", }, framework.SharedEtcd()) defer server.TearDownFn() // create kubeconfig for the apiserver apiserverConfig, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = apiserverConfig.WriteString(fmt.Sprintf(` apiVersion: v1 kind: Config clusters: - cluster: server: %s certificate-authority: %s name: integration contexts: - context: cluster: integration user: controller-manager name: default-context current-context: default-context users: - name: controller-manager user: token: %s `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token)); err != nil { t.Fatal(err) } if err = apiserverConfig.Close(); err != nil { t.Fatal(err) } statuszWantBodyText := "kube-controller-manager statusz\nWarning: This endpoint is not meant to be machine parseable" statuszWantBodyAlpha1 := &statuszv1alpha1.Statusz{ TypeMeta: metav1.TypeMeta{ Kind: "Statusz", APIVersion: "config.k8s.io/v1alpha1", }, ObjectMeta: metav1.ObjectMeta{ Name: "kube-controller-manager", }, Paths: []string{"/configz", "/flagz", "/healthz", "/metrics"}, } statuszWantBodyBeta1 := &statuszv1beta1.Statusz{ TypeMeta: metav1.TypeMeta{ Kind: "Statusz", APIVersion: "config.k8s.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: "kube-controller-manager", }, Paths: []string{"/configz", "/flagz", "/healthz", "/metrics"}, } flagzWantBodyText := "kube-controller-manager flagz\nWarning: This endpoint is not meant to be machine parseable" flagzWantBodyStructuredAlpha := &flagzv1alpha1.Flagz{ TypeMeta: metav1.TypeMeta{ Kind: "Flagz", APIVersion: "config.k8s.io/v1alpha1", }, ObjectMeta: metav1.ObjectMeta{ Name: "kube-controller-manager", }, } flagzWantBodyStructuredBeta := &flagzv1beta1.Flagz{ TypeMeta: metav1.TypeMeta{ Kind: "Flagz", APIVersion: "config.k8s.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: "kube-controller-manager", }, } statuszTestCases := []struct { name string acceptHeader string wantStatus int wantBodyText string wantBodyStructured interface{} wantDeprecationHeader bool }{ { name: "text plain response", acceptHeader: "text/plain", wantStatus: http.StatusOK, wantBodyText: statuszWantBodyText, }, { name: "structured json response", acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Statusz", wantStatus: http.StatusOK, wantBodyStructured: statuszWantBodyBeta1, }, { name: "no accept header (defaults to text)", acceptHeader: "", wantStatus: http.StatusOK, wantBodyText: statuszWantBodyText, }, { 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=v1beta1;g=config.k8s.io", wantStatus: http.StatusNotAcceptable, }, { name: "wildcard accept header", acceptHeader: "*/*", wantStatus: http.StatusOK, wantBodyText: statuszWantBodyText, }, { name: "bad json header fall back wildcard", acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*", wantStatus: http.StatusOK, wantBodyText: statuszWantBodyText, }, { name: "structured yaml response", acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Statusz", wantStatus: http.StatusOK, wantBodyStructured: statuszWantBodyBeta1, }, { name: "structured cbor response", acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Statusz", wantStatus: http.StatusOK, wantBodyStructured: statuszWantBodyBeta1, }, { name: "alpha specified before beta, should show warning", acceptHeader: "application/json;g=config.k8s.io;v=v1alpha1;as=Statusz,application/json;g=config.k8s.io;v=v1beta1;as=Statusz", wantStatus: http.StatusOK, wantBodyStructured: statuszWantBodyAlpha1, wantDeprecationHeader: true, }, { name: "beta specified before alpha, no warning", acceptHeader: "application/json;g=config.k8s.io;v=v1beta1;as=Statusz,application/json;g=config.k8s.io;v=v1alpha1;as=Statusz", wantStatus: http.StatusOK, wantBodyStructured: statuszWantBodyBeta1, wantDeprecationHeader: false, }, } flagzTestCases := []struct { name string acceptHeader string wantStatus int wantBodyText string // for text/plain wantBodyStructured interface{} // for structured json wantDeprecationHeader bool }{ { name: "text plain response", acceptHeader: "text/plain", wantStatus: http.StatusOK, wantBodyText: flagzWantBodyText, }, { name: "structured json response", acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz", wantStatus: http.StatusOK, wantBodyStructured: flagzWantBodyStructuredBeta, }, { name: "no accept header (defaults to text)", acceptHeader: "", wantStatus: http.StatusOK, wantBodyText: flagzWantBodyText, }, { 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=v1beta1;g=config.k8s.io", wantStatus: http.StatusNotAcceptable, }, { name: "wildcard accept header", acceptHeader: "*/*", wantStatus: http.StatusOK, wantBodyText: flagzWantBodyText, }, { name: "bad json header fall back wildcard", acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*", wantStatus: http.StatusOK, wantBodyText: flagzWantBodyText, }, { name: "structured cbor response", acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz", wantStatus: http.StatusOK, wantBodyStructured: flagzWantBodyStructuredBeta, }, { name: "structured yaml response", acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz", wantStatus: http.StatusOK, wantBodyStructured: flagzWantBodyStructuredBeta, }, { name: "alpha specified before beta, should show warning", acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz,application/json;v=v1beta1;g=config.k8s.io;as=Flagz", wantStatus: http.StatusOK, wantBodyStructured: flagzWantBodyStructuredAlpha, wantDeprecationHeader: true, }, { name: "beta specified before alpha, no warning", acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz,application/json;v=v1alpha1;g=config.k8s.io;as=Flagz", wantStatus: http.StatusOK, wantBodyStructured: flagzWantBodyStructuredBeta, wantDeprecationHeader: false, }, } featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentStatusz, true) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, zpagesfeatures.ComponentFlagz, true) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiserverfeat.CBORServingAndStorage, true) _, ctx := ktesting.NewTestContext(t) flags := []string{ "--authentication-skip-lookup", "--authentication-kubeconfig", apiserverConfig.Name(), "--authorization-kubeconfig", apiserverConfig.Name(), "--authorization-always-allow-paths", "/statusz,/flagz,/configz", "--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", } secureOptions, secureInfo, tearDownFn, err := kubeControllerManagerTester{}.StartTestServer(t, ctx, flags) if tearDownFn != nil { defer tearDownFn() } if err != nil { t.Fatalf("StartTestServer() error = %v", err) } if secureInfo == nil { t.Fatalf("SecureServing not enabled") } _, port, err := net.SplitHostPort(secureInfo.Listener.Addr().String()) if err != nil { t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err) } statuszURL := fmt.Sprintf("https://127.0.0.1:%s/statusz", port) flagzURL := fmt.Sprintf("https://127.0.0.1:%s/flagz", port) configzURL := fmt.Sprintf("https://127.0.0.1:%s/configz", port) // read self-signed server cert disk pool := x509.NewCertPool() serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt") serverCert, err := os.ReadFile(serverCertPath) if err != nil { t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err) } pool.AppendCertsFromPEM(serverCert) tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, }, } client := &http.Client{Transport: tr} 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) } 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 /statusz: %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.wantBodyText != "" { if !strings.Contains(string(body), tc.wantBodyText) { t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodyText, string(body)) } } if tc.wantBodyStructured != nil { warnings := append([]string{}, r.Header.Values("Warning")...) statusztesting.VerifyStructuredResponse(t, tc.acceptHeader, body, warnings, tc.wantBodyStructured, tc.wantDeprecationHeader) } } }) } 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.wantBodyText != "" { if !strings.Contains(string(body), tc.wantBodyText) { t.Errorf("body missing expected substring: %q\nGot:\n%s", tc.wantBodyText, string(body)) } } if tc.wantBodyStructured != nil { warnings := append([]string{}, r.Header.Values("Warning")...) flagztesting.VerifyStructuredResponse(t, tc.acceptHeader, body, warnings, tc.wantBodyStructured, tc.wantDeprecationHeader) } } }) } t.Run("configz", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, configzURL, nil) if err != nil { t.Fatal(err) } req.Header.Add("Authorization", fmt.Sprintf("Token %s", token)) r, err := client.Do(req) if err != nil { t.Fatalf("failed to GET /configz: %v", err) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } defer func() { _ = r.Body.Close() }() if r.StatusCode != http.StatusOK { t.Fatalf("want status %d, got %d", http.StatusOK, r.StatusCode) } var configz map[string]unstructured.Unstructured if err := json.Unmarshal(body, &configz); err != nil { t.Fatalf("failed to unmarshal configz: %v", err) } cfg, ok := configz["kubecontrollermanager.config.k8s.io"] if !ok { t.Fatalf("configz missing 'kubecontrollermanager.config.k8s.io' key") } if cfg.GetAPIVersion() != "kubecontrollermanager.config.k8s.io/v1alpha1" { t.Errorf("unexpected APIVersion: %s", cfg.GetAPIVersion()) } if cfg.GetKind() != "KubeControllerManagerConfiguration" { t.Errorf("unexpected Kind: %s", cfg.GetKind()) } // confirm that they expose public config type var kcmConfig kubecontrollermanagerconfigv1alpha1.KubeControllerManagerConfiguration err = json.Unmarshal(body, &struct { ComponentConfig *kubecontrollermanagerconfigv1alpha1.KubeControllerManagerConfiguration `json:"kubecontrollermanager.config.k8s.io"` }{ComponentConfig: &kcmConfig}) if err != nil { t.Errorf("failed to deserialize into public config type: %v", err) } }) } func TestKubeControllerManagerInformerMetrics(t *testing.T) { // authenticate to apiserver via bearer token token := "flwqkenfjasasdfmwerasd" // Fake token for testing. tokenFile, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = fmt.Fprintf(tokenFile, ` %s,system:kube-controller-manager,system:kube-controller-manager,"" `, token); err != nil { t.Fatal(err) } if err = tokenFile.Close(); err != nil { t.Fatal(err) } // start apiserver server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ "--token-auth-file", tokenFile.Name(), "--authorization-mode", "RBAC", }, framework.SharedEtcd()) defer server.TearDownFn() // create kubeconfig for the apiserver apiserverConfig, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = fmt.Fprintf(apiserverConfig, ` apiVersion: v1 kind: Config clusters: - cluster: server: %s certificate-authority: %s name: integration contexts: - context: cluster: integration user: controller-manager name: default-context current-context: default-context users: - name: controller-manager user: token: %s `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token); err != nil { t.Fatal(err) } if err = apiserverConfig.Close(); err != nil { t.Fatal(err) } legacyregistry.Reset() cache.ResetInformerNamesForTesting() fifo.Register() _, ctx := ktesting.NewTestContext(t) flags := []string{ "--authentication-skip-lookup", "--authentication-kubeconfig", apiserverConfig.Name(), "--authorization-kubeconfig", apiserverConfig.Name(), "--authorization-always-allow-paths", "/metrics", "--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", // Enable a minimal set of controllers to ensure informers are started and metrics are produced. "--controllers=garbagecollector", } secureOptions, secureInfo, tearDownFn, err := kubeControllerManagerTester{}.StartTestServer(t, ctx, flags) if tearDownFn != nil { defer tearDownFn() } if err != nil { t.Fatalf("StartTestServer() error = %v", err) } if secureInfo == nil { t.Fatalf("SecureServing not enabled") } _, port, err := net.SplitHostPort(secureInfo.Listener.Addr().String()) if err != nil { t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err) } metricsURL := fmt.Sprintf("https://127.0.0.1:%s/metrics", port) // read self-signed server cert disk pool := x509.NewCertPool() serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt") serverCert, err := os.ReadFile(serverCertPath) if err != nil { t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err) } pool.AppendCertsFromPEM(serverCert) tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, }, } client := &http.Client{Transport: tr} req, err := http.NewRequest(http.MethodGet, metricsURL, nil) if err != nil { t.Fatal(err) } req.Header.Add("Authorization", fmt.Sprintf("Token %s", token)) r, err := client.Do(req) if err != nil { t.Fatalf("failed to GET /metrics: %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 != http.StatusOK { t.Fatalf("want status %d, got %d: %s", http.StatusOK, r.StatusCode, string(body)) } // Parse metrics using testutil metrics := testutil.NewMetrics() if err := testutil.ParseMetrics(string(body), &metrics); err != nil { t.Fatalf("failed to parse metrics: %v", err) } // Verify that informer_queued_items metric exists wantMetricName := "informer_queued_items" samples, found := metrics[wantMetricName] if !found { t.Fatalf("expected metrics to contain %q, but it was not found", wantMetricName) } // Verify that at least one sample has the kube-controller-manager name label wantLabelValue := testutil.LabelValue("kube-controller-manager") foundKCM := false for _, sample := range samples { if nameLabel, ok := sample.Metric["name"]; ok && nameLabel == wantLabelValue { foundKCM = true t.Logf("Found expected metric: %s", sample.String()) break } } if !foundKCM { t.Errorf("expected to find informer_queued_items metric with name=\"kube-controller-manager\" label") } } func TestCloudControllerManagerServingZPages(t *testing.T) { if !cloudprovider.IsCloudProvider("fake") { cloudprovider.RegisterCloudProvider("fake", fakeCloudProviderFactory) } // authenticate to apiserver via bearer token token := "flwqkenfjasasdfmwerasd" // Fake token for testing. tokenFile, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = fmt.Fprintf(tokenFile, "\n%s,system:cloud-controller-manager,system:cloud-controller-manager,\"\"\n", token); err != nil { t.Fatal(err) } if err = tokenFile.Close(); err != nil { t.Fatal(err) } // start apiserver server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{ "--token-auth-file", tokenFile.Name(), "--authorization-mode", "RBAC", }, framework.SharedEtcd()) defer server.TearDownFn() // create kubeconfig for the apiserver apiserverConfig, err := os.CreateTemp("", "kubeconfig") if err != nil { t.Fatal(err) } if _, err = fmt.Fprintf(apiserverConfig, ` apiVersion: v1 kind: Config clusters: - cluster: server: %s certificate-authority: %s name: integration contexts: - context: cluster: integration user: cloud-controller-manager name: default-context current-context: default-context users: - name: cloud-controller-manager user: token: %s `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token); err != nil { t.Fatal(err) } if err = apiserverConfig.Close(); err != nil { t.Fatal(err) } _, ctx := ktesting.NewTestContext(t) flags := []string{ "--authentication-skip-lookup", "--authentication-kubeconfig", apiserverConfig.Name(), "--authorization-kubeconfig", apiserverConfig.Name(), "--authorization-always-allow-paths", "/statusz,/flagz,/configz", "--kubeconfig", apiserverConfig.Name(), "--leader-elect=false", "--cloud-provider=fake", "--webhook-secure-port=0", } secureOptions, secureInfo, tearDownFn, err := cloudControllerManagerTester{}.StartTestServer(t, ctx, flags) if tearDownFn != nil { defer tearDownFn() } if err != nil { t.Fatalf("StartTestServer() error = %v", err) } if secureInfo == nil { t.Fatalf("SecureServing not enabled") } _, port, err := net.SplitHostPort(secureInfo.Listener.Addr().String()) if err != nil { t.Fatalf("could not get host and port from %s : %v", secureInfo.Listener.Addr().String(), err) } configzURL := fmt.Sprintf("https://127.0.0.1:%s/configz", port) // read self-signed server cert disk pool := x509.NewCertPool() serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt") serverCert, err := os.ReadFile(serverCertPath) if err != nil { t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err) } pool.AppendCertsFromPEM(serverCert) tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, }, } client := &http.Client{Transport: tr} t.Run("configz", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, configzURL, nil) if err != nil { t.Fatal(err) } req.Header.Add("Authorization", fmt.Sprintf("Token %s", token)) r, err := client.Do(req) if err != nil { t.Fatalf("failed to GET /configz: %v", err) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } defer func() { _ = r.Body.Close() }() if r.StatusCode != http.StatusOK { t.Fatalf("want status %d, got %d", http.StatusOK, r.StatusCode) } var configz map[string]unstructured.Unstructured if err := json.Unmarshal(body, &configz); err != nil { t.Fatalf("failed to unmarshal configz: %v", err) } cfg, ok := configz["cloudcontrollermanager.config.k8s.io"] if !ok { t.Fatalf("configz missing 'cloudcontrollermanager.config.k8s.io' key") } if cfg.GetAPIVersion() != "cloudcontrollermanager.config.k8s.io/v1alpha1" { t.Errorf("unexpected APIVersion: %s", cfg.GetAPIVersion()) } if cfg.GetKind() != "CloudControllerManagerConfiguration" { t.Errorf("unexpected Kind: %s", cfg.GetKind()) } // confirm that they expose public config type var ccmConfig cloudproviderconfigv1alpha1.CloudControllerManagerConfiguration err = json.Unmarshal(body, &struct { ComponentConfig *cloudproviderconfigv1alpha1.CloudControllerManagerConfiguration `json:"cloudcontrollermanager.config.k8s.io"` }{ComponentConfig: &ccmConfig}) if err != nil { t.Errorf("failed to deserialize into public config type: %v", err) } }) }