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 }