k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kube-apiserver/app/testing/testserver.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testing 18 19 import ( 20 "context" 21 "crypto/ecdsa" 22 "crypto/elliptic" 23 "crypto/rand" 24 "crypto/rsa" 25 "crypto/x509" 26 "crypto/x509/pkix" 27 "encoding/pem" 28 "fmt" 29 "math" 30 "math/big" 31 "net" 32 "os" 33 "path/filepath" 34 "runtime" 35 "testing" 36 "time" 37 38 "github.com/spf13/pflag" 39 "go.etcd.io/etcd/client/pkg/v3/transport" 40 clientv3 "go.etcd.io/etcd/client/v3" 41 "google.golang.org/grpc" 42 43 "k8s.io/apimachinery/pkg/api/errors" 44 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 45 utilerrors "k8s.io/apimachinery/pkg/util/errors" 46 "k8s.io/apimachinery/pkg/util/wait" 47 serveroptions "k8s.io/apiserver/pkg/server/options" 48 "k8s.io/apiserver/pkg/storage/storagebackend" 49 "k8s.io/apiserver/pkg/storageversion" 50 utilfeature "k8s.io/apiserver/pkg/util/feature" 51 "k8s.io/client-go/kubernetes" 52 restclient "k8s.io/client-go/rest" 53 clientgotransport "k8s.io/client-go/transport" 54 "k8s.io/client-go/util/cert" 55 "k8s.io/client-go/util/keyutil" 56 logsapi "k8s.io/component-base/logs/api/v1" 57 "k8s.io/klog/v2" 58 "k8s.io/kube-aggregator/pkg/apiserver" 59 "k8s.io/kubernetes/pkg/features" 60 testutil "k8s.io/kubernetes/test/utils" 61 "k8s.io/kubernetes/test/utils/ktesting" 62 63 "k8s.io/kubernetes/cmd/kube-apiserver/app" 64 "k8s.io/kubernetes/cmd/kube-apiserver/app/options" 65 ) 66 67 func init() { 68 // If instantiated more than once or together with other servers, the 69 // servers would try to modify the global logging state. This must get 70 // ignored during testing. 71 logsapi.ReapplyHandling = logsapi.ReapplyHandlingIgnoreUnchanged 72 } 73 74 // This key is for testing purposes only and is not considered secure. 75 const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- 76 MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 77 AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 78 /IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== 79 -----END EC PRIVATE KEY-----` 80 81 // TearDownFunc is to be called to tear down a test server. 82 type TearDownFunc func() 83 84 // TestServerInstanceOptions Instance options the TestServer 85 type TestServerInstanceOptions struct { 86 // SkipHealthzCheck returns without waiting for the server to become healthy. 87 // Useful for testing server configurations expected to prevent /healthz from completing. 88 SkipHealthzCheck bool 89 // Enable cert-auth for the kube-apiserver 90 EnableCertAuth bool 91 // Wrap the storage version interface of the created server's generic server. 92 StorageVersionWrapFunc func(storageversion.Manager) storageversion.Manager 93 // CA file used for requestheader authn during communication between: 94 // 1. kube-apiserver and peer when the local apiserver is not able to serve the request due 95 // to version skew 96 // 2. kube-apiserver and aggregated apiserver 97 98 // We specify this as on option to pass a common proxyCA to multiple apiservers to simulate 99 // an apiserver version skew scenario where all apiservers use the same proxyCA to verify client connections. 100 ProxyCA *ProxyCA 101 } 102 103 // TestServer return values supplied by kube-test-ApiServer 104 type TestServer struct { 105 ClientConfig *restclient.Config // Rest client config 106 ServerOpts *options.ServerRunOptions // ServerOpts 107 TearDownFn TearDownFunc // TearDown function 108 TmpDir string // Temp Dir used, by the apiserver 109 EtcdClient *clientv3.Client // used by tests that need to check data migrated from APIs that are no longer served 110 EtcdStoragePrefix string // storage prefix in etcd 111 } 112 113 // Logger allows t.Testing and b.Testing to be passed to StartTestServer and StartTestServerOrDie 114 type Logger interface { 115 Helper() 116 Errorf(format string, args ...interface{}) 117 Fatalf(format string, args ...interface{}) 118 Logf(format string, args ...interface{}) 119 Cleanup(func()) 120 } 121 122 // ProxyCA contains the certificate authority certificate and key which is used to verify client connections 123 // to kube-apiservers. The clients can be : 124 // 1. aggregated apiservers 125 // 2. peer kube-apiservers 126 type ProxyCA struct { 127 ProxySigningCert *x509.Certificate 128 ProxySigningKey *rsa.PrivateKey 129 } 130 131 // NewDefaultTestServerOptions Default options for TestServer instances 132 func NewDefaultTestServerOptions() *TestServerInstanceOptions { 133 return &TestServerInstanceOptions{ 134 EnableCertAuth: true, 135 } 136 } 137 138 // StartTestServer starts a etcd server and kube-apiserver. A rest client config and a tear-down func, 139 // and location of the tmpdir are returned. 140 // 141 // Note: we return a tear-down func instead of a stop channel because the later will leak temporary 142 // files that because Golang testing's call to os.Exit will not give a stop channel go routine 143 // enough time to remove temporary files. 144 func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions, customFlags []string, storageConfig *storagebackend.Config) (result TestServer, err error) { 145 tCtx := ktesting.Init(t) 146 147 if instanceOptions == nil { 148 instanceOptions = NewDefaultTestServerOptions() 149 } 150 151 result.TmpDir, err = os.MkdirTemp("", "kubernetes-kube-apiserver") 152 if err != nil { 153 return result, fmt.Errorf("failed to create temp dir: %v", err) 154 } 155 156 var errCh chan error 157 tearDown := func() { 158 // Cancel is stopping apiserver and cleaning up 159 // after itself, including shutting down its storage layer. 160 tCtx.Cancel("tearing down") 161 162 // If the apiserver was started, let's wait for it to 163 // shutdown clearly. 164 if errCh != nil { 165 err, ok := <-errCh 166 if ok && err != nil { 167 klog.Errorf("Failed to shutdown test server clearly: %v", err) 168 } 169 } 170 os.RemoveAll(result.TmpDir) 171 } 172 defer func() { 173 if result.TearDownFn == nil { 174 tearDown() 175 } 176 }() 177 178 fs := pflag.NewFlagSet("test", pflag.PanicOnError) 179 180 s := options.NewServerRunOptions() 181 for _, f := range s.Flags().FlagSets { 182 fs.AddFlagSet(f) 183 } 184 185 s.SecureServing.Listener, s.SecureServing.BindPort, err = createLocalhostListenerOnFreePort() 186 if err != nil { 187 return result, fmt.Errorf("failed to create listener: %v", err) 188 } 189 s.SecureServing.ServerCert.CertDirectory = result.TmpDir 190 191 if instanceOptions.EnableCertAuth { 192 // set up default headers for request header auth 193 reqHeaders := serveroptions.NewDelegatingAuthenticationOptions() 194 s.Authentication.RequestHeader = &reqHeaders.RequestHeader 195 196 var proxySigningKey *rsa.PrivateKey 197 var proxySigningCert *x509.Certificate 198 199 if instanceOptions.ProxyCA != nil { 200 // use provided proxyCA 201 proxySigningKey = instanceOptions.ProxyCA.ProxySigningKey 202 proxySigningCert = instanceOptions.ProxyCA.ProxySigningCert 203 204 } else { 205 // create certificates for aggregation and client-cert auth 206 proxySigningKey, err = testutil.NewPrivateKey() 207 if err != nil { 208 return result, err 209 } 210 proxySigningCert, err = cert.NewSelfSignedCACert(cert.Config{CommonName: "front-proxy-ca"}, proxySigningKey) 211 if err != nil { 212 return result, err 213 } 214 } 215 proxyCACertFile := filepath.Join(s.SecureServing.ServerCert.CertDirectory, "proxy-ca.crt") 216 if err := os.WriteFile(proxyCACertFile, testutil.EncodeCertPEM(proxySigningCert), 0644); err != nil { 217 return result, err 218 } 219 s.Authentication.RequestHeader.ClientCAFile = proxyCACertFile 220 221 // give the kube api server an "identity" it can use to for request header auth 222 // so that aggregated api servers can understand who the calling user is 223 s.Authentication.RequestHeader.AllowedNames = []string{"ash", "misty", "brock"} 224 225 // create private key 226 signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 227 if err != nil { 228 return result, err 229 } 230 231 // make a client certificate for the api server - common name has to match one of our defined names above 232 serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) 233 if err != nil { 234 return result, err 235 } 236 serial = new(big.Int).Add(serial, big.NewInt(1)) 237 tenThousandHoursLater := time.Now().Add(10_000 * time.Hour) 238 certTmpl := x509.Certificate{ 239 Subject: pkix.Name{ 240 CommonName: "misty", 241 }, 242 SerialNumber: serial, 243 NotBefore: proxySigningCert.NotBefore, 244 NotAfter: tenThousandHoursLater, 245 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 246 ExtKeyUsage: []x509.ExtKeyUsage{ 247 x509.ExtKeyUsageClientAuth, 248 }, 249 BasicConstraintsValid: true, 250 } 251 certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, proxySigningCert, signer.Public(), proxySigningKey) 252 if err != nil { 253 return result, err 254 } 255 clientCrtOfAPIServer, err := x509.ParseCertificate(certDERBytes) 256 if err != nil { 257 return result, err 258 } 259 260 // write the cert to disk 261 certificatePath := filepath.Join(s.SecureServing.ServerCert.CertDirectory, "misty-crt.crt") 262 certBlock := pem.Block{ 263 Type: "CERTIFICATE", 264 Bytes: clientCrtOfAPIServer.Raw, 265 } 266 certBytes := pem.EncodeToMemory(&certBlock) 267 if err := cert.WriteCert(certificatePath, certBytes); err != nil { 268 return result, err 269 } 270 271 // write the key to disk 272 privateKeyPath := filepath.Join(s.SecureServing.ServerCert.CertDirectory, "misty-crt.key") 273 encodedPrivateKey, err := keyutil.MarshalPrivateKeyToPEM(signer) 274 if err != nil { 275 return result, err 276 } 277 if err := keyutil.WriteKey(privateKeyPath, encodedPrivateKey); err != nil { 278 return result, err 279 } 280 281 s.ProxyClientKeyFile = filepath.Join(s.SecureServing.ServerCert.CertDirectory, "misty-crt.key") 282 s.ProxyClientCertFile = filepath.Join(s.SecureServing.ServerCert.CertDirectory, "misty-crt.crt") 283 284 clientSigningKey, err := testutil.NewPrivateKey() 285 if err != nil { 286 return result, err 287 } 288 clientSigningCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "client-ca"}, clientSigningKey) 289 if err != nil { 290 return result, err 291 } 292 clientCACertFile := filepath.Join(s.SecureServing.ServerCert.CertDirectory, "client-ca.crt") 293 if err := os.WriteFile(clientCACertFile, testutil.EncodeCertPEM(clientSigningCert), 0644); err != nil { 294 return result, err 295 } 296 s.Authentication.ClientCert.ClientCA = clientCACertFile 297 if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) { 298 // TODO: set up a general clean up for testserver 299 if clientgotransport.DialerStopCh == wait.NeverStop { 300 ctx, cancel := context.WithTimeout(context.Background(), time.Hour) 301 t.Cleanup(cancel) 302 clientgotransport.DialerStopCh = ctx.Done() 303 } 304 s.PeerCAFile = filepath.Join(s.SecureServing.ServerCert.CertDirectory, s.SecureServing.ServerCert.PairName+".crt") 305 } 306 } 307 308 s.SecureServing.ExternalAddress = s.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device 309 310 pkgPath, err := pkgPath(t) 311 if err != nil { 312 return result, err 313 } 314 s.SecureServing.ServerCert.FixtureDirectory = filepath.Join(pkgPath, "testdata") 315 316 s.ServiceClusterIPRanges = "10.0.0.0/16" 317 s.Etcd.StorageConfig = *storageConfig 318 s.APIEnablement.RuntimeConfig.Set("api/all=true") 319 320 if err := fs.Parse(customFlags); err != nil { 321 return result, err 322 } 323 324 saSigningKeyFile, err := os.CreateTemp("/tmp", "insecure_test_key") 325 if err != nil { 326 t.Fatalf("create temp file failed: %v", err) 327 } 328 defer os.RemoveAll(saSigningKeyFile.Name()) 329 if err = os.WriteFile(saSigningKeyFile.Name(), []byte(ecdsaPrivateKey), 0666); err != nil { 330 t.Fatalf("write file %s failed: %v", saSigningKeyFile.Name(), err) 331 } 332 s.ServiceAccountSigningKeyFile = saSigningKeyFile.Name() 333 s.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"} 334 s.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()} 335 336 completedOptions, err := s.Complete() 337 if err != nil { 338 return result, fmt.Errorf("failed to set default ServerRunOptions: %v", err) 339 } 340 341 if errs := completedOptions.Validate(); len(errs) != 0 { 342 return result, fmt.Errorf("failed to validate ServerRunOptions: %v", utilerrors.NewAggregate(errs)) 343 } 344 345 t.Logf("runtime-config=%v", completedOptions.APIEnablement.RuntimeConfig) 346 t.Logf("Starting kube-apiserver on port %d...", s.SecureServing.BindPort) 347 348 config, err := app.NewConfig(completedOptions) 349 if err != nil { 350 return result, err 351 } 352 completed, err := config.Complete() 353 if err != nil { 354 return result, err 355 } 356 server, err := app.CreateServerChain(completed) 357 if err != nil { 358 return result, fmt.Errorf("failed to create server chain: %v", err) 359 } 360 if instanceOptions.StorageVersionWrapFunc != nil { 361 server.GenericAPIServer.StorageVersionManager = instanceOptions.StorageVersionWrapFunc(server.GenericAPIServer.StorageVersionManager) 362 } 363 364 errCh = make(chan error) 365 go func() { 366 defer close(errCh) 367 prepared, err := server.PrepareRun() 368 if err != nil { 369 errCh <- err 370 } else if err := prepared.Run(tCtx); err != nil { 371 errCh <- err 372 } 373 }() 374 375 client, err := kubernetes.NewForConfig(server.GenericAPIServer.LoopbackClientConfig) 376 if err != nil { 377 return result, fmt.Errorf("failed to create a client: %v", err) 378 } 379 380 if !instanceOptions.SkipHealthzCheck { 381 t.Logf("Waiting for /healthz to be ok...") 382 383 // wait until healthz endpoint returns ok 384 err = wait.Poll(100*time.Millisecond, time.Minute, func() (bool, error) { 385 select { 386 case err := <-errCh: 387 return false, err 388 default: 389 } 390 391 req := client.CoreV1().RESTClient().Get().AbsPath("/healthz") 392 // The storage version bootstrap test wraps the storage version post-start 393 // hook, so the hook won't become health when the server bootstraps 394 if instanceOptions.StorageVersionWrapFunc != nil { 395 // We hardcode the param instead of having a new instanceOptions field 396 // to avoid confusing users with more options. 397 storageVersionCheck := fmt.Sprintf("poststarthook/%s", apiserver.StorageVersionPostStartHookName) 398 req.Param("exclude", storageVersionCheck) 399 } 400 result := req.Do(context.TODO()) 401 status := 0 402 result.StatusCode(&status) 403 if status == 200 { 404 return true, nil 405 } 406 return false, nil 407 }) 408 if err != nil { 409 return result, fmt.Errorf("failed to wait for /healthz to return ok: %v", err) 410 } 411 } 412 413 // wait until default namespace is created 414 err = wait.Poll(100*time.Millisecond, 30*time.Second, func() (bool, error) { 415 select { 416 case err := <-errCh: 417 return false, err 418 default: 419 } 420 421 if _, err := client.CoreV1().Namespaces().Get(context.TODO(), "default", metav1.GetOptions{}); err != nil { 422 if !errors.IsNotFound(err) { 423 t.Logf("Unable to get default namespace: %v", err) 424 } 425 return false, nil 426 } 427 return true, nil 428 }) 429 if err != nil { 430 return result, fmt.Errorf("failed to wait for default namespace to be created: %v", err) 431 } 432 433 tlsInfo := transport.TLSInfo{ 434 CertFile: storageConfig.Transport.CertFile, 435 KeyFile: storageConfig.Transport.KeyFile, 436 TrustedCAFile: storageConfig.Transport.TrustedCAFile, 437 } 438 tlsConfig, err := tlsInfo.ClientConfig() 439 if err != nil { 440 return result, err 441 } 442 etcdConfig := clientv3.Config{ 443 Endpoints: storageConfig.Transport.ServerList, 444 DialTimeout: 20 * time.Second, 445 DialOptions: []grpc.DialOption{ 446 grpc.WithBlock(), // block until the underlying connection is up 447 }, 448 TLS: tlsConfig, 449 } 450 etcdClient, err := clientv3.New(etcdConfig) 451 if err != nil { 452 return result, err 453 } 454 455 // from here the caller must call tearDown 456 result.ClientConfig = restclient.CopyConfig(server.GenericAPIServer.LoopbackClientConfig) 457 result.ClientConfig.QPS = 1000 458 result.ClientConfig.Burst = 10000 459 result.ServerOpts = s 460 result.TearDownFn = func() { 461 tearDown() 462 etcdClient.Close() 463 } 464 result.EtcdClient = etcdClient 465 result.EtcdStoragePrefix = storageConfig.Prefix 466 467 return result, nil 468 } 469 470 // StartTestServerOrDie calls StartTestServer t.Fatal if it does not succeed. 471 func StartTestServerOrDie(t testing.TB, instanceOptions *TestServerInstanceOptions, flags []string, storageConfig *storagebackend.Config) *TestServer { 472 result, err := StartTestServer(t, instanceOptions, flags, storageConfig) 473 if err == nil { 474 return &result 475 } 476 477 t.Fatalf("failed to launch server: %v", err) 478 return nil 479 } 480 481 func createLocalhostListenerOnFreePort() (net.Listener, int, error) { 482 ln, err := net.Listen("tcp", "127.0.0.1:0") 483 if err != nil { 484 return nil, 0, err 485 } 486 487 // get port 488 tcpAddr, ok := ln.Addr().(*net.TCPAddr) 489 if !ok { 490 ln.Close() 491 return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String()) 492 } 493 494 return ln, tcpAddr.Port, nil 495 } 496 497 // pkgPath returns the absolute file path to this package's directory. With go 498 // test, we can just look at the runtime call stack. However, bazel compiles go 499 // binaries with the -trimpath option so the simple approach fails however we 500 // can consult environment variables to derive the path. 501 // 502 // The approach taken here works for both go test and bazel on the assumption 503 // that if and only if trimpath is passed, we are running under bazel. 504 func pkgPath(t Logger) (string, error) { 505 _, thisFile, _, ok := runtime.Caller(0) 506 if !ok { 507 return "", fmt.Errorf("failed to get current file") 508 } 509 510 pkgPath := filepath.Dir(thisFile) 511 512 // If we find bazel env variables, then -trimpath was passed so we need to 513 // construct the path from the environment. 514 if testSrcdir, testWorkspace := os.Getenv("TEST_SRCDIR"), os.Getenv("TEST_WORKSPACE"); testSrcdir != "" && testWorkspace != "" { 515 t.Logf("Detected bazel env varaiables: TEST_SRCDIR=%q TEST_WORKSPACE=%q", testSrcdir, testWorkspace) 516 pkgPath = filepath.Join(testSrcdir, testWorkspace, pkgPath) 517 } 518 519 // If the path is still not absolute, something other than bazel compiled 520 // with -trimpath. 521 if !filepath.IsAbs(pkgPath) { 522 return "", fmt.Errorf("can't construct an absolute path from %q", pkgPath) 523 } 524 525 t.Logf("Resolved testserver package path to: %q", pkgPath) 526 527 return pkgPath, nil 528 }