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  }