k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/etcd/server.go (about)

     1  /*
     2  Copyright 2018 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 etcd
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	utiltesting "k8s.io/client-go/util/testing"
    30  
    31  	clientv3 "go.etcd.io/etcd/client/v3"
    32  
    33  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    34  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    35  	"k8s.io/apimachinery/pkg/api/meta"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    38  	"k8s.io/apimachinery/pkg/runtime"
    39  	"k8s.io/apimachinery/pkg/runtime/schema"
    40  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    41  	"k8s.io/apimachinery/pkg/util/wait"
    42  	genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
    43  	cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
    44  	"k8s.io/client-go/dynamic"
    45  	clientset "k8s.io/client-go/kubernetes"
    46  	restclient "k8s.io/client-go/rest"
    47  	"k8s.io/client-go/restmapper"
    48  	"k8s.io/kubernetes/cmd/kube-apiserver/app"
    49  	"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
    50  	"k8s.io/kubernetes/test/integration"
    51  	"k8s.io/kubernetes/test/integration/framework"
    52  	"k8s.io/kubernetes/test/utils/ktesting"
    53  	netutils "k8s.io/utils/net"
    54  
    55  	// install all APIs
    56  	_ "k8s.io/kubernetes/pkg/controlplane"
    57  )
    58  
    59  // This key is for testing purposes only and is not considered secure.
    60  const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY-----
    61  MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49
    62  AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0
    63  /IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg==
    64  -----END EC PRIVATE KEY-----`
    65  
    66  // StartRealAPIServerOrDie starts an API server that is appropriate for use in tests that require one of every resource
    67  func StartRealAPIServerOrDie(t *testing.T, configFuncs ...func(*options.ServerRunOptions)) *APIServer {
    68  	tCtx := ktesting.Init(t)
    69  
    70  	certDir, err := os.MkdirTemp("", t.Name())
    71  	if err != nil {
    72  		t.Fatal(err)
    73  	}
    74  
    75  	_, defaultServiceClusterIPRange, err := netutils.ParseCIDRSloppy("10.0.0.0/24")
    76  	if err != nil {
    77  		t.Fatal(err)
    78  	}
    79  
    80  	listener, _, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0", net.ListenConfig{})
    81  	if err != nil {
    82  		t.Fatal(err)
    83  	}
    84  
    85  	saSigningKeyFile, err := os.CreateTemp("/tmp", "insecure_test_key")
    86  	if err != nil {
    87  		t.Fatalf("create temp file failed: %v", err)
    88  	}
    89  	defer utiltesting.CloseAndRemove(t, saSigningKeyFile)
    90  	if err = os.WriteFile(saSigningKeyFile.Name(), []byte(ecdsaPrivateKey), 0666); err != nil {
    91  		t.Fatalf("write file %s failed: %v", saSigningKeyFile.Name(), err)
    92  	}
    93  
    94  	opts := options.NewServerRunOptions()
    95  	opts.Options.SecureServing.Listener = listener
    96  	opts.Options.SecureServing.ServerCert.CertDirectory = certDir
    97  	opts.Options.ServiceAccountSigningKeyFile = saSigningKeyFile.Name()
    98  	opts.Options.Etcd.StorageConfig.Transport.ServerList = []string{framework.GetEtcdURL()}
    99  	opts.Options.Etcd.DefaultStorageMediaType = runtime.ContentTypeJSON // force json we can easily interpret the result in etcd
   100  	opts.ServiceClusterIPRanges = defaultServiceClusterIPRange.String()
   101  	opts.Options.Authentication.APIAudiences = []string{"https://foo.bar.example.com"}
   102  	opts.Options.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"}
   103  	opts.Options.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()}
   104  	opts.Options.Authorization.Modes = []string{"RBAC"}
   105  	opts.Options.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount"}
   106  	opts.Options.APIEnablement.RuntimeConfig["api/all"] = "true"
   107  	for _, f := range configFuncs {
   108  		f(opts)
   109  	}
   110  	completedOptions, err := opts.Complete()
   111  	if err != nil {
   112  		t.Fatal(err)
   113  	}
   114  
   115  	if errs := completedOptions.Validate(); len(errs) != 0 {
   116  		t.Fatalf("failed to validate ServerRunOptions: %v", utilerrors.NewAggregate(errs))
   117  	}
   118  
   119  	// get etcd client before starting API server
   120  	rawClient, kvClient, err := integration.GetEtcdClients(completedOptions.Etcd.StorageConfig.Transport)
   121  	if err != nil {
   122  		t.Fatal(err)
   123  	}
   124  
   125  	// make sure we start with a clean slate
   126  	if _, err := kvClient.Delete(context.Background(), "/registry/", clientv3.WithPrefix()); err != nil {
   127  		t.Fatal(err)
   128  	}
   129  
   130  	config, err := app.NewConfig(completedOptions)
   131  	if err != nil {
   132  		t.Fatal(err)
   133  	}
   134  	completed, err := config.Complete()
   135  	if err != nil {
   136  		t.Fatal(err)
   137  	}
   138  	kubeAPIServer, err := app.CreateServerChain(completed)
   139  	if err != nil {
   140  		t.Fatal(err)
   141  	}
   142  
   143  	kubeClientConfig := restclient.CopyConfig(kubeAPIServer.GenericAPIServer.LoopbackClientConfig)
   144  
   145  	// we make lots of requests, don't be slow
   146  	kubeClientConfig.QPS = 99999
   147  	kubeClientConfig.Burst = 9999
   148  
   149  	// we make requests to all resources, don't log warnings about deprecated ones
   150  	restclient.SetDefaultWarningHandler(restclient.NoWarnings{})
   151  
   152  	kubeClient := clientset.NewForConfigOrDie(kubeClientConfig)
   153  
   154  	errCh := make(chan error)
   155  	go func() {
   156  		// Catch panics that occur in this go routine so we get a comprehensible failure
   157  		defer func() {
   158  			if err := recover(); err != nil {
   159  				t.Errorf("Unexpected panic trying to start API server: %#v", err)
   160  			}
   161  		}()
   162  		defer close(errCh)
   163  
   164  		prepared, err := kubeAPIServer.PrepareRun()
   165  		if err != nil {
   166  			errCh <- err
   167  			return
   168  		}
   169  		if err := prepared.Run(tCtx); err != nil {
   170  			errCh <- err
   171  			t.Error(err)
   172  			return
   173  		}
   174  	}()
   175  
   176  	lastHealth := ""
   177  	attempt := 0
   178  	if err := wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) {
   179  		select {
   180  		case err := <-errCh:
   181  			return false, err
   182  		default:
   183  		}
   184  
   185  		// wait for the server to be healthy
   186  		result := kubeClient.RESTClient().Get().AbsPath("/healthz").Do(context.TODO())
   187  		content, _ := result.Raw()
   188  		lastHealth = string(content)
   189  		if errResult := result.Error(); errResult != nil {
   190  			attempt++
   191  			if attempt < 10 {
   192  				t.Log("waiting for server to be healthy")
   193  			} else {
   194  				t.Log(errResult)
   195  			}
   196  			return false, nil
   197  		}
   198  		var status int
   199  		result.StatusCode(&status)
   200  		return status == http.StatusOK, nil
   201  	}); err != nil {
   202  		t.Log(lastHealth)
   203  		t.Fatal(err)
   204  	}
   205  
   206  	// create CRDs so we can make sure that custom resources do not get lost
   207  	CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(kubeClientConfig), false, GetCustomResourceDefinitionData()...)
   208  
   209  	// force cached discovery reset
   210  	discoveryClient := cacheddiscovery.NewMemCacheClient(kubeClient.Discovery())
   211  	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
   212  	restMapper.Reset()
   213  
   214  	_, serverResources, err := kubeClient.Discovery().ServerGroupsAndResources()
   215  	if err != nil {
   216  		t.Fatal(err)
   217  	}
   218  
   219  	cleanup := func() {
   220  		// Cancel stopping apiserver and cleaning up
   221  		// after itself, including shutting down its storage layer.
   222  		tCtx.Cancel("cleaning up")
   223  
   224  		// If the apiserver was started, let's wait for it to
   225  		// shutdown clearly.
   226  		err, ok := <-errCh
   227  		if ok && err != nil {
   228  			t.Error(err)
   229  		}
   230  		rawClient.Close()
   231  		if err := os.RemoveAll(certDir); err != nil {
   232  			t.Log(err)
   233  		}
   234  	}
   235  
   236  	return &APIServer{
   237  		Client:    kubeClient,
   238  		Dynamic:   dynamic.NewForConfigOrDie(kubeClientConfig),
   239  		Config:    kubeClientConfig,
   240  		KV:        kvClient,
   241  		Mapper:    restMapper,
   242  		Resources: GetResources(t, serverResources),
   243  		Cleanup:   cleanup,
   244  	}
   245  }
   246  
   247  // APIServer represents a running API server that is ready for use
   248  // The Cleanup func must be deferred to prevent resource leaks
   249  type APIServer struct {
   250  	Client    clientset.Interface
   251  	Dynamic   dynamic.Interface
   252  	Config    *restclient.Config
   253  	KV        clientv3.KV
   254  	Mapper    meta.RESTMapper
   255  	Resources []Resource
   256  	Cleanup   func()
   257  }
   258  
   259  // Resource contains REST mapping information for a specific resource and extra metadata such as delete collection support
   260  type Resource struct {
   261  	Mapping             *meta.RESTMapping
   262  	HasDeleteCollection bool
   263  }
   264  
   265  // GetResources fetches the Resources associated with serverResources that support get and create
   266  func GetResources(t *testing.T, serverResources []*metav1.APIResourceList) []Resource {
   267  	var resources []Resource
   268  
   269  	for _, discoveryGroup := range serverResources {
   270  		for _, discoveryResource := range discoveryGroup.APIResources {
   271  			// this is a subresource, skip it
   272  			if strings.Contains(discoveryResource.Name, "/") {
   273  				continue
   274  			}
   275  			hasCreate := false
   276  			hasGet := false
   277  			hasDeleteCollection := false
   278  			for _, verb := range discoveryResource.Verbs {
   279  				if verb == "get" {
   280  					hasGet = true
   281  				}
   282  				if verb == "create" {
   283  					hasCreate = true
   284  				}
   285  				if verb == "deletecollection" {
   286  					hasDeleteCollection = true
   287  				}
   288  			}
   289  			if !(hasCreate && hasGet) {
   290  				continue
   291  			}
   292  
   293  			resourceGV, err := schema.ParseGroupVersion(discoveryGroup.GroupVersion)
   294  			if err != nil {
   295  				t.Fatal(err)
   296  			}
   297  			gvk := resourceGV.WithKind(discoveryResource.Kind)
   298  			if len(discoveryResource.Group) > 0 || len(discoveryResource.Version) > 0 {
   299  				gvk = schema.GroupVersionKind{
   300  					Group:   discoveryResource.Group,
   301  					Version: discoveryResource.Version,
   302  					Kind:    discoveryResource.Kind,
   303  				}
   304  			}
   305  			gvr := resourceGV.WithResource(discoveryResource.Name)
   306  
   307  			resources = append(resources, Resource{
   308  				Mapping: &meta.RESTMapping{
   309  					Resource:         gvr,
   310  					GroupVersionKind: gvk,
   311  					Scope:            scope(discoveryResource.Namespaced),
   312  				},
   313  				HasDeleteCollection: hasDeleteCollection,
   314  			})
   315  		}
   316  	}
   317  
   318  	return resources
   319  }
   320  
   321  func scope(namespaced bool) meta.RESTScope {
   322  	if namespaced {
   323  		return meta.RESTScopeNamespace
   324  	}
   325  	return meta.RESTScopeRoot
   326  }
   327  
   328  // JSONToUnstructured converts a JSON stub to unstructured.Unstructured and
   329  // returns a dynamic resource client that can be used to interact with it
   330  func JSONToUnstructured(stub, namespace string, mapping *meta.RESTMapping, dynamicClient dynamic.Interface) (dynamic.ResourceInterface, *unstructured.Unstructured, error) {
   331  	typeMetaAdder := map[string]interface{}{}
   332  	if err := json.Unmarshal([]byte(stub), &typeMetaAdder); err != nil {
   333  		return nil, nil, err
   334  	}
   335  
   336  	// we don't require GVK on the data we provide, so we fill it in here.  We could, but that seems extraneous.
   337  	typeMetaAdder["apiVersion"] = mapping.GroupVersionKind.GroupVersion().String()
   338  	typeMetaAdder["kind"] = mapping.GroupVersionKind.Kind
   339  
   340  	if mapping.Scope == meta.RESTScopeRoot {
   341  		namespace = ""
   342  	}
   343  
   344  	return dynamicClient.Resource(mapping.Resource).Namespace(namespace), &unstructured.Unstructured{Object: typeMetaAdder}, nil
   345  }
   346  
   347  // CreateTestCRDs creates the given CRDs, any failure causes the test to Fatal.
   348  // If skipCrdExistsInDiscovery is true, the CRDs are only checked for the Established condition via their Status.
   349  // If skipCrdExistsInDiscovery is false, the CRDs are checked via discovery, see CrdExistsInDiscovery.
   350  func CreateTestCRDs(t *testing.T, client apiextensionsclientset.Interface, skipCrdExistsInDiscovery bool, crds ...*apiextensionsv1.CustomResourceDefinition) {
   351  	for _, crd := range crds {
   352  		createTestCRD(t, client, skipCrdExistsInDiscovery, crd)
   353  	}
   354  }
   355  
   356  func createTestCRD(t *testing.T, client apiextensionsclientset.Interface, skipCrdExistsInDiscovery bool, crd *apiextensionsv1.CustomResourceDefinition) {
   357  	if _, err := client.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}); err != nil {
   358  		t.Fatalf("Failed to create %s CRD; %v", crd.Name, err)
   359  	}
   360  	if skipCrdExistsInDiscovery {
   361  		if err := waitForEstablishedCRD(client, crd.Name); err != nil {
   362  			t.Fatalf("Failed to establish %s CRD; %v", crd.Name, err)
   363  		}
   364  		return
   365  	}
   366  	if err := wait.PollImmediate(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   367  		return CrdExistsInDiscovery(client, crd), nil
   368  	}); err != nil {
   369  		t.Fatalf("Failed to see %s in discovery: %v", crd.Name, err)
   370  	}
   371  }
   372  
   373  func waitForEstablishedCRD(client apiextensionsclientset.Interface, name string) error {
   374  	return wait.PollImmediate(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   375  		crd, err := client.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   376  		if err != nil {
   377  			return false, err
   378  		}
   379  		for _, cond := range crd.Status.Conditions {
   380  			switch cond.Type {
   381  			case apiextensionsv1.Established:
   382  				if cond.Status == apiextensionsv1.ConditionTrue {
   383  					return true, nil
   384  				}
   385  			}
   386  		}
   387  		return false, nil
   388  	})
   389  }
   390  
   391  // CrdExistsInDiscovery checks to see if the given CRD exists in discovery at all served versions.
   392  func CrdExistsInDiscovery(client apiextensionsclientset.Interface, crd *apiextensionsv1.CustomResourceDefinition) bool {
   393  	var versions []string
   394  	for _, v := range crd.Spec.Versions {
   395  		if v.Served {
   396  			versions = append(versions, v.Name)
   397  		}
   398  	}
   399  	for _, v := range versions {
   400  		if !crdVersionExistsInDiscovery(client, crd, v) {
   401  			return false
   402  		}
   403  	}
   404  	return true
   405  }
   406  
   407  func crdVersionExistsInDiscovery(client apiextensionsclientset.Interface, crd *apiextensionsv1.CustomResourceDefinition, version string) bool {
   408  	resourceList, err := client.Discovery().ServerResourcesForGroupVersion(crd.Spec.Group + "/" + version)
   409  	if err != nil {
   410  		return false
   411  	}
   412  	for _, resource := range resourceList.APIResources {
   413  		if resource.Name == crd.Spec.Names.Plural {
   414  			return true
   415  		}
   416  	}
   417  	return false
   418  }