sigs.k8s.io/controller-runtime@v0.18.2/pkg/client/fake/client.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 fake 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "reflect" 26 "runtime/debug" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 // Using v4 to match upstream 33 jsonpatch "github.com/evanphx/json-patch" 34 corev1 "k8s.io/api/core/v1" 35 policyv1 "k8s.io/api/policy/v1" 36 policyv1beta1 "k8s.io/api/policy/v1beta1" 37 apierrors "k8s.io/apimachinery/pkg/api/errors" 38 "k8s.io/apimachinery/pkg/api/meta" 39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 41 "k8s.io/apimachinery/pkg/fields" 42 "k8s.io/apimachinery/pkg/labels" 43 "k8s.io/apimachinery/pkg/runtime" 44 "k8s.io/apimachinery/pkg/runtime/schema" 45 "k8s.io/apimachinery/pkg/types" 46 utilrand "k8s.io/apimachinery/pkg/util/rand" 47 "k8s.io/apimachinery/pkg/util/sets" 48 "k8s.io/apimachinery/pkg/util/strategicpatch" 49 "k8s.io/apimachinery/pkg/util/validation/field" 50 "k8s.io/apimachinery/pkg/watch" 51 "k8s.io/client-go/kubernetes/scheme" 52 "k8s.io/client-go/testing" 53 54 "sigs.k8s.io/controller-runtime/pkg/client" 55 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 56 "sigs.k8s.io/controller-runtime/pkg/client/interceptor" 57 "sigs.k8s.io/controller-runtime/pkg/internal/field/selector" 58 "sigs.k8s.io/controller-runtime/pkg/internal/objectutil" 59 ) 60 61 type versionedTracker struct { 62 testing.ObjectTracker 63 scheme *runtime.Scheme 64 withStatusSubresource sets.Set[schema.GroupVersionKind] 65 } 66 67 type fakeClient struct { 68 tracker versionedTracker 69 scheme *runtime.Scheme 70 restMapper meta.RESTMapper 71 withStatusSubresource sets.Set[schema.GroupVersionKind] 72 73 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. 74 // The inner map maps from index name to IndexerFunc. 75 indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc 76 77 schemeWriteLock sync.Mutex 78 } 79 80 var _ client.WithWatch = &fakeClient{} 81 82 const ( 83 maxNameLength = 63 84 randomLength = 5 85 maxGeneratedNameLength = maxNameLength - randomLength 86 ) 87 88 // NewFakeClient creates a new fake client for testing. 89 // You can choose to initialize it with a slice of runtime.Object. 90 func NewFakeClient(initObjs ...runtime.Object) client.WithWatch { 91 return NewClientBuilder().WithRuntimeObjects(initObjs...).Build() 92 } 93 94 // NewClientBuilder returns a new builder to create a fake client. 95 func NewClientBuilder() *ClientBuilder { 96 return &ClientBuilder{} 97 } 98 99 // ClientBuilder builds a fake client. 100 type ClientBuilder struct { 101 scheme *runtime.Scheme 102 restMapper meta.RESTMapper 103 initObject []client.Object 104 initLists []client.ObjectList 105 initRuntimeObjects []runtime.Object 106 withStatusSubresource []client.Object 107 objectTracker testing.ObjectTracker 108 interceptorFuncs *interceptor.Funcs 109 110 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. 111 // The inner map maps from index name to IndexerFunc. 112 indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc 113 } 114 115 // WithScheme sets this builder's internal scheme. 116 // If not set, defaults to client-go's global scheme.Scheme. 117 func (f *ClientBuilder) WithScheme(scheme *runtime.Scheme) *ClientBuilder { 118 f.scheme = scheme 119 return f 120 } 121 122 // WithRESTMapper sets this builder's restMapper. 123 // The restMapper is directly set as mapper in the Client. This can be used for example 124 // with a meta.DefaultRESTMapper to provide a static rest mapping. 125 // If not set, defaults to an empty meta.DefaultRESTMapper. 126 func (f *ClientBuilder) WithRESTMapper(restMapper meta.RESTMapper) *ClientBuilder { 127 f.restMapper = restMapper 128 return f 129 } 130 131 // WithObjects can be optionally used to initialize this fake client with client.Object(s). 132 func (f *ClientBuilder) WithObjects(initObjs ...client.Object) *ClientBuilder { 133 f.initObject = append(f.initObject, initObjs...) 134 return f 135 } 136 137 // WithLists can be optionally used to initialize this fake client with client.ObjectList(s). 138 func (f *ClientBuilder) WithLists(initLists ...client.ObjectList) *ClientBuilder { 139 f.initLists = append(f.initLists, initLists...) 140 return f 141 } 142 143 // WithRuntimeObjects can be optionally used to initialize this fake client with runtime.Object(s). 144 func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *ClientBuilder { 145 f.initRuntimeObjects = append(f.initRuntimeObjects, initRuntimeObjs...) 146 return f 147 } 148 149 // WithObjectTracker can be optionally used to initialize this fake client with testing.ObjectTracker. 150 func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuilder { 151 f.objectTracker = ot 152 return f 153 } 154 155 // WithIndex can be optionally used to register an index with name `field` and indexer `extractValue` 156 // for API objects of the same GroupVersionKind (GVK) as `obj` in the fake client. 157 // It can be invoked multiple times, both with objects of the same GVK or different ones. 158 // Invoking WithIndex twice with the same `field` and GVK (via `obj`) arguments will panic. 159 // WithIndex retrieves the GVK of `obj` using the scheme registered via WithScheme if 160 // WithScheme was previously invoked, the default scheme otherwise. 161 func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue client.IndexerFunc) *ClientBuilder { 162 objScheme := f.scheme 163 if objScheme == nil { 164 objScheme = scheme.Scheme 165 } 166 167 gvk, err := apiutil.GVKForObject(obj, objScheme) 168 if err != nil { 169 panic(err) 170 } 171 172 // If this is the first index being registered, we initialize the map storing all the indexes. 173 if f.indexes == nil { 174 f.indexes = make(map[schema.GroupVersionKind]map[string]client.IndexerFunc) 175 } 176 177 // If this is the first index being registered for the GroupVersionKind of `obj`, we initialize 178 // the map storing the indexes for that GroupVersionKind. 179 if f.indexes[gvk] == nil { 180 f.indexes[gvk] = make(map[string]client.IndexerFunc) 181 } 182 183 if _, fieldAlreadyIndexed := f.indexes[gvk][field]; fieldAlreadyIndexed { 184 panic(fmt.Errorf("indexer conflict: field %s for GroupVersionKind %v is already indexed", 185 field, gvk)) 186 } 187 188 f.indexes[gvk][field] = extractValue 189 190 return f 191 } 192 193 // WithStatusSubresource configures the passed object with a status subresource, which means 194 // calls to Update and Patch will not alter its status. 195 func (f *ClientBuilder) WithStatusSubresource(o ...client.Object) *ClientBuilder { 196 f.withStatusSubresource = append(f.withStatusSubresource, o...) 197 return f 198 } 199 200 // WithInterceptorFuncs configures the client methods to be intercepted using the provided interceptor.Funcs. 201 func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs) *ClientBuilder { 202 f.interceptorFuncs = &interceptorFuncs 203 return f 204 } 205 206 // Build builds and returns a new fake client. 207 func (f *ClientBuilder) Build() client.WithWatch { 208 if f.scheme == nil { 209 f.scheme = scheme.Scheme 210 } 211 if f.restMapper == nil { 212 f.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{}) 213 } 214 215 var tracker versionedTracker 216 217 withStatusSubResource := sets.New(inTreeResourcesWithStatus()...) 218 for _, o := range f.withStatusSubresource { 219 gvk, err := apiutil.GVKForObject(o, f.scheme) 220 if err != nil { 221 panic(fmt.Errorf("failed to get gvk for object %T: %w", withStatusSubResource, err)) 222 } 223 withStatusSubResource.Insert(gvk) 224 } 225 226 if f.objectTracker == nil { 227 tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme, withStatusSubresource: withStatusSubResource} 228 } else { 229 tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource} 230 } 231 232 for _, obj := range f.initObject { 233 if err := tracker.Add(obj); err != nil { 234 panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err)) 235 } 236 } 237 for _, obj := range f.initLists { 238 if err := tracker.Add(obj); err != nil { 239 panic(fmt.Errorf("failed to add list %v to fake client: %w", obj, err)) 240 } 241 } 242 for _, obj := range f.initRuntimeObjects { 243 if err := tracker.Add(obj); err != nil { 244 panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err)) 245 } 246 } 247 248 var result client.WithWatch = &fakeClient{ 249 tracker: tracker, 250 scheme: f.scheme, 251 restMapper: f.restMapper, 252 indexes: f.indexes, 253 withStatusSubresource: withStatusSubResource, 254 } 255 256 if f.interceptorFuncs != nil { 257 result = interceptor.NewClient(result, *f.interceptorFuncs) 258 } 259 260 return result 261 } 262 263 const trackerAddResourceVersion = "999" 264 265 func (t versionedTracker) Add(obj runtime.Object) error { 266 var objects []runtime.Object 267 if meta.IsListType(obj) { 268 var err error 269 objects, err = meta.ExtractList(obj) 270 if err != nil { 271 return err 272 } 273 } else { 274 objects = []runtime.Object{obj} 275 } 276 for _, obj := range objects { 277 accessor, err := meta.Accessor(obj) 278 if err != nil { 279 return fmt.Errorf("failed to get accessor for object: %w", err) 280 } 281 if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 { 282 return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName()) 283 } 284 if accessor.GetResourceVersion() == "" { 285 // We use a "magic" value of 999 here because this field 286 // is parsed as uint and and 0 is already used in Update. 287 // As we can't go lower, go very high instead so this can 288 // be recognized 289 accessor.SetResourceVersion(trackerAddResourceVersion) 290 } 291 292 obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) 293 if err != nil { 294 return err 295 } 296 if err := t.ObjectTracker.Add(obj); err != nil { 297 return err 298 } 299 } 300 301 return nil 302 } 303 304 func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { 305 accessor, err := meta.Accessor(obj) 306 if err != nil { 307 return fmt.Errorf("failed to get accessor for object: %w", err) 308 } 309 if accessor.GetName() == "" { 310 return apierrors.NewInvalid( 311 obj.GetObjectKind().GroupVersionKind().GroupKind(), 312 accessor.GetName(), 313 field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) 314 } 315 if accessor.GetResourceVersion() != "" { 316 return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") 317 } 318 accessor.SetResourceVersion("1") 319 obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) 320 if err != nil { 321 return err 322 } 323 if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil { 324 accessor.SetResourceVersion("") 325 return err 326 } 327 328 return nil 329 } 330 331 // convertFromUnstructuredIfNecessary will convert runtime.Unstructured for a GVK that is recognized 332 // by the schema into the whatever the schema produces with New() for said GVK. 333 // This is required because the tracker unconditionally saves on manipulations, but its List() implementation 334 // tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure 335 // we save as the very same type, otherwise subsequent List requests will fail. 336 func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { 337 u, isUnstructured := o.(runtime.Unstructured) 338 if !isUnstructured { 339 return o, nil 340 } 341 gvk := o.GetObjectKind().GroupVersionKind() 342 if !s.Recognizes(gvk) { 343 return o, nil 344 } 345 346 typed, err := s.New(gvk) 347 if err != nil { 348 return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", gvk, err) 349 } 350 351 unstructuredSerialized, err := json.Marshal(u) 352 if err != nil { 353 return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err) 354 } 355 if err := json.Unmarshal(unstructuredSerialized, typed); err != nil { 356 return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err) 357 } 358 359 return typed, nil 360 } 361 362 func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { 363 isStatus := false 364 // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change 365 // that reaction, we use the callstack to figure out if this originated from the status client. 366 if bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) { 367 isStatus = true 368 } 369 return t.update(gvr, obj, ns, isStatus, false) 370 } 371 372 func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool, deleting bool) error { 373 accessor, err := meta.Accessor(obj) 374 if err != nil { 375 return fmt.Errorf("failed to get accessor for object: %w", err) 376 } 377 378 if accessor.GetName() == "" { 379 return apierrors.NewInvalid( 380 obj.GetObjectKind().GroupVersionKind().GroupKind(), 381 accessor.GetName(), 382 field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) 383 } 384 385 gvk, err := apiutil.GVKForObject(obj, t.scheme) 386 if err != nil { 387 return err 388 } 389 390 oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName()) 391 if err != nil { 392 // If the resource is not found and the resource allows create on update, issue a 393 // create instead. 394 if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) { 395 return t.Create(gvr, obj, ns) 396 } 397 return err 398 } 399 400 if t.withStatusSubresource.Has(gvk) { 401 if isStatus { // copy everything but status and metadata.ResourceVersion from original object 402 if err := copyStatusFrom(obj, oldObject); err != nil { 403 return fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err) 404 } 405 passedRV := accessor.GetResourceVersion() 406 if err := copyFrom(oldObject, obj); err != nil { 407 return fmt.Errorf("failed to restore non-status fields: %w", err) 408 } 409 accessor.SetResourceVersion(passedRV) 410 } else { // copy status from original object 411 if err := copyStatusFrom(oldObject, obj); err != nil { 412 return fmt.Errorf("failed to copy the status for object with status subresource: %w", err) 413 } 414 } 415 } else if isStatus { 416 return apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName()) 417 } 418 419 oldAccessor, err := meta.Accessor(oldObject) 420 if err != nil { 421 return err 422 } 423 424 // If the new object does not have the resource version set and it allows unconditional update, 425 // default it to the resource version of the existing resource 426 if accessor.GetResourceVersion() == "" { 427 switch { 428 case allowsUnconditionalUpdate(gvk): 429 accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) 430 case bytes. 431 Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): 432 // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change 433 // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func. 434 accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) 435 } 436 } 437 438 if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() { 439 return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified")) 440 } 441 if oldAccessor.GetResourceVersion() == "" { 442 oldAccessor.SetResourceVersion("0") 443 } 444 intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64) 445 if err != nil { 446 return fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err) 447 } 448 intResourceVersion++ 449 accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10)) 450 451 if !deleting && !deletionTimestampEqual(accessor, oldAccessor) { 452 return fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName()) 453 } 454 455 if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 { 456 return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) 457 } 458 obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) 459 if err != nil { 460 return err 461 } 462 return t.ObjectTracker.Update(gvr, obj, ns) 463 } 464 465 func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 466 gvr, err := getGVRFromObject(obj, c.scheme) 467 if err != nil { 468 return err 469 } 470 o, err := c.tracker.Get(gvr, key.Namespace, key.Name) 471 if err != nil { 472 return err 473 } 474 475 if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { 476 gvk, err := apiutil.GVKForObject(obj, c.scheme) 477 if err != nil { 478 return err 479 } 480 ta, err := meta.TypeAccessor(o) 481 if err != nil { 482 return err 483 } 484 ta.SetKind(gvk.Kind) 485 ta.SetAPIVersion(gvk.GroupVersion().String()) 486 } 487 488 j, err := json.Marshal(o) 489 if err != nil { 490 return err 491 } 492 zero(obj) 493 return json.Unmarshal(j, obj) 494 } 495 496 func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { 497 gvk, err := apiutil.GVKForObject(list, c.scheme) 498 if err != nil { 499 return nil, err 500 } 501 502 gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") 503 504 listOpts := client.ListOptions{} 505 listOpts.ApplyOptions(opts) 506 507 gvr, _ := meta.UnsafeGuessKindToResource(gvk) 508 return c.tracker.Watch(gvr, listOpts.Namespace) 509 } 510 511 func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error { 512 gvk, err := apiutil.GVKForObject(obj, c.scheme) 513 if err != nil { 514 return err 515 } 516 517 originalKind := gvk.Kind 518 519 gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") 520 521 if _, isUnstructuredList := obj.(runtime.Unstructured); isUnstructuredList && !c.scheme.Recognizes(gvk) { 522 // We need to register the ListKind with UnstructuredList: 523 // https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51 524 c.schemeWriteLock.Lock() 525 c.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), &unstructured.UnstructuredList{}) 526 c.schemeWriteLock.Unlock() 527 } 528 529 listOpts := client.ListOptions{} 530 listOpts.ApplyOptions(opts) 531 532 gvr, _ := meta.UnsafeGuessKindToResource(gvk) 533 o, err := c.tracker.List(gvr, gvk, listOpts.Namespace) 534 if err != nil { 535 return err 536 } 537 538 if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { 539 ta, err := meta.TypeAccessor(o) 540 if err != nil { 541 return err 542 } 543 ta.SetKind(originalKind) 544 ta.SetAPIVersion(gvk.GroupVersion().String()) 545 } 546 547 j, err := json.Marshal(o) 548 if err != nil { 549 return err 550 } 551 zero(obj) 552 if err := json.Unmarshal(j, obj); err != nil { 553 return err 554 } 555 556 if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil { 557 return nil 558 } 559 560 // If we're here, either a label or field selector are specified (or both), so before we return 561 // the list we must filter it. If both selectors are set, they are ANDed. 562 objs, err := meta.ExtractList(obj) 563 if err != nil { 564 return err 565 } 566 567 filteredList, err := c.filterList(objs, gvk, listOpts.LabelSelector, listOpts.FieldSelector) 568 if err != nil { 569 return err 570 } 571 572 return meta.SetList(obj, filteredList) 573 } 574 575 func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKind, ls labels.Selector, fs fields.Selector) ([]runtime.Object, error) { 576 // Filter the objects with the label selector 577 filteredList := list 578 if ls != nil { 579 objsFilteredByLabel, err := objectutil.FilterWithLabels(list, ls) 580 if err != nil { 581 return nil, err 582 } 583 filteredList = objsFilteredByLabel 584 } 585 586 // Filter the result of the previous pass with the field selector 587 if fs != nil { 588 objsFilteredByField, err := c.filterWithFields(filteredList, gvk, fs) 589 if err != nil { 590 return nil, err 591 } 592 filteredList = objsFilteredByField 593 } 594 595 return filteredList, nil 596 } 597 598 func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) { 599 requiresExact := selector.RequiresExactMatch(fs) 600 if !requiresExact { 601 return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"", 602 fs) 603 } 604 605 // Field selection is mimicked via indexes, so there's no sane answer this function can give 606 // if there are no indexes registered for the GroupVersionKind of the objects in the list. 607 indexes := c.indexes[gvk] 608 for _, req := range fs.Requirements() { 609 if len(indexes) == 0 || indexes[req.Field] == nil { 610 return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+ 611 "index with name %s has been registered for GroupVersionKind %v", gvk, req.Field, req.Field, gvk) 612 } 613 } 614 615 filteredList := make([]runtime.Object, 0, len(list)) 616 for _, obj := range list { 617 matches := true 618 for _, req := range fs.Requirements() { 619 indexExtractor := indexes[req.Field] 620 if !c.objMatchesFieldSelector(obj, indexExtractor, req.Value) { 621 matches = false 622 break 623 } 624 } 625 if matches { 626 filteredList = append(filteredList, obj) 627 } 628 } 629 return filteredList, nil 630 } 631 632 func (c *fakeClient) objMatchesFieldSelector(o runtime.Object, extractIndex client.IndexerFunc, val string) bool { 633 obj, isClientObject := o.(client.Object) 634 if !isClientObject { 635 panic(fmt.Errorf("expected object %v to be of type client.Object, but it's not", o)) 636 } 637 638 for _, extractedVal := range extractIndex(obj) { 639 if extractedVal == val { 640 return true 641 } 642 } 643 644 return false 645 } 646 647 func (c *fakeClient) Scheme() *runtime.Scheme { 648 return c.scheme 649 } 650 651 func (c *fakeClient) RESTMapper() meta.RESTMapper { 652 return c.restMapper 653 } 654 655 // GroupVersionKindFor returns the GroupVersionKind for the given object. 656 func (c *fakeClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 657 return apiutil.GVKForObject(obj, c.scheme) 658 } 659 660 // IsObjectNamespaced returns true if the GroupVersionKind of the object is namespaced. 661 func (c *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { 662 return apiutil.IsObjectNamespaced(obj, c.scheme, c.restMapper) 663 } 664 665 func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 666 createOptions := &client.CreateOptions{} 667 createOptions.ApplyOptions(opts) 668 669 for _, dryRunOpt := range createOptions.DryRun { 670 if dryRunOpt == metav1.DryRunAll { 671 return nil 672 } 673 } 674 675 gvr, err := getGVRFromObject(obj, c.scheme) 676 if err != nil { 677 return err 678 } 679 accessor, err := meta.Accessor(obj) 680 if err != nil { 681 return err 682 } 683 684 if accessor.GetName() == "" && accessor.GetGenerateName() != "" { 685 base := accessor.GetGenerateName() 686 if len(base) > maxGeneratedNameLength { 687 base = base[:maxGeneratedNameLength] 688 } 689 accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength))) 690 } 691 // Ignore attempts to set deletion timestamp 692 if !accessor.GetDeletionTimestamp().IsZero() { 693 accessor.SetDeletionTimestamp(nil) 694 } 695 696 return c.tracker.Create(gvr, obj, accessor.GetNamespace()) 697 } 698 699 func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 700 gvr, err := getGVRFromObject(obj, c.scheme) 701 if err != nil { 702 return err 703 } 704 accessor, err := meta.Accessor(obj) 705 if err != nil { 706 return err 707 } 708 delOptions := client.DeleteOptions{} 709 delOptions.ApplyOptions(opts) 710 711 for _, dryRunOpt := range delOptions.DryRun { 712 if dryRunOpt == metav1.DryRunAll { 713 return nil 714 } 715 } 716 717 // Check the ResourceVersion if that Precondition was specified. 718 if delOptions.Preconditions != nil && delOptions.Preconditions.ResourceVersion != nil { 719 name := accessor.GetName() 720 dbObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), name) 721 if err != nil { 722 return err 723 } 724 oldAccessor, err := meta.Accessor(dbObj) 725 if err != nil { 726 return err 727 } 728 actualRV := oldAccessor.GetResourceVersion() 729 expectRV := *delOptions.Preconditions.ResourceVersion 730 if actualRV != expectRV { 731 msg := fmt.Sprintf( 732 "the ResourceVersion in the precondition (%s) does not match the ResourceVersion in record (%s). "+ 733 "The object might have been modified", 734 expectRV, actualRV) 735 return apierrors.NewConflict(gvr.GroupResource(), name, errors.New(msg)) 736 } 737 } 738 739 return c.deleteObject(gvr, accessor) 740 } 741 742 func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 743 gvk, err := apiutil.GVKForObject(obj, c.scheme) 744 if err != nil { 745 return err 746 } 747 748 dcOptions := client.DeleteAllOfOptions{} 749 dcOptions.ApplyOptions(opts) 750 751 for _, dryRunOpt := range dcOptions.DryRun { 752 if dryRunOpt == metav1.DryRunAll { 753 return nil 754 } 755 } 756 757 gvr, _ := meta.UnsafeGuessKindToResource(gvk) 758 o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace) 759 if err != nil { 760 return err 761 } 762 763 objs, err := meta.ExtractList(o) 764 if err != nil { 765 return err 766 } 767 filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.LabelSelector) 768 if err != nil { 769 return err 770 } 771 for _, o := range filteredObjs { 772 accessor, err := meta.Accessor(o) 773 if err != nil { 774 return err 775 } 776 err = c.deleteObject(gvr, accessor) 777 if err != nil { 778 return err 779 } 780 } 781 return nil 782 } 783 784 func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 785 return c.update(obj, false, opts...) 786 } 787 788 func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.UpdateOption) error { 789 updateOptions := &client.UpdateOptions{} 790 updateOptions.ApplyOptions(opts) 791 792 for _, dryRunOpt := range updateOptions.DryRun { 793 if dryRunOpt == metav1.DryRunAll { 794 return nil 795 } 796 } 797 798 gvr, err := getGVRFromObject(obj, c.scheme) 799 if err != nil { 800 return err 801 } 802 accessor, err := meta.Accessor(obj) 803 if err != nil { 804 return err 805 } 806 return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false) 807 } 808 809 func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 810 return c.patch(obj, patch, opts...) 811 } 812 813 func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 814 patchOptions := &client.PatchOptions{} 815 patchOptions.ApplyOptions(opts) 816 817 for _, dryRunOpt := range patchOptions.DryRun { 818 if dryRunOpt == metav1.DryRunAll { 819 return nil 820 } 821 } 822 823 gvr, err := getGVRFromObject(obj, c.scheme) 824 if err != nil { 825 return err 826 } 827 accessor, err := meta.Accessor(obj) 828 if err != nil { 829 return err 830 } 831 data, err := patch.Data(obj) 832 if err != nil { 833 return err 834 } 835 836 gvk, err := apiutil.GVKForObject(obj, c.scheme) 837 if err != nil { 838 return err 839 } 840 841 oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) 842 if err != nil { 843 return err 844 } 845 oldAccessor, err := meta.Accessor(oldObj) 846 if err != nil { 847 return err 848 } 849 850 // Apply patch without updating object. 851 // To remain in accordance with the behavior of k8s api behavior, 852 // a patch must not allow for changes to the deletionTimestamp of an object. 853 // The reaction() function applies the patch to the object and calls Update(), 854 // whereas dryPatch() replicates this behavior but skips the call to Update(). 855 // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior 856 // to updating the object. 857 action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data) 858 o, err := dryPatch(action, c.tracker) 859 if err != nil { 860 return err 861 } 862 newObj, err := meta.Accessor(o) 863 if err != nil { 864 return err 865 } 866 867 // Validate that deletionTimestamp has not been changed 868 if !deletionTimestampEqual(newObj, oldAccessor) { 869 return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable") 870 } 871 872 reaction := testing.ObjectReaction(c.tracker) 873 handled, o, err := reaction(action) 874 if err != nil { 875 return err 876 } 877 if !handled { 878 panic("tracker could not handle patch method") 879 } 880 881 if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { 882 ta, err := meta.TypeAccessor(o) 883 if err != nil { 884 return err 885 } 886 ta.SetKind(gvk.Kind) 887 ta.SetAPIVersion(gvk.GroupVersion().String()) 888 } 889 890 j, err := json.Marshal(o) 891 if err != nil { 892 return err 893 } 894 zero(obj) 895 return json.Unmarshal(j, obj) 896 } 897 898 // Applying a patch results in a deletionTimestamp that is truncated to the nearest second. 899 // Check that the diff between a new and old deletion timestamp is within a reasonable threshold 900 // to be considered unchanged. 901 func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool { 902 newTime := newObj.GetDeletionTimestamp() 903 oldTime := obj.GetDeletionTimestamp() 904 905 if newTime == nil || oldTime == nil { 906 return newTime == oldTime 907 } 908 return newTime.Time.Sub(oldTime.Time).Abs() < time.Second 909 } 910 911 // The behavior of applying the patch is pulled out into dryPatch(), 912 // which applies the patch and returns an object, but does not Update() the object. 913 // This function returns a patched runtime object that may then be validated before a call to Update() is executed. 914 // This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data 915 // and easier than refactoring the k8s client-go method upstream. 916 // Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194 917 func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) { 918 ns := action.GetNamespace() 919 gvr := action.GetResource() 920 921 obj, err := tracker.Get(gvr, ns, action.GetName()) 922 if err != nil { 923 return nil, err 924 } 925 926 old, err := json.Marshal(obj) 927 if err != nil { 928 return nil, err 929 } 930 931 // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields 932 // in obj that are removed by patch are cleared 933 value := reflect.ValueOf(obj) 934 value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) 935 936 switch action.GetPatchType() { 937 case types.JSONPatchType: 938 patch, err := jsonpatch.DecodePatch(action.GetPatch()) 939 if err != nil { 940 return nil, err 941 } 942 modified, err := patch.Apply(old) 943 if err != nil { 944 return nil, err 945 } 946 947 if err = json.Unmarshal(modified, obj); err != nil { 948 return nil, err 949 } 950 case types.MergePatchType: 951 modified, err := jsonpatch.MergePatch(old, action.GetPatch()) 952 if err != nil { 953 return nil, err 954 } 955 956 if err := json.Unmarshal(modified, obj); err != nil { 957 return nil, err 958 } 959 case types.StrategicMergePatchType: 960 mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj) 961 if err != nil { 962 return nil, err 963 } 964 if err = json.Unmarshal(mergedByte, obj); err != nil { 965 return nil, err 966 } 967 case types.ApplyPatchType: 968 return nil, errors.New("apply patches are not supported in the fake client. Follow https://github.com/kubernetes/kubernetes/issues/115598 for the current status") 969 default: 970 return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType()) 971 } 972 return obj, nil 973 } 974 975 // copyStatusFrom copies the status from old into new 976 func copyStatusFrom(old, new runtime.Object) error { 977 oldMapStringAny, err := toMapStringAny(old) 978 if err != nil { 979 return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err) 980 } 981 newMapStringAny, err := toMapStringAny(new) 982 if err != nil { 983 return fmt.Errorf("failed to convert new to *unststructured.Unstructured: %w", err) 984 } 985 986 newMapStringAny["status"] = oldMapStringAny["status"] 987 988 if err := fromMapStringAny(newMapStringAny, new); err != nil { 989 return fmt.Errorf("failed to convert back from map[string]any: %w", err) 990 } 991 992 return nil 993 } 994 995 // copyFrom copies from old into new 996 func copyFrom(old, new runtime.Object) error { 997 oldMapStringAny, err := toMapStringAny(old) 998 if err != nil { 999 return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err) 1000 } 1001 if err := fromMapStringAny(oldMapStringAny, new); err != nil { 1002 return fmt.Errorf("failed to convert back from map[string]any: %w", err) 1003 } 1004 1005 return nil 1006 } 1007 1008 func toMapStringAny(obj runtime.Object) (map[string]any, error) { 1009 if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured { 1010 return unstructured.Object, nil 1011 } 1012 1013 serialized, err := json.Marshal(obj) 1014 if err != nil { 1015 return nil, err 1016 } 1017 1018 u := map[string]any{} 1019 return u, json.Unmarshal(serialized, &u) 1020 } 1021 1022 func fromMapStringAny(u map[string]any, target runtime.Object) error { 1023 if targetUnstructured, isUnstructured := target.(*unstructured.Unstructured); isUnstructured { 1024 targetUnstructured.Object = u 1025 return nil 1026 } 1027 1028 serialized, err := json.Marshal(u) 1029 if err != nil { 1030 return fmt.Errorf("failed to serialize: %w", err) 1031 } 1032 1033 zero(target) 1034 if err := json.Unmarshal(serialized, &target); err != nil { 1035 return fmt.Errorf("failed to deserialize: %w", err) 1036 } 1037 1038 return nil 1039 } 1040 1041 func (c *fakeClient) Status() client.SubResourceWriter { 1042 return c.SubResource("status") 1043 } 1044 1045 func (c *fakeClient) SubResource(subResource string) client.SubResourceClient { 1046 return &fakeSubResourceClient{client: c, subResource: subResource} 1047 } 1048 1049 func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error { 1050 old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) 1051 if err == nil { 1052 oldAccessor, err := meta.Accessor(old) 1053 if err == nil { 1054 if len(oldAccessor.GetFinalizers()) > 0 { 1055 now := metav1.Now() 1056 oldAccessor.SetDeletionTimestamp(&now) 1057 // Call update directly with mutability parameter set to true to allow 1058 // changes to deletionTimestamp 1059 return c.tracker.update(gvr, old, accessor.GetNamespace(), false, true) 1060 } 1061 } 1062 } 1063 1064 //TODO: implement propagation 1065 return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) 1066 } 1067 1068 func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) { 1069 gvk, err := apiutil.GVKForObject(obj, scheme) 1070 if err != nil { 1071 return schema.GroupVersionResource{}, err 1072 } 1073 gvr, _ := meta.UnsafeGuessKindToResource(gvk) 1074 return gvr, nil 1075 } 1076 1077 type fakeSubResourceClient struct { 1078 client *fakeClient 1079 subResource string 1080 } 1081 1082 func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error { 1083 panic("fakeSubResourceClient does not support get") 1084 } 1085 1086 func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error { 1087 switch sw.subResource { 1088 case "eviction": 1089 _, isEviction := subResource.(*policyv1beta1.Eviction) 1090 if !isEviction { 1091 _, isEviction = subResource.(*policyv1.Eviction) 1092 } 1093 if !isEviction { 1094 return apierrors.NewBadRequest(fmt.Sprintf("got invalid type %t, expected Eviction", subResource)) 1095 } 1096 if _, isPod := obj.(*corev1.Pod); !isPod { 1097 return apierrors.NewNotFound(schema.GroupResource{}, "") 1098 } 1099 1100 return sw.client.Delete(ctx, obj) 1101 default: 1102 return fmt.Errorf("fakeSubResourceWriter does not support create for %s", sw.subResource) 1103 } 1104 } 1105 1106 func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { 1107 updateOptions := client.SubResourceUpdateOptions{} 1108 updateOptions.ApplyOptions(opts) 1109 1110 body := obj 1111 if updateOptions.SubResourceBody != nil { 1112 body = updateOptions.SubResourceBody 1113 } 1114 return sw.client.update(body, true, &updateOptions.UpdateOptions) 1115 } 1116 1117 func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { 1118 patchOptions := client.SubResourcePatchOptions{} 1119 patchOptions.ApplyOptions(opts) 1120 1121 body := obj 1122 if patchOptions.SubResourceBody != nil { 1123 body = patchOptions.SubResourceBody 1124 } 1125 1126 // this is necessary to identify that last call was made for status patch, through stack trace. 1127 if sw.subResource == "status" { 1128 return sw.statusPatch(body, patch, patchOptions) 1129 } 1130 1131 return sw.client.patch(body, patch, &patchOptions.PatchOptions) 1132 } 1133 1134 func (sw *fakeSubResourceClient) statusPatch(body client.Object, patch client.Patch, patchOptions client.SubResourcePatchOptions) error { 1135 return sw.client.patch(body, patch, &patchOptions.PatchOptions) 1136 } 1137 1138 func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool { 1139 switch gvk.Group { 1140 case "apps": 1141 switch gvk.Kind { 1142 case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet": 1143 return true 1144 } 1145 case "autoscaling": 1146 switch gvk.Kind { 1147 case "HorizontalPodAutoscaler": 1148 return true 1149 } 1150 case "batch": 1151 switch gvk.Kind { 1152 case "CronJob", "Job": 1153 return true 1154 } 1155 case "certificates": 1156 switch gvk.Kind { 1157 case "Certificates": 1158 return true 1159 } 1160 case "flowcontrol": 1161 switch gvk.Kind { 1162 case "FlowSchema", "PriorityLevelConfiguration": 1163 return true 1164 } 1165 case "networking": 1166 switch gvk.Kind { 1167 case "Ingress", "IngressClass", "NetworkPolicy": 1168 return true 1169 } 1170 case "policy": 1171 switch gvk.Kind { 1172 case "PodSecurityPolicy": 1173 return true 1174 } 1175 case "rbac.authorization.k8s.io": 1176 switch gvk.Kind { 1177 case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": 1178 return true 1179 } 1180 case "scheduling": 1181 switch gvk.Kind { 1182 case "PriorityClass": 1183 return true 1184 } 1185 case "settings": 1186 switch gvk.Kind { 1187 case "PodPreset": 1188 return true 1189 } 1190 case "storage": 1191 switch gvk.Kind { 1192 case "StorageClass": 1193 return true 1194 } 1195 case "": 1196 switch gvk.Kind { 1197 case "ConfigMap", "Endpoint", "Event", "LimitRange", "Namespace", "Node", 1198 "PersistentVolume", "PersistentVolumeClaim", "Pod", "PodTemplate", 1199 "ReplicationController", "ResourceQuota", "Secret", "Service", 1200 "ServiceAccount", "EndpointSlice": 1201 return true 1202 } 1203 } 1204 1205 return false 1206 } 1207 1208 func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool { 1209 switch gvk.Group { 1210 case "coordination": 1211 switch gvk.Kind { 1212 case "Lease": 1213 return true 1214 } 1215 case "node": 1216 switch gvk.Kind { 1217 case "RuntimeClass": 1218 return true 1219 } 1220 case "rbac": 1221 switch gvk.Kind { 1222 case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": 1223 return true 1224 } 1225 case "": 1226 switch gvk.Kind { 1227 case "Endpoint", "Event", "LimitRange", "Service": 1228 return true 1229 } 1230 } 1231 1232 return false 1233 } 1234 1235 func inTreeResourcesWithStatus() []schema.GroupVersionKind { 1236 return []schema.GroupVersionKind{ 1237 {Version: "v1", Kind: "Namespace"}, 1238 {Version: "v1", Kind: "Node"}, 1239 {Version: "v1", Kind: "PersistentVolumeClaim"}, 1240 {Version: "v1", Kind: "PersistentVolume"}, 1241 {Version: "v1", Kind: "Pod"}, 1242 {Version: "v1", Kind: "ReplicationController"}, 1243 {Version: "v1", Kind: "Service"}, 1244 1245 {Group: "apps", Version: "v1", Kind: "Deployment"}, 1246 {Group: "apps", Version: "v1", Kind: "DaemonSet"}, 1247 {Group: "apps", Version: "v1", Kind: "ReplicaSet"}, 1248 {Group: "apps", Version: "v1", Kind: "StatefulSet"}, 1249 1250 {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}, 1251 1252 {Group: "batch", Version: "v1", Kind: "CronJob"}, 1253 {Group: "batch", Version: "v1", Kind: "Job"}, 1254 1255 {Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequest"}, 1256 1257 {Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"}, 1258 {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}, 1259 1260 {Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"}, 1261 1262 {Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"}, 1263 1264 {Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}, 1265 1266 {Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"}, 1267 {Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "PriorityLevelConfiguration"}, 1268 {Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "FlowSchema"}, 1269 {Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "PriorityLevelConfiguration"}, 1270 } 1271 } 1272 1273 // zero zeros the value of a pointer. 1274 func zero(x interface{}) { 1275 if x == nil { 1276 return 1277 } 1278 res := reflect.ValueOf(x).Elem() 1279 res.Set(reflect.Zero(res.Type())) 1280 }