Skip to content

Commit c6570c2

Browse files
authored
Merge pull request #2316 from lleshchi/deletion_timestamp
⚠ Update fake client deletionTimestamp behavior
2 parents c8cf90e + 7a66d58 commit c6570c2

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)