@@ -27,7 +27,10 @@ import (
27
27
"strconv"
28
28
"strings"
29
29
"sync"
30
+ "time"
30
31
32
+ // Using v4 to match upstream
33
+ jsonpatch "github.com/evanphx/json-patch"
31
34
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
32
35
33
36
corev1 "k8s.io/api/core/v1"
@@ -41,8 +44,10 @@ import (
41
44
"k8s.io/apimachinery/pkg/labels"
42
45
"k8s.io/apimachinery/pkg/runtime"
43
46
"k8s.io/apimachinery/pkg/runtime/schema"
47
+ "k8s.io/apimachinery/pkg/types"
44
48
utilrand "k8s.io/apimachinery/pkg/util/rand"
45
49
"k8s.io/apimachinery/pkg/util/sets"
50
+ "k8s.io/apimachinery/pkg/util/strategicpatch"
46
51
"k8s.io/apimachinery/pkg/util/validation/field"
47
52
"k8s.io/apimachinery/pkg/watch"
48
53
"k8s.io/client-go/kubernetes/scheme"
@@ -285,6 +290,9 @@ func (t versionedTracker) Add(obj runtime.Object) error {
285
290
if err != nil {
286
291
return fmt .Errorf ("failed to get accessor for object: %w" , err )
287
292
}
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
+ }
288
296
if accessor .GetResourceVersion () == "" {
289
297
// We use a "magic" value of 999 here because this field
290
298
// 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
368
376
if bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).Patch" )) {
369
377
isStatus = true
370
378
}
371
- return t .update (gvr , obj , ns , isStatus )
379
+ return t .update (gvr , obj , ns , isStatus , false )
372
380
}
373
381
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 {
375
383
accessor , err := meta .Accessor (obj )
376
384
if err != nil {
377
385
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
438
446
}
439
447
intResourceVersion ++
440
448
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
+
441
454
if ! accessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
442
455
return t .ObjectTracker .Delete (gvr , accessor .GetNamespace (), accessor .GetName ())
443
456
}
@@ -667,6 +680,10 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
667
680
}
668
681
accessor .SetName (fmt .Sprintf ("%s%s" , base , utilrand .String (randomLength )))
669
682
}
683
+ // Ignore attempts to set deletion timestamp
684
+ if ! accessor .GetDeletionTimestamp ().IsZero () {
685
+ accessor .SetDeletionTimestamp (nil )
686
+ }
670
687
671
688
return c .tracker .Create (gvr , obj , accessor .GetNamespace ())
672
689
}
@@ -778,7 +795,7 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
778
795
if err != nil {
779
796
return err
780
797
}
781
- return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus )
798
+ return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus , false )
782
799
}
783
800
784
801
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
813
830
return err
814
831
}
815
832
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
+
816
864
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 )
818
866
if err != nil {
819
867
return err
820
868
}
@@ -838,6 +886,81 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
838
886
return err
839
887
}
840
888
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
+
841
964
func copyNonStatusFrom (old , new runtime.Object ) error {
842
965
newClientObject , ok := new .(client.Object )
843
966
if ! ok {
@@ -945,7 +1068,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
945
1068
if len (oldAccessor .GetFinalizers ()) > 0 {
946
1069
now := metav1 .Now ()
947
1070
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 )
949
1074
}
950
1075
}
951
1076
}
0 commit comments