k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/controlplane/transformation/transformation_test.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 transformation
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	clientv3 "go.etcd.io/etcd/client/v3"
    32  
    33  	appsv1 "k8s.io/api/apps/v1"
    34  	batchv1 "k8s.io/api/batch/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	"k8s.io/apimachinery/pkg/api/errors"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/runtime/schema"
    40  	"k8s.io/apimachinery/pkg/util/wait"
    41  	apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
    42  	"k8s.io/apiserver/pkg/storage/storagebackend"
    43  	"k8s.io/apiserver/pkg/storage/value"
    44  	"k8s.io/client-go/dynamic"
    45  	"k8s.io/client-go/kubernetes"
    46  	"k8s.io/client-go/rest"
    47  	"k8s.io/component-base/metrics/legacyregistry"
    48  	"k8s.io/klog/v2"
    49  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    50  	"k8s.io/kubernetes/test/integration"
    51  	"k8s.io/kubernetes/test/integration/etcd"
    52  	"k8s.io/kubernetes/test/integration/framework"
    53  	"k8s.io/kubernetes/test/utils/ktesting"
    54  	"k8s.io/utils/pointer"
    55  	"sigs.k8s.io/yaml"
    56  )
    57  
    58  const (
    59  	secretKey                = "api_key"
    60  	secretVal                = "086a7ffc-0225-11e8-ba89-0ed5f89f718b" // Fake value for testing.
    61  	encryptionConfigFileName = "encryption.conf"
    62  	testNamespace            = "secret-encryption-test"
    63  	testSecret               = "test-secret"
    64  	testConfigmap            = "test-configmap"
    65  	metricsPrefix            = "apiserver_storage_"
    66  	configMapKey             = "foo"
    67  	configMapVal             = "bar"
    68  
    69  	// precomputed key and secret for use with AES CBC
    70  	// this looks exactly the same as the AES GCM secret but with a different value
    71  	oldAESCBCKey = "e0/+tts8FS254BZimFZWtUsOCOUDSkvzB72PyimMlkY="
    72  	oldSecret    = "azhzAAoMCgJ2MRIGU2VjcmV0En4KXwoLdGVzdC1zZWNyZXQSABoWc2VjcmV0LWVuY3J5cHRpb24tdGVzdCIAKiQ3MmRmZTVjNC0xNDU2LTQyMzktYjFlZC1hZGZmYTJmMWY3YmEyADgAQggI5Jy/7wUQAHoAEhMKB2FwaV9rZXkSCPCfpJfwn5C8GgZPcGFxdWUaACIA"
    73  	oldSecretVal = "\xf0\x9f\xa4\x97\xf0\x9f\x90\xbc"
    74  )
    75  
    76  type unSealSecret func(ctx context.Context, cipherText []byte, dataCtx value.Context, config apiserverv1.ProviderConfiguration) ([]byte, error)
    77  
    78  type transformTest struct {
    79  	ktesting.TContext
    80  	storageConfig     *storagebackend.Config
    81  	configDir         string
    82  	transformerConfig string
    83  	kubeAPIServer     kubeapiservertesting.TestServer
    84  	restClient        *kubernetes.Clientset
    85  	ns                *corev1.Namespace
    86  	secret            *corev1.Secret
    87  }
    88  
    89  func newTransformTest(tb testing.TB, transformerConfigYAML string, reload bool, configDir string, storageConfig *storagebackend.Config) (*transformTest, error) {
    90  	tCtx := ktesting.Init(tb)
    91  	if storageConfig == nil {
    92  		storageConfig = framework.SharedEtcd()
    93  	}
    94  	e := transformTest{
    95  		TContext:          tCtx,
    96  		transformerConfig: transformerConfigYAML,
    97  		storageConfig:     storageConfig,
    98  	}
    99  
   100  	var err error
   101  	// create config dir with provided config yaml
   102  	if transformerConfigYAML != "" && configDir == "" {
   103  		if e.configDir, err = e.createEncryptionConfig(); err != nil {
   104  			e.cleanUp()
   105  			return nil, fmt.Errorf("error while creating KubeAPIServer encryption config: %w", err)
   106  		}
   107  	} else {
   108  		// configDir already exists. api-server must be restarting with existing encryption config
   109  		e.configDir = configDir
   110  	}
   111  	configFile := filepath.Join(e.configDir, encryptionConfigFileName)
   112  	_, err = os.ReadFile(configFile)
   113  	if err != nil {
   114  		e.cleanUp()
   115  		return nil, fmt.Errorf("failed to read config file: %w", err)
   116  	}
   117  
   118  	if e.kubeAPIServer, err = kubeapiservertesting.StartTestServer(tb, nil, e.getEncryptionOptions(reload), e.storageConfig); err != nil {
   119  		e.cleanUp()
   120  		return nil, fmt.Errorf("failed to start KubeAPI server: %w", err)
   121  	}
   122  	klog.Infof("Started kube-apiserver %v", e.kubeAPIServer.ClientConfig.Host)
   123  
   124  	if e.restClient, err = kubernetes.NewForConfig(e.kubeAPIServer.ClientConfig); err != nil {
   125  		e.cleanUp()
   126  		return nil, fmt.Errorf("error while creating rest client: %w", err)
   127  	}
   128  
   129  	if e.ns, err = e.createNamespace(testNamespace); err != nil {
   130  		e.cleanUp()
   131  		return nil, err
   132  	}
   133  
   134  	if transformerConfigYAML != "" && reload {
   135  		// when reloading is enabled, this healthz endpoint is always present
   136  		mustBeHealthy(tCtx, "/kms-providers", "ok", e.kubeAPIServer.ClientConfig)
   137  		mustNotHaveLivez(tCtx, "/kms-providers", "404 page not found", e.kubeAPIServer.ClientConfig)
   138  
   139  		// excluding healthz endpoints even if they do not exist should work
   140  		mustBeHealthy(tCtx, "", `warn: some health checks cannot be excluded: no matches for "kms-provider-0","kms-provider-1","kms-provider-2","kms-provider-3"`,
   141  			e.kubeAPIServer.ClientConfig, "kms-provider-0", "kms-provider-1", "kms-provider-2", "kms-provider-3")
   142  	}
   143  
   144  	return &e, nil
   145  }
   146  
   147  func (e *transformTest) cleanUp() {
   148  	if e.configDir != "" {
   149  		os.RemoveAll(e.configDir)
   150  	}
   151  
   152  	if e.kubeAPIServer.ClientConfig != nil {
   153  		e.shutdownAPIServer()
   154  	}
   155  }
   156  
   157  func (e *transformTest) shutdownAPIServer() {
   158  	e.kubeAPIServer.TearDownFn()
   159  }
   160  
   161  func (e *transformTest) runResource(l kubeapiservertesting.Logger, unSealSecretFunc unSealSecret, expectedEnvelopePrefix,
   162  	group,
   163  	version,
   164  	resource,
   165  	name,
   166  	namespaceName string,
   167  ) {
   168  	response, err := e.readRawRecordFromETCD(e.getETCDPathForResource(e.storageConfig.Prefix, group, resource, name, namespaceName))
   169  	if err != nil {
   170  		l.Errorf("failed to read from etcd: %v", err)
   171  		return
   172  	}
   173  
   174  	if !bytes.HasPrefix(response.Kvs[0].Value, []byte(expectedEnvelopePrefix)) {
   175  		l.Errorf("expected data to be prefixed with %s, but got %s",
   176  			expectedEnvelopePrefix, response.Kvs[0].Value)
   177  		return
   178  	}
   179  
   180  	// etcd path of the key is used as the authenticated context - need to pass it to decrypt
   181  	ctx := context.Background()
   182  	dataCtx := value.DefaultContext(e.getETCDPathForResource(e.storageConfig.Prefix, group, resource, name, namespaceName))
   183  	// Envelope header precedes the cipherTextPayload
   184  	sealedData := response.Kvs[0].Value[len(expectedEnvelopePrefix):]
   185  	transformerConfig, err := e.getEncryptionConfig()
   186  	if err != nil {
   187  		l.Errorf("failed to parse transformer config: %v", err)
   188  	}
   189  	v, err := unSealSecretFunc(ctx, sealedData, dataCtx, *transformerConfig)
   190  	if err != nil {
   191  		l.Errorf("failed to unseal secret: %v", err)
   192  		return
   193  	}
   194  	if resource == "secrets" {
   195  		if !strings.Contains(string(v), secretVal) {
   196  			l.Errorf("expected %q after decryption, but got %q", secretVal, string(v))
   197  		}
   198  	} else if resource == "configmaps" {
   199  		if !strings.Contains(string(v), configMapVal) {
   200  			l.Errorf("expected %q after decryption, but got %q", configMapVal, string(v))
   201  		}
   202  	} else {
   203  		if !strings.Contains(string(v), name) {
   204  			l.Errorf("expected %q after decryption, but got %q", name, string(v))
   205  		}
   206  	}
   207  
   208  	// Data should be un-enveloped on direct reads from Kube API Server.
   209  	if resource == "secrets" {
   210  		s, err := e.restClient.CoreV1().Secrets(testNamespace).Get(context.TODO(), name, metav1.GetOptions{})
   211  		if err != nil {
   212  			l.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err)
   213  		}
   214  		if secretVal != string(s.Data[secretKey]) {
   215  			l.Errorf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
   216  		}
   217  	} else if resource == "configmaps" {
   218  		s, err := e.restClient.CoreV1().ConfigMaps(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
   219  		if err != nil {
   220  			l.Fatalf("failed to get ConfigMap from %s, err: %v", namespaceName, err)
   221  		}
   222  		if configMapVal != string(s.Data[configMapKey]) {
   223  			l.Errorf("expected %s from KubeAPI, but got %s", configMapVal, string(s.Data[configMapKey]))
   224  		}
   225  	} else if resource == "pods" {
   226  		p, err := e.restClient.CoreV1().Pods(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
   227  		if err != nil {
   228  			l.Fatalf("failed to get Pod from %s, err: %v", namespaceName, err)
   229  		}
   230  		if p.Name != name {
   231  			l.Errorf("expected %s from KubeAPI, but got %s", name, p.Name)
   232  		}
   233  	} else {
   234  		l.Logf("Get object with dynamic client")
   235  		fooResource := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
   236  		obj, err := dynamic.NewForConfigOrDie(e.kubeAPIServer.ClientConfig).Resource(fooResource).Namespace(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
   237  		if err != nil {
   238  			l.Fatalf("Failed to get test instance: %v, name: %s", err, name)
   239  		}
   240  		if obj.GetObjectKind().GroupVersionKind().Group == group && obj.GroupVersionKind().Version == version && obj.GetKind() == resource && obj.GetNamespace() == namespaceName && obj.GetName() != name {
   241  			l.Errorf("expected %s from KubeAPI, but got %s", name, obj.GetName())
   242  		}
   243  	}
   244  }
   245  
   246  func (e *transformTest) benchmark(b *testing.B) {
   247  	for i := 0; i < b.N; i++ {
   248  		_, err := e.createSecret(e.secret.Name+strconv.Itoa(i), e.ns.Name)
   249  		if err != nil {
   250  			b.Fatalf("failed to create a secret: %v", err)
   251  		}
   252  	}
   253  }
   254  
   255  func (e *transformTest) getETCDPathForResource(storagePrefix, group, resource, name, namespaceName string) string {
   256  	groupResource := resource
   257  	if group != "" {
   258  		groupResource = fmt.Sprintf("%s/%s", group, resource)
   259  	}
   260  	if namespaceName == "" {
   261  		return fmt.Sprintf("/%s/%s/%s", storagePrefix, groupResource, name)
   262  	}
   263  	return fmt.Sprintf("/%s/%s/%s/%s", storagePrefix, groupResource, namespaceName, name)
   264  }
   265  
   266  func (e *transformTest) getRawSecretFromETCD() ([]byte, error) {
   267  	secretETCDPath := e.getETCDPathForResource(e.storageConfig.Prefix, "", "secrets", e.secret.Name, e.secret.Namespace)
   268  	etcdResponse, err := e.readRawRecordFromETCD(secretETCDPath)
   269  	if err != nil {
   270  		return nil, fmt.Errorf("failed to read %s from etcd: %v", secretETCDPath, err)
   271  	}
   272  	return etcdResponse.Kvs[0].Value, nil
   273  }
   274  
   275  func (e *transformTest) getEncryptionOptions(reload bool) []string {
   276  	if e.transformerConfig != "" {
   277  		return []string{
   278  			"--encryption-provider-config", filepath.Join(e.configDir, encryptionConfigFileName),
   279  			fmt.Sprintf("--encryption-provider-config-automatic-reload=%v", reload),
   280  			"--disable-admission-plugins", "ServiceAccount"}
   281  	}
   282  
   283  	return nil
   284  }
   285  
   286  func (e *transformTest) createEncryptionConfig() (
   287  	filePathForEncryptionConfig string,
   288  	err error,
   289  ) {
   290  	tempDir, err := os.MkdirTemp("", "secrets-encryption-test")
   291  	if err != nil {
   292  		return "", fmt.Errorf("failed to create temp directory: %v", err)
   293  	}
   294  
   295  	if err = os.WriteFile(filepath.Join(tempDir, encryptionConfigFileName), []byte(e.transformerConfig), 0644); err != nil {
   296  		os.RemoveAll(tempDir)
   297  		return tempDir, fmt.Errorf("error while writing encryption config: %v", err)
   298  	}
   299  
   300  	return tempDir, nil
   301  }
   302  
   303  func (e *transformTest) getEncryptionConfig() (*apiserverv1.ProviderConfiguration, error) {
   304  	var config apiserverv1.EncryptionConfiguration
   305  	err := yaml.Unmarshal([]byte(e.transformerConfig), &config)
   306  	if err != nil {
   307  		return nil, fmt.Errorf("failed to extract transformer key: %v", err)
   308  	}
   309  
   310  	return &config.Resources[0].Providers[0], nil
   311  }
   312  
   313  func (e *transformTest) createNamespace(name string) (*corev1.Namespace, error) {
   314  	ns := &corev1.Namespace{
   315  		ObjectMeta: metav1.ObjectMeta{
   316  			Name: name,
   317  		},
   318  	}
   319  
   320  	if _, err := e.restClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}); err != nil {
   321  		if errors.IsAlreadyExists(err) {
   322  			existingNs, err := e.restClient.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
   323  			if err != nil {
   324  				return nil, fmt.Errorf("unable to get testing namespace, err: [%v]", err)
   325  			}
   326  			return existingNs, nil
   327  		}
   328  		return nil, fmt.Errorf("unable to create testing namespace, err: [%v]", err)
   329  	}
   330  
   331  	return ns, nil
   332  }
   333  
   334  func (e *transformTest) createSecret(name, namespace string) (*corev1.Secret, error) {
   335  	secret := &corev1.Secret{
   336  		ObjectMeta: metav1.ObjectMeta{
   337  			Name:      name,
   338  			Namespace: namespace,
   339  		},
   340  		Data: map[string][]byte{
   341  			secretKey: []byte(secretVal),
   342  		},
   343  	}
   344  	if _, err := e.restClient.CoreV1().Secrets(secret.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
   345  		return nil, fmt.Errorf("error while writing secret: %v", err)
   346  	}
   347  
   348  	return secret, nil
   349  }
   350  
   351  func (e *transformTest) createConfigMap(name, namespace string) (*corev1.ConfigMap, error) {
   352  	cm := &corev1.ConfigMap{
   353  		ObjectMeta: metav1.ObjectMeta{
   354  			Name:      name,
   355  			Namespace: namespace,
   356  		},
   357  		Data: map[string]string{
   358  			configMapKey: configMapVal,
   359  		},
   360  	}
   361  	if _, err := e.restClient.CoreV1().ConfigMaps(cm.Namespace).Create(context.TODO(), cm, metav1.CreateOptions{}); err != nil {
   362  		return nil, fmt.Errorf("error while writing configmap: %v", err)
   363  	}
   364  
   365  	return cm, nil
   366  }
   367  
   368  // create jobs
   369  func (e *transformTest) createJob(name, namespace string) (*batchv1.Job, error) {
   370  	job := &batchv1.Job{
   371  		ObjectMeta: metav1.ObjectMeta{
   372  			Name:      name,
   373  			Namespace: namespace,
   374  		},
   375  		Spec: batchv1.JobSpec{
   376  			Template: corev1.PodTemplateSpec{
   377  				Spec: corev1.PodSpec{
   378  					Containers: []corev1.Container{
   379  						{
   380  							Name:  "test",
   381  							Image: "test",
   382  						},
   383  					},
   384  					RestartPolicy: corev1.RestartPolicyNever,
   385  				},
   386  			},
   387  		},
   388  	}
   389  	if _, err := e.restClient.BatchV1().Jobs(job.Namespace).Create(context.TODO(), job, metav1.CreateOptions{}); err != nil {
   390  		return nil, fmt.Errorf("error while creating job: %v", err)
   391  	}
   392  
   393  	return job, nil
   394  }
   395  
   396  // create deployment
   397  func (e *transformTest) createDeployment(name, namespace string) (*appsv1.Deployment, error) {
   398  	deployment := &appsv1.Deployment{
   399  		ObjectMeta: metav1.ObjectMeta{
   400  			Name:      name,
   401  			Namespace: namespace,
   402  		},
   403  		Spec: appsv1.DeploymentSpec{
   404  			Replicas: pointer.Int32(2),
   405  			Selector: &metav1.LabelSelector{
   406  				MatchLabels: map[string]string{
   407  					"app": "nginx",
   408  				},
   409  			},
   410  			Template: corev1.PodTemplateSpec{
   411  				ObjectMeta: metav1.ObjectMeta{
   412  					Labels: map[string]string{
   413  						"app": "nginx",
   414  					},
   415  				},
   416  				Spec: corev1.PodSpec{
   417  					Containers: []corev1.Container{
   418  						{
   419  							Name:  "nginx",
   420  							Image: "nginx:1.17",
   421  							Ports: []corev1.ContainerPort{
   422  								{
   423  									Name:          "http",
   424  									Protocol:      corev1.ProtocolTCP,
   425  									ContainerPort: 80,
   426  								},
   427  							},
   428  						},
   429  					},
   430  				},
   431  			},
   432  		},
   433  	}
   434  	if _, err := e.restClient.AppsV1().Deployments(deployment.Namespace).Create(context.TODO(), deployment, metav1.CreateOptions{}); err != nil {
   435  		return nil, fmt.Errorf("error while creating deployment: %v", err)
   436  	}
   437  
   438  	return deployment, nil
   439  }
   440  
   441  func gvr(group, version, resource string) schema.GroupVersionResource {
   442  	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
   443  }
   444  
   445  func createResource(client dynamic.Interface, gvr schema.GroupVersionResource, ns string) (*unstructured.Unstructured, error) {
   446  	stubObj, err := getStubObj(gvr)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
   451  }
   452  
   453  func inplaceUpdateResource(client dynamic.Interface, gvr schema.GroupVersionResource, ns string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
   454  	return client.Resource(gvr).Namespace(ns).Update(context.TODO(), obj, metav1.UpdateOptions{})
   455  }
   456  
   457  func getStubObj(gvr schema.GroupVersionResource) (*unstructured.Unstructured, error) {
   458  	stub := ""
   459  	if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
   460  		stub = data.Stub
   461  	}
   462  	if len(stub) == 0 {
   463  		return nil, fmt.Errorf("no stub data for %#v", gvr)
   464  	}
   465  
   466  	stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
   467  	if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
   468  		return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
   469  	}
   470  	return stubObj, nil
   471  }
   472  
   473  func (e *transformTest) createPod(namespace string, dynamicInterface dynamic.Interface) (*unstructured.Unstructured, error) {
   474  	podGVR := gvr("", "v1", "pods")
   475  	pod, err := createResource(dynamicInterface, podGVR, namespace)
   476  	if err != nil {
   477  		return nil, fmt.Errorf("error while writing pod: %v", err)
   478  	}
   479  	return pod, nil
   480  }
   481  
   482  func (e *transformTest) deletePod(namespace string, dynamicInterface dynamic.Interface) error {
   483  	podGVR := gvr("", "v1", "pods")
   484  	stubObj, err := getStubObj(podGVR)
   485  	if err != nil {
   486  		return err
   487  	}
   488  	return dynamicInterface.Resource(podGVR).Namespace(namespace).Delete(context.TODO(), stubObj.GetName(), metav1.DeleteOptions{})
   489  }
   490  
   491  func (e *transformTest) inplaceUpdatePod(namespace string, obj *unstructured.Unstructured, dynamicInterface dynamic.Interface) (*unstructured.Unstructured, error) {
   492  	podGVR := gvr("", "v1", "pods")
   493  	pod, err := inplaceUpdateResource(dynamicInterface, podGVR, namespace, obj)
   494  	if err != nil {
   495  		return nil, fmt.Errorf("error while writing pod: %v", err)
   496  	}
   497  	return pod, nil
   498  }
   499  
   500  func (e *transformTest) readRawRecordFromETCD(path string) (*clientv3.GetResponse, error) {
   501  	rawClient, etcdClient, err := integration.GetEtcdClients(e.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Transport)
   502  	if err != nil {
   503  		return nil, fmt.Errorf("failed to create etcd client: %v", err)
   504  	}
   505  	// kvClient is a wrapper around rawClient and to avoid leaking goroutines we need to
   506  	// close the client (which we can do by closing rawClient).
   507  	defer rawClient.Close()
   508  
   509  	response, err := etcdClient.Get(context.Background(), path, clientv3.WithPrefix())
   510  	if err != nil {
   511  		return nil, fmt.Errorf("failed to retrieve secret from etcd %v", err)
   512  	}
   513  
   514  	return response, nil
   515  }
   516  
   517  func (e *transformTest) writeRawRecordToETCD(path string, data []byte) (*clientv3.PutResponse, error) {
   518  	rawClient, etcdClient, err := integration.GetEtcdClients(e.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Transport)
   519  	if err != nil {
   520  		return nil, fmt.Errorf("failed to create etcd client: %v", err)
   521  	}
   522  	// kvClient is a wrapper around rawClient and to avoid leaking goroutines we need to
   523  	// close the client (which we can do by closing rawClient).
   524  	defer rawClient.Close()
   525  
   526  	response, err := etcdClient.Put(context.Background(), path, string(data))
   527  	if err != nil {
   528  		return nil, fmt.Errorf("failed to write secret to etcd %v", err)
   529  	}
   530  
   531  	return response, nil
   532  }
   533  
   534  func (e *transformTest) printMetrics() error {
   535  	e.Logf("Transformation Metrics:")
   536  	metrics, err := legacyregistry.DefaultGatherer.Gather()
   537  	if err != nil {
   538  		return fmt.Errorf("failed to gather metrics: %s", err)
   539  	}
   540  
   541  	for _, mf := range metrics {
   542  		if strings.HasPrefix(*mf.Name, metricsPrefix) {
   543  			e.Logf("%s", *mf.Name)
   544  			for _, metric := range mf.GetMetric() {
   545  				e.Logf("%v", metric)
   546  			}
   547  		}
   548  	}
   549  
   550  	return nil
   551  }
   552  
   553  func mustBeHealthy(t kubeapiservertesting.Logger, checkName, wantBodyContains string, clientConfig *rest.Config, excludes ...string) {
   554  	t.Helper()
   555  	var restErr error
   556  	pollErr := wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
   557  		body, ok, err := getHealthz(checkName, clientConfig, excludes...)
   558  		restErr = err
   559  		if err != nil {
   560  			return false, err
   561  		}
   562  		done := ok && strings.Contains(body, wantBodyContains)
   563  		if !done {
   564  			t.Logf("expected server check %q to be healthy with message %q but it is not: %s", checkName, wantBodyContains, body)
   565  		}
   566  		return done, nil
   567  	})
   568  
   569  	if pollErr != nil {
   570  		t.Fatalf("failed to get the expected healthz status of OK for check: %s, error: %v, debug inner error: %v", checkName, pollErr, restErr)
   571  	}
   572  }
   573  
   574  func mustBeUnHealthy(t kubeapiservertesting.Logger, checkName, wantBodyContains string, clientConfig *rest.Config, excludes ...string) {
   575  	t.Helper()
   576  	var restErr error
   577  	pollErr := wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
   578  		body, ok, err := getHealthz(checkName, clientConfig, excludes...)
   579  		restErr = err
   580  		if err != nil {
   581  			return false, err
   582  		}
   583  		done := !ok && strings.Contains(body, wantBodyContains)
   584  		if !done {
   585  			t.Logf("expected server check %q to be unhealthy with message %q but it is not: %s", checkName, wantBodyContains, body)
   586  		}
   587  		return done, nil
   588  	})
   589  
   590  	if pollErr != nil {
   591  		t.Fatalf("failed to get the expected healthz status of !OK for check: %s, error: %v, debug inner error: %v", checkName, pollErr, restErr)
   592  	}
   593  }
   594  
   595  func mustNotHaveLivez(t kubeapiservertesting.Logger, checkName, wantBodyContains string, clientConfig *rest.Config, excludes ...string) {
   596  	t.Helper()
   597  	var restErr error
   598  	pollErr := wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {
   599  		body, ok, err := getLivez(checkName, clientConfig, excludes...)
   600  		restErr = err
   601  		if err != nil {
   602  			return false, err
   603  		}
   604  		done := !ok && strings.Contains(body, wantBodyContains)
   605  		if !done {
   606  			t.Logf("expected server check %q with message %q but it is not: %s", checkName, wantBodyContains, body)
   607  		}
   608  		return done, nil
   609  	})
   610  
   611  	if pollErr != nil {
   612  		t.Fatalf("failed to get the expected livez status of !OK for check: %s, error: %v, debug inner error: %v", checkName, pollErr, restErr)
   613  	}
   614  }
   615  
   616  func getHealthz(checkName string, clientConfig *rest.Config, excludes ...string) (string, bool, error) {
   617  	client, err := kubernetes.NewForConfig(clientConfig)
   618  	if err != nil {
   619  		return "", false, fmt.Errorf("failed to create a client: %v", err)
   620  	}
   621  
   622  	req := client.CoreV1().RESTClient().Get().AbsPath(fmt.Sprintf("/healthz%v", checkName)).Param("verbose", "true")
   623  	for _, exclude := range excludes {
   624  		req.Param("exclude", exclude)
   625  	}
   626  	body, err := req.DoRaw(context.TODO()) // we can still have a response body during an error case
   627  	return string(body), err == nil, nil
   628  }
   629  
   630  func getLivez(checkName string, clientConfig *rest.Config, excludes ...string) (string, bool, error) {
   631  	client, err := kubernetes.NewForConfig(clientConfig)
   632  	if err != nil {
   633  		return "", false, fmt.Errorf("failed to create a client: %v", err)
   634  	}
   635  
   636  	req := client.CoreV1().RESTClient().Get().AbsPath(fmt.Sprintf("/livez%v", checkName)).Param("verbose", "true")
   637  	for _, exclude := range excludes {
   638  		req.Param("exclude", exclude)
   639  	}
   640  	body, err := req.DoRaw(context.TODO()) // we can still have a response body during an error case
   641  	return string(body), err == nil, nil
   642  }