Skip to content

Commit 7a66d58

Browse files
committed
Update fake client deletionTimestamp behavior
The current fake client methods diverge from the behavior of real k8s. This results in scenarios where an object can be created or modified to have a deletionTimestamp and no finalizers, with a delay in garbage collection, causing inaccurate or ambiguous behavior in test cases that use the fake client. Bring fake client behavior closer in line with real k8s behavior by modifying the following: - Update() and Patch() will reject changes that set a previously-unset deletionTimestamp - Create() will ignore a deletionTimestamp on an object - Build() will error if initialized with an object that has a deletionTimestamp but no finalizers; but will accept a deletionTimestamp if it has finalizers. Signed-off-by: Leah Leshchinsky <[email protected]>
1 parent af986b8 commit 7a66d58

File tree

3 files changed

+318
-11
lines changed

3 files changed

+318
-11
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module sigs.k8s.io/controller-runtime
33
go 1.20
44

55
require (
6+
github.com/evanphx/json-patch v4.12.0+incompatible
67
github.com/evanphx/json-patch/v5 v5.6.0
78
github.com/fsnotify/fsnotify v1.6.0
89
github.com/go-logr/logr v1.2.4
@@ -31,7 +32,6 @@ require (
3132
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3233
github.com/davecgh/go-spew v1.1.1 // indirect
3334
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
34-
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
3535
github.com/go-openapi/jsonpointer v0.19.6 // indirect
3636
github.com/go-openapi/jsonreference v0.20.1 // indirect
3737
github.com/go-openapi/swag v0.22.3 // indirect

pkg/client/fake/client.go

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ import (
2727
"strconv"
2828
"strings"
2929
"sync"
30+
"time"
3031

32+
// Using v4 to match upstream
33+
jsonpatch "github.com/evanphx/json-patch"
3134
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
3235

3336
corev1 "k8s.io/api/core/v1"
@@ -41,8 +44,10 @@ import (
4144
"k8s.io/apimachinery/pkg/labels"
4245
"k8s.io/apimachinery/pkg/runtime"
4346
"k8s.io/apimachinery/pkg/runtime/schema"
47+
"k8s.io/apimachinery/pkg/types"
4448
utilrand "k8s.io/apimachinery/pkg/util/rand"
4549
"k8s.io/apimachinery/pkg/util/sets"
50+
"k8s.io/apimachinery/pkg/util/strategicpatch"
4651
"k8s.io/apimachinery/pkg/util/validation/field"
4752
"k8s.io/apimachinery/pkg/watch"
4853
"k8s.io/client-go/kubernetes/scheme"
@@ -285,6 +290,9 @@ func (t versionedTracker) Add(obj runtime.Object) error {
285290
if err != nil {
286291
return fmt.Errorf("failed to get accessor for object: %w", err)
287292
}
293+
if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 {
294+
return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName())
295+
}
288296
if accessor.GetResourceVersion() == "" {
289297
// We use a "magic" value of 999 here because this field
290298
// is parsed as uint and and 0 is already used in Update.
@@ -368,10 +376,10 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
368376
if bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).Patch")) {
369377
isStatus = true
370378
}
371-
return t.update(gvr, obj, ns, isStatus)
379+
return t.update(gvr, obj, ns, isStatus, false)
372380
}
373381

374-
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool) error {
382+
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool, deleting bool) error {
375383
accessor, err := meta.Accessor(obj)
376384
if err != nil {
377385
return fmt.Errorf("failed to get accessor for object: %w", err)
@@ -438,6 +446,11 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
438446
}
439447
intResourceVersion++
440448
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
449+
450+
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
451+
return fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
452+
}
453+
441454
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
442455
return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
443456
}
@@ -667,6 +680,10 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
667680
}
668681
accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength)))
669682
}
683+
// Ignore attempts to set deletion timestamp
684+
if !accessor.GetDeletionTimestamp().IsZero() {
685+
accessor.SetDeletionTimestamp(nil)
686+
}
670687

671688
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
672689
}
@@ -778,7 +795,7 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
778795
if err != nil {
779796
return err
780797
}
781-
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus)
798+
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false)
782799
}
783800

784801
func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
@@ -813,8 +830,39 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
813830
return err
814831
}
815832

833+
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
834+
if err != nil {
835+
return err
836+
}
837+
oldAccessor, err := meta.Accessor(oldObj)
838+
if err != nil {
839+
return err
840+
}
841+
842+
// Apply patch without updating object.
843+
// To remain in accordance with the behavior of k8s api behavior,
844+
// a patch must not allow for changes to the deletionTimestamp of an object.
845+
// The reaction() function applies the patch to the object and calls Update(),
846+
// whereas dryPatch() replicates this behavior but skips the call to Update().
847+
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
848+
// to updating the object.
849+
action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)
850+
o, err := dryPatch(action, c.tracker)
851+
if err != nil {
852+
return err
853+
}
854+
newObj, err := meta.Accessor(o)
855+
if err != nil {
856+
return err
857+
}
858+
859+
// Validate that deletionTimestamp has not been changed
860+
if !deletionTimestampEqual(newObj, oldAccessor) {
861+
return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
862+
}
863+
816864
reaction := testing.ObjectReaction(c.tracker)
817-
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data))
865+
handled, o, err := reaction(action)
818866
if err != nil {
819867
return err
820868
}
@@ -838,6 +886,81 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
838886
return err
839887
}
840888

889+
// Applying a patch results in a deletionTimestamp that is truncated to the nearest second.
890+
// Check that the diff between a new and old deletion timestamp is within a reasonable threshold
891+
// to be considered unchanged.
892+
func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
893+
newTime := newObj.GetDeletionTimestamp()
894+
oldTime := obj.GetDeletionTimestamp()
895+
896+
if newTime == nil || oldTime == nil {
897+
return newTime == oldTime
898+
}
899+
return newTime.Time.Sub(oldTime.Time).Abs() < time.Second
900+
}
901+
902+
// The behavior of applying the patch is pulled out into dryPatch(),
903+
// which applies the patch and returns an object, but does not Update() the object.
904+
// This function returns a patched runtime object that may then be validated before a call to Update() is executed.
905+
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
906+
// and easier than refactoring the k8s client-go method upstream.
907+
// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
908+
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) {
909+
ns := action.GetNamespace()
910+
gvr := action.GetResource()
911+
912+
obj, err := tracker.Get(gvr, ns, action.GetName())
913+
if err != nil {
914+
return nil, err
915+
}
916+
917+
old, err := json.Marshal(obj)
918+
if err != nil {
919+
return nil, err
920+
}
921+
922+
// reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields
923+
// in obj that are removed by patch are cleared
924+
value := reflect.ValueOf(obj)
925+
value.Elem().Set(reflect.New(value.Type().Elem()).Elem())
926+
927+
switch action.GetPatchType() {
928+
case types.JSONPatchType:
929+
patch, err := jsonpatch.DecodePatch(action.GetPatch())
930+
if err != nil {
931+
return nil, err
932+
}
933+
modified, err := patch.Apply(old)
934+
if err != nil {
935+
return nil, err
936+
}
937+
938+
if err = json.Unmarshal(modified, obj); err != nil {
939+
return nil, err
940+
}
941+
case types.MergePatchType:
942+
modified, err := jsonpatch.MergePatch(old, action.GetPatch())
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.StrategicMergePatchType, types.ApplyPatchType:
951+
mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj)
952+
if err != nil {
953+
return nil, err
954+
}
955+
if err = json.Unmarshal(mergedByte, obj); err != nil {
956+
return nil, err
957+
}
958+
default:
959+
return nil, fmt.Errorf("PatchType is not supported")
960+
}
961+
return obj, nil
962+
}
963+
841964
func copyNonStatusFrom(old, new runtime.Object) error {
842965
newClientObject, ok := new.(client.Object)
843966
if !ok {
@@ -945,7 +1068,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
9451068
if len(oldAccessor.GetFinalizers()) > 0 {
9461069
now := metav1.Now()
9471070
oldAccessor.SetDeletionTimestamp(&now)
948-
return c.tracker.Update(gvr, old, accessor.GetNamespace())
1071+
// Call update directly with mutability parameter set to true to allow
1072+
// changes to deletionTimestamp
1073+
return c.tracker.update(gvr, old, accessor.GetNamespace(), false, true)
9491074
}
9501075
}
9511076
}

0 commit comments

Comments
 (0)