k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/framework/client/objects.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 client
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net"
    23  	"time"
    24  
    25  	apiv1 "k8s.io/api/core/v1"
    26  	apierrs "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
    33  	"k8s.io/apimachinery/pkg/util/mergepatch"
    34  	utilnet "k8s.io/apimachinery/pkg/util/net"
    35  	"k8s.io/apimachinery/pkg/util/wait"
    36  	"k8s.io/client-go/dynamic"
    37  	clientset "k8s.io/client-go/kubernetes"
    38  )
    39  
    40  const (
    41  	// Parameters for retrying with exponential backoff.
    42  	retryBackoffInitialDuration = 100 * time.Millisecond
    43  	retryBackoffFactor          = 3
    44  	retryBackoffJitter          = 0
    45  	retryBackoffSteps           = 6
    46  
    47  	// Parameters for namespace deletion operations.
    48  	DefaultNamespaceDeletionTimeout  = 10 * time.Minute
    49  	defaultNamespaceDeletionInterval = 5 * time.Second
    50  
    51  	// String const defined in https://go.googlesource.com/net/+/749bd193bc2bcebc5f1a048da8af0392cfb2fa5d/http2/transport.go#1041
    52  	// TODO(mborsz): Migrate to error object comparison when the error type is exported.
    53  	http2ClientConnectionLostErr = "http2: client connection lost"
    54  )
    55  
    56  // RetryWithExponentialBackOff a utility for retrying the given function with exponential backoff.
    57  func RetryWithExponentialBackOff(fn wait.ConditionFunc) error {
    58  	backoff := wait.Backoff{
    59  		Duration: retryBackoffInitialDuration,
    60  		Factor:   retryBackoffFactor,
    61  		Jitter:   retryBackoffJitter,
    62  		Steps:    retryBackoffSteps,
    63  	}
    64  	return wait.ExponentialBackoff(backoff, fn)
    65  }
    66  
    67  // IsRetryableAPIError verifies whether the error is retryable.
    68  func IsRetryableAPIError(err error) bool {
    69  	// These errors may indicate a transient error that we can retry in tests.
    70  	if apierrs.IsInternalError(err) || apierrs.IsTimeout(err) || apierrs.IsServerTimeout(err) ||
    71  		apierrs.IsTooManyRequests(err) || utilnet.IsProbableEOF(err) || utilnet.IsConnectionReset(err) ||
    72  		// Retryable resource-quotas conflict errors may be returned in some cases, e.g. https://github.com/kubernetes/kubernetes/issues/67761
    73  		isResourceQuotaConflictError(err) ||
    74  		// Our client is using OAuth2 where 401 (unauthorized) can mean that our token has expired and we need to retry with a new one.
    75  		apierrs.IsUnauthorized(err) {
    76  		return true
    77  	}
    78  	// If the error sends the Retry-After header, we respect it as an explicit confirmation we should retry.
    79  	if _, shouldRetry := apierrs.SuggestsClientDelay(err); shouldRetry {
    80  		return true
    81  	}
    82  	return false
    83  }
    84  
    85  func isResourceQuotaConflictError(err error) bool {
    86  	apiErr, ok := err.(apierrs.APIStatus)
    87  	if !ok {
    88  		return false
    89  	}
    90  	if apiErr.Status().Reason != metav1.StatusReasonConflict {
    91  		return false
    92  	}
    93  	return apiErr.Status().Details != nil && apiErr.Status().Details.Kind == "resourcequotas"
    94  }
    95  
    96  // IsRetryableNetError determines whether the error is a retryable net error.
    97  func IsRetryableNetError(err error) bool {
    98  	if netError, ok := err.(net.Error); ok {
    99  		return netError.Temporary() || netError.Timeout()
   100  	}
   101  
   102  	if err.Error() == http2ClientConnectionLostErr {
   103  		return true
   104  	}
   105  	return false
   106  }
   107  
   108  // APICallOptions describes how api call errors should be treated, i.e. which errors should be
   109  // allowed (ignored) and which should be retried.
   110  type APICallOptions struct {
   111  	shouldAllowError func(error) bool
   112  	shouldRetryError func(error) bool
   113  }
   114  
   115  // Allow creates an APICallOptions that allows (ignores) errors matching the given predicate.
   116  func Allow(allowErrorPredicate func(error) bool) *APICallOptions {
   117  	return &APICallOptions{shouldAllowError: allowErrorPredicate}
   118  }
   119  
   120  // Retry creates an APICallOptions that retries errors matching the given predicate.
   121  func Retry(retryErrorPredicate func(error) bool) *APICallOptions {
   122  	return &APICallOptions{shouldRetryError: retryErrorPredicate}
   123  }
   124  
   125  // RetryFunction opaques given function into retryable function.
   126  func RetryFunction(f func() error, options ...*APICallOptions) wait.ConditionFunc {
   127  	var shouldAllowErrorFuncs, shouldRetryErrorFuncs []func(error) bool
   128  	for _, option := range options {
   129  		if option.shouldAllowError != nil {
   130  			shouldAllowErrorFuncs = append(shouldAllowErrorFuncs, option.shouldAllowError)
   131  		}
   132  		if option.shouldRetryError != nil {
   133  			shouldRetryErrorFuncs = append(shouldRetryErrorFuncs, option.shouldRetryError)
   134  		}
   135  	}
   136  	return func() (bool, error) {
   137  		err := f()
   138  		if err == nil {
   139  			return true, nil
   140  		}
   141  		if IsRetryableAPIError(err) || IsRetryableNetError(err) {
   142  			return false, nil
   143  		}
   144  		for _, shouldAllowError := range shouldAllowErrorFuncs {
   145  			if shouldAllowError(err) {
   146  				return true, nil
   147  			}
   148  		}
   149  		for _, shouldRetryError := range shouldRetryErrorFuncs {
   150  			if shouldRetryError(err) {
   151  				return false, nil
   152  			}
   153  		}
   154  		return false, err
   155  	}
   156  }
   157  
   158  // ListPodsWithOptions lists the pods using the provided options.
   159  func ListPodsWithOptions(c clientset.Interface, namespace string, listOpts metav1.ListOptions) ([]apiv1.Pod, error) {
   160  	var pods []apiv1.Pod
   161  	listFunc := func() error {
   162  		podsList, err := c.CoreV1().Pods(namespace).List(context.TODO(), listOpts)
   163  		if err != nil {
   164  			return err
   165  		}
   166  		pods = podsList.Items
   167  		return nil
   168  	}
   169  	if err := RetryWithExponentialBackOff(RetryFunction(listFunc)); err != nil {
   170  		return pods, err
   171  	}
   172  	return pods, nil
   173  }
   174  
   175  // ListNodes returns list of cluster nodes.
   176  func ListNodes(c clientset.Interface) ([]apiv1.Node, error) {
   177  	return ListNodesWithOptions(c, metav1.ListOptions{})
   178  }
   179  
   180  // ListNodesWithOptions lists the cluster nodes using the provided options.
   181  func ListNodesWithOptions(c clientset.Interface, listOpts metav1.ListOptions) ([]apiv1.Node, error) {
   182  	var nodes []apiv1.Node
   183  	listFunc := func() error {
   184  		nodesList, err := c.CoreV1().Nodes().List(context.TODO(), listOpts)
   185  		if err != nil {
   186  			return err
   187  		}
   188  		nodes = nodesList.Items
   189  		return nil
   190  	}
   191  	if err := RetryWithExponentialBackOff(RetryFunction(listFunc)); err != nil {
   192  		return nodes, err
   193  	}
   194  	return nodes, nil
   195  }
   196  
   197  // CreateNamespace creates a single namespace with given name.
   198  func CreateNamespace(c clientset.Interface, namespace string) error {
   199  	createFunc := func() error {
   200  		_, err := c.CoreV1().Namespaces().Create(context.TODO(), &apiv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{})
   201  		return err
   202  	}
   203  	return RetryWithExponentialBackOff(RetryFunction(createFunc, Allow(apierrs.IsAlreadyExists)))
   204  }
   205  
   206  // DeleteNamespace deletes namespace with given name.
   207  func DeleteNamespace(c clientset.Interface, namespace string) error {
   208  	deleteFunc := func() error {
   209  		return c.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{})
   210  	}
   211  	return RetryWithExponentialBackOff(RetryFunction(deleteFunc, Allow(apierrs.IsNotFound)))
   212  }
   213  
   214  // ListNamespaces returns list of existing namespace names.
   215  func ListNamespaces(c clientset.Interface) ([]apiv1.Namespace, error) {
   216  	var namespaces []apiv1.Namespace
   217  	listFunc := func() error {
   218  		namespacesList, err := c.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
   219  		if err != nil {
   220  			return err
   221  		}
   222  		namespaces = namespacesList.Items
   223  		return nil
   224  	}
   225  	if err := RetryWithExponentialBackOff(RetryFunction(listFunc)); err != nil {
   226  		return namespaces, err
   227  	}
   228  	return namespaces, nil
   229  }
   230  
   231  // WaitForDeleteNamespace waits untils namespace is terminated.
   232  func WaitForDeleteNamespace(c clientset.Interface, namespace string, timeout time.Duration) error {
   233  	retryWaitFunc := func() (bool, error) {
   234  		_, err := c.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
   235  		if err != nil {
   236  			if apierrs.IsNotFound(err) {
   237  				return true, nil
   238  			}
   239  			if !IsRetryableAPIError(err) {
   240  				return false, err
   241  			}
   242  		}
   243  		return false, nil
   244  	}
   245  	return wait.PollImmediate(defaultNamespaceDeletionInterval, timeout, retryWaitFunc)
   246  }
   247  
   248  // ListEvents retrieves events for the object with the given name.
   249  func ListEvents(c clientset.Interface, namespace string, name string, options ...*APICallOptions) (obj *apiv1.EventList, err error) {
   250  	getFunc := func() error {
   251  		obj, err = c.CoreV1().Events(namespace).List(context.TODO(), metav1.ListOptions{
   252  			FieldSelector: "involvedObject.name=" + name,
   253  		})
   254  		return err
   255  	}
   256  	if err := RetryWithExponentialBackOff(RetryFunction(getFunc, options...)); err != nil {
   257  		return nil, err
   258  	}
   259  	return obj, nil
   260  }
   261  
   262  // DeleteStorageClass deletes storage class with given name.
   263  func DeleteStorageClass(c clientset.Interface, name string) error {
   264  	deleteFunc := func() error {
   265  		return c.StorageV1().StorageClasses().Delete(context.TODO(), name, metav1.DeleteOptions{})
   266  	}
   267  	return RetryWithExponentialBackOff(RetryFunction(deleteFunc, Allow(apierrs.IsNotFound)))
   268  }
   269  
   270  // CreateObject creates object based on given object description.
   271  func CreateObject(dynamicClient dynamic.Interface, namespace string, name string, obj *unstructured.Unstructured, options ...*APICallOptions) error {
   272  	gvk := obj.GroupVersionKind()
   273  	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   274  	obj.SetName(name)
   275  	createFunc := func() error {
   276  		_, err := dynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), obj, metav1.CreateOptions{})
   277  		return err
   278  	}
   279  	options = append(options, Allow(apierrs.IsAlreadyExists))
   280  	return RetryWithExponentialBackOff(RetryFunction(createFunc, options...))
   281  }
   282  
   283  // PatchObject updates (using patch) object with given name, group, version and kind based on given object description.
   284  func PatchObject(dynamicClient dynamic.Interface, namespace string, name string, obj *unstructured.Unstructured, options ...*APICallOptions) error {
   285  	gvk := obj.GroupVersionKind()
   286  	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   287  	obj.SetName(name)
   288  	updateFunc := func() error {
   289  		currentObj, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   290  		if err != nil {
   291  			return err
   292  		}
   293  		patch, err := createPatch(currentObj, obj)
   294  		if err != nil {
   295  			return fmt.Errorf("creating patch diff error: %v", err)
   296  		}
   297  		_, err = dynamicClient.Resource(gvr).Namespace(namespace).Patch(context.TODO(), obj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{})
   298  		return err
   299  	}
   300  	return RetryWithExponentialBackOff(RetryFunction(updateFunc, options...))
   301  }
   302  
   303  // DeleteObject deletes object with given name, group, version and kind.
   304  func DeleteObject(dynamicClient dynamic.Interface, gvk schema.GroupVersionKind, namespace string, name string, options ...*APICallOptions) error {
   305  	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   306  	deleteFunc := func() error {
   307  		// Delete operation removes object with all of the dependants.
   308  		policy := metav1.DeletePropagationBackground
   309  		deleteOption := metav1.DeleteOptions{PropagationPolicy: &policy}
   310  		return dynamicClient.Resource(gvr).Namespace(namespace).Delete(context.TODO(), name, deleteOption)
   311  	}
   312  	options = append(options, Allow(apierrs.IsNotFound))
   313  	return RetryWithExponentialBackOff(RetryFunction(deleteFunc, options...))
   314  }
   315  
   316  // GetObject retrieves object with given name, group, version and kind.
   317  func GetObject(dynamicClient dynamic.Interface, gvk schema.GroupVersionKind, namespace string, name string, options ...*APICallOptions) (*unstructured.Unstructured, error) {
   318  	var obj *unstructured.Unstructured
   319  	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   320  	getFunc := func() error {
   321  		var err error
   322  		// TODO(krzysied): Check in which cases IncludeUninitialized=true option is required -
   323  		// implement additional handling if needed.
   324  		obj, err = dynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   325  		return err
   326  	}
   327  	if err := RetryWithExponentialBackOff(RetryFunction(getFunc, options...)); err != nil {
   328  		return nil, err
   329  	}
   330  	return obj, nil
   331  }
   332  
   333  func createPatch(current, modified *unstructured.Unstructured) ([]byte, error) {
   334  	currentJSON, err := current.MarshalJSON()
   335  	if err != nil {
   336  		return []byte{}, err
   337  	}
   338  	modifiedJSON, err := modified.MarshalJSON()
   339  	if err != nil {
   340  		return []byte{}, err
   341  	}
   342  	preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
   343  		mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
   344  	// We are passing nil as original object to CreateThreeWayJSONMergePatch which has a drawback that
   345  	// if some field has been deleted between `original` and `modified` object
   346  	// (e.g. by removing field in object's yaml), we will never remove that field from 'current'.
   347  	// TODO(mborsz): Pass here the original object.
   348  	return jsonmergepatch.CreateThreeWayJSONMergePatch(nil /* original */, modifiedJSON, currentJSON, preconditions...)
   349  }