mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-11 00:56:14 +00:00
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com> generic Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com>
349 lines
11 KiB
Go
349 lines
11 KiB
Go
/*
|
|
Copyright 2017 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 testing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/spf13/pflag"
|
|
"go.etcd.io/etcd/client/pkg/v3/transport"
|
|
clientv3 "go.etcd.io/etcd/client/v3"
|
|
"google.golang.org/grpc"
|
|
"k8s.io/kubernetes/pkg/controlplane/apiserver/samples/generic/server"
|
|
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
|
"k8s.io/client-go/kubernetes"
|
|
restclient "k8s.io/client-go/rest"
|
|
cliflag "k8s.io/component-base/cli/flag"
|
|
logsapi "k8s.io/component-base/logs/api/v1"
|
|
"k8s.io/klog/v2"
|
|
controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options"
|
|
"k8s.io/kubernetes/test/utils/ktesting"
|
|
)
|
|
|
|
func init() {
|
|
// If instantiated more than once or together with other servers, the
|
|
// servers would try to modify the global logging state. This must get
|
|
// ignored during testing.
|
|
logsapi.ReapplyHandling = logsapi.ReapplyHandlingIgnoreUnchanged
|
|
}
|
|
|
|
// This key is for testing purposes only and is not considered secure.
|
|
const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY-----
|
|
MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49
|
|
AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0
|
|
/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg==
|
|
-----END EC PRIVATE KEY-----`
|
|
|
|
// TearDownFunc is to be called to tear down a test server.
|
|
type TearDownFunc func()
|
|
|
|
// TestServerInstanceOptions Instance options the TestServer
|
|
type TestServerInstanceOptions struct {
|
|
// SkipHealthzCheck returns without waiting for the server to become healthy.
|
|
// Useful for testing server configurations expected to prevent /healthz from completing.
|
|
SkipHealthzCheck bool
|
|
}
|
|
|
|
// TestServer represents a running test server with everything to access it and
|
|
// its backing store etcd.
|
|
type TestServer struct {
|
|
ClientConfig *restclient.Config // Rest client config
|
|
ServerOpts *controlplaneapiserver.Options // ServerOpts
|
|
TearDownFn TearDownFunc // TearDown function
|
|
TmpDir string // Temp Dir used, by the apiserver
|
|
EtcdClient *clientv3.Client // used by tests that need to check data migrated from APIs that are no longer served
|
|
EtcdStoragePrefix string // storage prefix in etcd
|
|
}
|
|
|
|
// NewDefaultTestServerOptions default options for TestServer instances
|
|
func NewDefaultTestServerOptions() *TestServerInstanceOptions {
|
|
return &TestServerInstanceOptions{}
|
|
}
|
|
|
|
// StartTestServer starts an etcd server and sample-generic-controlplane and
|
|
// returns a TestServer struct with a tear-down func and clients to access it
|
|
// and its backing store.
|
|
func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions, customFlags []string, storageConfig *storagebackend.Config) (result TestServer, err error) {
|
|
tCtx := ktesting.Init(t)
|
|
|
|
if instanceOptions == nil {
|
|
instanceOptions = NewDefaultTestServerOptions()
|
|
}
|
|
|
|
result.TmpDir, err = os.MkdirTemp("", "sample-generic-controlplane")
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to create temp dir: %w", err)
|
|
}
|
|
|
|
var errCh chan error
|
|
tearDown := func() {
|
|
// Cancel is stopping apiserver and cleaning up
|
|
// after itself, including shutting down its storage layer.
|
|
tCtx.Cancel("tearing down")
|
|
|
|
// If the apiserver was started, let's wait for it to
|
|
// shutdown clearly.
|
|
if errCh != nil {
|
|
err, ok := <-errCh
|
|
if ok && err != nil {
|
|
klog.Errorf("Failed to shutdown test server clearly: %v", err)
|
|
}
|
|
}
|
|
os.RemoveAll(result.TmpDir) //nolint:errcheck // best effort
|
|
}
|
|
defer func() {
|
|
if result.TearDownFn == nil {
|
|
tearDown()
|
|
}
|
|
}()
|
|
|
|
o := server.NewOptions()
|
|
var fss cliflag.NamedFlagSets
|
|
o.AddFlags(&fss)
|
|
|
|
fs := pflag.NewFlagSet("test", pflag.PanicOnError)
|
|
for _, f := range fss.FlagSets {
|
|
fs.AddFlagSet(f)
|
|
}
|
|
|
|
o.SecureServing.Listener, o.SecureServing.BindPort, err = createLocalhostListenerOnFreePort()
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to create listener: %w", err)
|
|
}
|
|
o.SecureServing.ServerCert.CertDirectory = result.TmpDir
|
|
o.SecureServing.ExternalAddress = o.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device
|
|
|
|
pkgPath, err := pkgPath(t)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
o.SecureServing.ServerCert.FixtureDirectory = filepath.Join(pkgPath, "testdata")
|
|
|
|
o.Etcd.StorageConfig = *storageConfig
|
|
utilruntime.Must(o.APIEnablement.RuntimeConfig.Set("api/all=true"))
|
|
|
|
if err := fs.Parse(customFlags); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
saSigningKeyFile, err := os.CreateTemp("/tmp", "insecure_test_key")
|
|
if err != nil {
|
|
t.Fatalf("create temp file failed: %v", err)
|
|
}
|
|
defer os.RemoveAll(saSigningKeyFile.Name()) //nolint:errcheck // best effort
|
|
if err = os.WriteFile(saSigningKeyFile.Name(), []byte(ecdsaPrivateKey), 0666); err != nil {
|
|
t.Fatalf("write file %s failed: %v", saSigningKeyFile.Name(), err)
|
|
}
|
|
o.ServiceAccountSigningKeyFile = saSigningKeyFile.Name()
|
|
o.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
|
|
o.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
|
|
|
|
completedOptions, err := o.Complete(nil, nil)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to set default ServerRunOptions: %w", err)
|
|
}
|
|
|
|
if errs := completedOptions.Validate(); len(errs) != 0 {
|
|
return result, fmt.Errorf("failed to validate ServerRunOptions: %w", utilerrors.NewAggregate(errs))
|
|
}
|
|
|
|
t.Logf("runtime-config=%v", completedOptions.APIEnablement.RuntimeConfig)
|
|
t.Logf("Starting sample-generic-controlplane on port %d...", o.SecureServing.BindPort)
|
|
|
|
config, err := server.NewConfig(completedOptions)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
completed, err := config.Complete()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
s, err := server.CreateServerChain(completed)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to create server chain: %w", err)
|
|
}
|
|
|
|
errCh = make(chan error)
|
|
go func() {
|
|
defer close(errCh)
|
|
prepared, err := s.PrepareRun()
|
|
if err != nil {
|
|
errCh <- err
|
|
} else if err := prepared.Run(tCtx); err != nil {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
|
|
client, err := kubernetes.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to create a client: %w", err)
|
|
}
|
|
|
|
if !instanceOptions.SkipHealthzCheck {
|
|
t.Logf("Waiting for /healthz to be ok...")
|
|
|
|
// wait until healthz endpoint returns ok
|
|
err = wait.PollUntilContextTimeout(tCtx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
|
select {
|
|
case err := <-errCh:
|
|
return false, err
|
|
default:
|
|
}
|
|
|
|
req := client.CoreV1().RESTClient().Get().AbsPath("/healthz")
|
|
result := req.Do(ctx)
|
|
status := 0
|
|
result.StatusCode(&status)
|
|
if status == 200 {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to wait for /healthz to return ok: %w", err)
|
|
}
|
|
}
|
|
|
|
// wait until default namespace is created
|
|
err = wait.PollUntilContextTimeout(tCtx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) {
|
|
select {
|
|
case err := <-errCh:
|
|
return false, err
|
|
default:
|
|
}
|
|
|
|
if _, err := client.CoreV1().Namespaces().Get(ctx, "default", metav1.GetOptions{}); err != nil {
|
|
if !errors.IsNotFound(err) {
|
|
t.Logf("Unable to get default namespace: %v", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
})
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to wait for default namespace to be created: %w", err)
|
|
}
|
|
|
|
tlsInfo := transport.TLSInfo{
|
|
CertFile: storageConfig.Transport.CertFile,
|
|
KeyFile: storageConfig.Transport.KeyFile,
|
|
TrustedCAFile: storageConfig.Transport.TrustedCAFile,
|
|
}
|
|
tlsConfig, err := tlsInfo.ClientConfig()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
etcdConfig := clientv3.Config{
|
|
Endpoints: storageConfig.Transport.ServerList,
|
|
DialTimeout: 20 * time.Second,
|
|
DialOptions: []grpc.DialOption{
|
|
grpc.WithBlock(), // block until the underlying connection is up
|
|
},
|
|
TLS: tlsConfig,
|
|
}
|
|
etcdClient, err := clientv3.New(etcdConfig)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
// from here the caller must call tearDown
|
|
result.ClientConfig = restclient.CopyConfig(s.GenericAPIServer.LoopbackClientConfig)
|
|
result.ClientConfig.QPS = 1000
|
|
result.ClientConfig.Burst = 10000
|
|
result.ServerOpts = o
|
|
result.TearDownFn = func() {
|
|
tearDown()
|
|
etcdClient.Close() //nolint:errcheck // best effort
|
|
}
|
|
result.EtcdClient = etcdClient
|
|
result.EtcdStoragePrefix = storageConfig.Prefix
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// StartTestServerOrDie calls StartTestServer t.Fatal if it does not succeed.
|
|
func StartTestServerOrDie(t ktesting.TB, instanceOptions *TestServerInstanceOptions, flags []string, storageConfig *storagebackend.Config) *TestServer {
|
|
result, err := StartTestServer(t, instanceOptions, flags, storageConfig)
|
|
if err == nil {
|
|
return &result
|
|
}
|
|
|
|
t.Fatalf("failed to launch server: %v", err)
|
|
return nil
|
|
}
|
|
|
|
func createLocalhostListenerOnFreePort() (net.Listener, int, error) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// get port
|
|
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
|
|
if !ok {
|
|
ln.Close() //nolint:errcheck // best effort
|
|
return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String())
|
|
}
|
|
|
|
return ln, tcpAddr.Port, nil
|
|
}
|
|
|
|
// pkgPath returns the absolute file path to this package's directory. With go
|
|
// test, we can just look at the runtime call stack. However, bazel compiles go
|
|
// binaries with the -trimpath option so the simple approach fails however we
|
|
// can consult environment variables to derive the path.
|
|
//
|
|
// The approach taken here works for both go test and bazel on the assumption
|
|
// that if and only if trimpath is passed, we are running under bazel.
|
|
func pkgPath(t ktesting.TB) (string, error) {
|
|
_, thisFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
return "", fmt.Errorf("failed to get current file")
|
|
}
|
|
|
|
pkgPath := filepath.Dir(thisFile)
|
|
|
|
// If we find bazel env variables, then -trimpath was passed so we need to
|
|
// construct the path from the environment.
|
|
if testSrcdir, testWorkspace := os.Getenv("TEST_SRCDIR"), os.Getenv("TEST_WORKSPACE"); testSrcdir != "" && testWorkspace != "" {
|
|
t.Logf("Detected bazel env varaiables: TEST_SRCDIR=%q TEST_WORKSPACE=%q", testSrcdir, testWorkspace)
|
|
pkgPath = filepath.Join(testSrcdir, testWorkspace, pkgPath)
|
|
}
|
|
|
|
// If the path is still not absolute, something other than bazel compiled
|
|
// with -trimpath.
|
|
if !filepath.IsAbs(pkgPath) {
|
|
return "", fmt.Errorf("can't construct an absolute path from %q", pkgPath)
|
|
}
|
|
|
|
t.Logf("Resolved testserver package path to: %q", pkgPath)
|
|
|
|
return pkgPath, nil
|
|
}
|