kubernetes/test/integration/serving/serving_test.go
2026-03-11 00:25:32 +00:00

1039 lines
33 KiB
Go

/*
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)
}
})
}