Skip to content

Commit 961fc2c

Browse files
authored
⚠️ Fakeclient: Fix a number of bugs when updating through apply (#3319)
* Move the versioned tracked into its own file * Versioned tracker: Implement object tracker * Fakeclient Apply Update: strip status and other issues
1 parent feefdb8 commit 961fc2c

File tree

3 files changed

+466
-235
lines changed

3 files changed

+466
-235
lines changed

pkg/client/fake/client.go

Lines changed: 10 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@ limitations under the License.
1717
package fake
1818

1919
import (
20-
"bytes"
2120
"context"
2221
"errors"
2322
"fmt"
2423
"reflect"
25-
"runtime/debug"
26-
"strconv"
2724
"strings"
2825
"sync"
2926
"time"
@@ -65,7 +62,6 @@ import (
6562
utilrand "k8s.io/apimachinery/pkg/util/rand"
6663
"k8s.io/apimachinery/pkg/util/sets"
6764
"k8s.io/apimachinery/pkg/util/strategicpatch"
68-
"k8s.io/apimachinery/pkg/util/validation/field"
6965
"k8s.io/apimachinery/pkg/watch"
7066
clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
7167
"k8s.io/client-go/kubernetes/scheme"
@@ -79,13 +75,6 @@ import (
7975
"sigs.k8s.io/controller-runtime/pkg/internal/objectutil"
8076
)
8177

82-
type versionedTracker struct {
83-
testing.ObjectTracker
84-
scheme *runtime.Scheme
85-
withStatusSubresource sets.Set[schema.GroupVersionKind]
86-
usesFieldManagedObjectTracker bool
87-
}
88-
8978
type fakeClient struct {
9079
// trackerWriteLock must be acquired before writing to
9180
// the tracker or performing reads that affect a following
@@ -313,7 +302,7 @@ func (f *ClientBuilder) Build() client.WithWatch {
313302
usesFieldManagedObjectTracker = true
314303
}
315304
tracker := versionedTracker{
316-
ObjectTracker: f.objectTracker,
305+
upstream: f.objectTracker,
317306
scheme: f.scheme,
318307
withStatusSubresource: withStatusSubResource,
319308
usesFieldManagedObjectTracker: usesFieldManagedObjectTracker,
@@ -354,83 +343,6 @@ func (f *ClientBuilder) Build() client.WithWatch {
354343

355344
const trackerAddResourceVersion = "999"
356345

357-
func (t versionedTracker) Add(obj runtime.Object) error {
358-
var objects []runtime.Object
359-
if meta.IsListType(obj) {
360-
var err error
361-
objects, err = meta.ExtractList(obj)
362-
if err != nil {
363-
return err
364-
}
365-
} else {
366-
objects = []runtime.Object{obj}
367-
}
368-
for _, obj := range objects {
369-
accessor, err := meta.Accessor(obj)
370-
if err != nil {
371-
return fmt.Errorf("failed to get accessor for object: %w", err)
372-
}
373-
if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 {
374-
return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName())
375-
}
376-
if accessor.GetResourceVersion() == "" {
377-
// We use a "magic" value of 999 here because this field
378-
// is parsed as uint and and 0 is already used in Update.
379-
// As we can't go lower, go very high instead so this can
380-
// be recognized
381-
accessor.SetResourceVersion(trackerAddResourceVersion)
382-
}
383-
384-
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
385-
if err != nil {
386-
return err
387-
}
388-
389-
// If the fieldManager can not decode fields, it will just silently clear them. This is pretty
390-
// much guaranteed not to be what someone that initializes a fake client with objects that
391-
// have them set wants, so validate them here.
392-
// Ref https://github.com/kubernetes/kubernetes/blob/a956ef4862993b825bcd524a19260192ff1da72d/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go#L105
393-
if t.usesFieldManagedObjectTracker {
394-
if err := managedfields.ValidateManagedFields(accessor.GetManagedFields()); err != nil {
395-
return fmt.Errorf("invalid managedFields on %T: %w", obj, err)
396-
}
397-
}
398-
if err := t.ObjectTracker.Add(obj); err != nil {
399-
return err
400-
}
401-
}
402-
403-
return nil
404-
}
405-
406-
func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error {
407-
accessor, err := meta.Accessor(obj)
408-
if err != nil {
409-
return fmt.Errorf("failed to get accessor for object: %w", err)
410-
}
411-
if accessor.GetName() == "" {
412-
gvk, _ := apiutil.GVKForObject(obj, t.scheme)
413-
return apierrors.NewInvalid(
414-
gvk.GroupKind(),
415-
accessor.GetName(),
416-
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
417-
}
418-
if accessor.GetResourceVersion() != "" {
419-
return apierrors.NewBadRequest("resourceVersion can not be set for Create requests")
420-
}
421-
accessor.SetResourceVersion("1")
422-
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
423-
if err != nil {
424-
return err
425-
}
426-
if err := t.ObjectTracker.Create(gvr, obj, ns, opts...); err != nil {
427-
accessor.SetResourceVersion("")
428-
return err
429-
}
430-
431-
return nil
432-
}
433-
434346
// convertFromUnstructuredIfNecessary will convert runtime.Unstructured for a GVK that is recognized
435347
// by the schema into the whatever the schema produces with New() for said GVK.
436348
// This is required because the tracker unconditionally saves on manipulations, but its List() implementation
@@ -465,151 +377,6 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
465377
return typed, nil
466378
}
467379

468-
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error {
469-
updateOpts, err := getSingleOrZeroOptions(opts)
470-
if err != nil {
471-
return err
472-
}
473-
474-
return t.update(gvr, obj, ns, false, false, updateOpts)
475-
}
476-
477-
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, opts metav1.UpdateOptions) error {
478-
gvk, err := apiutil.GVKForObject(obj, t.scheme)
479-
if err != nil {
480-
return err
481-
}
482-
obj, err = t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun)
483-
if err != nil {
484-
return err
485-
}
486-
if obj == nil {
487-
return nil
488-
}
489-
490-
if u, unstructured := obj.(*unstructured.Unstructured); unstructured {
491-
u.SetGroupVersionKind(gvk)
492-
}
493-
494-
return t.ObjectTracker.Update(gvr, obj, ns, opts)
495-
}
496-
497-
func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.PatchOptions) error {
498-
patchOptions, err := getSingleOrZeroOptions(opts)
499-
if err != nil {
500-
return err
501-
}
502-
503-
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
504-
// that reaction, we use the callstack to figure out if this originated from the status client.
505-
isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch"))
506-
507-
obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun)
508-
if err != nil {
509-
return err
510-
}
511-
if obj == nil {
512-
return nil
513-
}
514-
515-
return t.ObjectTracker.Patch(gvr, obj, ns, patchOptions)
516-
}
517-
518-
func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, dryRun []string) (runtime.Object, error) {
519-
accessor, err := meta.Accessor(obj)
520-
if err != nil {
521-
return nil, fmt.Errorf("failed to get accessor for object: %w", err)
522-
}
523-
524-
if accessor.GetName() == "" {
525-
gvk, _ := apiutil.GVKForObject(obj, t.scheme)
526-
return nil, apierrors.NewInvalid(
527-
gvk.GroupKind(),
528-
accessor.GetName(),
529-
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
530-
}
531-
532-
gvk, err := apiutil.GVKForObject(obj, t.scheme)
533-
if err != nil {
534-
return nil, err
535-
}
536-
537-
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
538-
if err != nil {
539-
// If the resource is not found and the resource allows create on update, issue a
540-
// create instead.
541-
if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) {
542-
return nil, t.Create(gvr, obj, ns)
543-
}
544-
return nil, err
545-
}
546-
547-
if t.withStatusSubresource.Has(gvk) {
548-
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
549-
if err := copyStatusFrom(obj, oldObject); err != nil {
550-
return nil, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
551-
}
552-
passedRV := accessor.GetResourceVersion()
553-
if err := copyFrom(oldObject, obj); err != nil {
554-
return nil, fmt.Errorf("failed to restore non-status fields: %w", err)
555-
}
556-
accessor.SetResourceVersion(passedRV)
557-
} else { // copy status from original object
558-
if err := copyStatusFrom(oldObject, obj); err != nil {
559-
return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
560-
}
561-
}
562-
} else if isStatus {
563-
return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
564-
}
565-
566-
oldAccessor, err := meta.Accessor(oldObject)
567-
if err != nil {
568-
return nil, err
569-
}
570-
571-
// If the new object does not have the resource version set and it allows unconditional update,
572-
// default it to the resource version of the existing resource
573-
if accessor.GetResourceVersion() == "" {
574-
switch {
575-
case allowsUnconditionalUpdate(gvk):
576-
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
577-
// This is needed because if the patch explicitly sets the RV to null, the client-go reaction we use
578-
// to apply it and whose output we process here will have it unset. It is not clear why the Kubernetes
579-
// apiserver accepts such a patch, but it does so we just copy that behavior.
580-
// Kubernetes apiserver behavior can be checked like this:
581-
// `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9`
582-
case bytes.
583-
Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")):
584-
// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
585-
// that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func.
586-
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
587-
}
588-
}
589-
590-
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
591-
return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
592-
}
593-
if oldAccessor.GetResourceVersion() == "" {
594-
oldAccessor.SetResourceVersion("0")
595-
}
596-
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
597-
if err != nil {
598-
return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
599-
}
600-
intResourceVersion++
601-
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
602-
603-
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
604-
return nil, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
605-
}
606-
607-
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
608-
return nil, t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
609-
}
610-
return convertFromUnstructuredIfNecessary(t.scheme, obj)
611-
}
612-
613380
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
614381
if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil {
615382
return err
@@ -1233,6 +1000,15 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
12331000
reaction := testing.ObjectReaction(c.tracker)
12341001
handled, o, err := reaction(action)
12351002
if err != nil {
1003+
// The reaction calls tracker.Get after tracker.Apply to return the object,
1004+
// but we may have deleted it in tracker.Apply if there was no finalizer
1005+
// left.
1006+
if apierrors.IsNotFound(err) &&
1007+
patch.Type() == types.ApplyPatchType &&
1008+
oldAccessor.GetDeletionTimestamp() != nil &&
1009+
len(obj.GetFinalizers()) == 0 {
1010+
return nil
1011+
}
12361012
return err
12371013
}
12381014
if !handled {

0 commit comments

Comments
 (0)