@@ -27,7 +27,9 @@ import (
27
27
"strconv"
28
28
"strings"
29
29
"sync"
30
+ "time"
30
31
32
+ jsonpatch "github.com/evanphx/json-patch"
31
33
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
32
34
33
35
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -38,8 +40,10 @@ import (
38
40
"k8s.io/apimachinery/pkg/labels"
39
41
"k8s.io/apimachinery/pkg/runtime"
40
42
"k8s.io/apimachinery/pkg/runtime/schema"
43
+ "k8s.io/apimachinery/pkg/types"
41
44
utilrand "k8s.io/apimachinery/pkg/util/rand"
42
45
"k8s.io/apimachinery/pkg/util/sets"
46
+ "k8s.io/apimachinery/pkg/util/strategicpatch"
43
47
"k8s.io/apimachinery/pkg/util/validation/field"
44
48
"k8s.io/apimachinery/pkg/watch"
45
49
"k8s.io/client-go/kubernetes/scheme"
@@ -282,6 +286,9 @@ func (t versionedTracker) Add(obj runtime.Object) error {
282
286
if err != nil {
283
287
return fmt .Errorf ("failed to get accessor for object: %w" , err )
284
288
}
289
+ if accessor .GetDeletionTimestamp () != nil && len (accessor .GetFinalizers ()) == 0 {
290
+ return fmt .Errorf ("refusing to init obj %s with metadata.deletionTimestamp but no finalizers" , accessor .GetName ())
291
+ }
285
292
if accessor .GetResourceVersion () == "" {
286
293
// We use a "magic" value of 999 here because this field
287
294
// is parsed as uint and and 0 is already used in Update.
@@ -365,10 +372,10 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
365
372
if bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).Patch" )) {
366
373
isStatus = true
367
374
}
368
- return t .update (gvr , obj , ns , isStatus )
375
+ return t .update (gvr , obj , ns , isStatus , false )
369
376
}
370
377
371
- func (t versionedTracker ) update (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus bool ) error {
378
+ func (t versionedTracker ) update (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus bool , deleting bool ) error {
372
379
accessor , err := meta .Accessor (obj )
373
380
if err != nil {
374
381
return fmt .Errorf ("failed to get accessor for object: %w" , err )
@@ -435,7 +442,12 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
435
442
}
436
443
intResourceVersion ++
437
444
accessor .SetResourceVersion (strconv .FormatUint (intResourceVersion , 10 ))
438
- if ! accessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
445
+
446
+ if ! deleting && ! validateTimestampDifference (accessor , oldAccessor ) {
447
+ return fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
448
+ }
449
+
450
+ if ! oldAccessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
439
451
return t .ObjectTracker .Delete (gvr , accessor .GetNamespace (), accessor .GetName ())
440
452
}
441
453
obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
@@ -664,6 +676,10 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
664
676
}
665
677
accessor .SetName (fmt .Sprintf ("%s%s" , base , utilrand .String (randomLength )))
666
678
}
679
+ // Ignore attempts to set deletion timestamp
680
+ if ! accessor .GetDeletionTimestamp ().IsZero () {
681
+ accessor .SetDeletionTimestamp (nil )
682
+ }
667
683
668
684
return c .tracker .Create (gvr , obj , accessor .GetNamespace ())
669
685
}
@@ -775,7 +791,7 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
775
791
if err != nil {
776
792
return err
777
793
}
778
- return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus )
794
+ return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus , false )
779
795
}
780
796
781
797
func (c * fakeClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
@@ -810,8 +826,39 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
810
826
return err
811
827
}
812
828
829
+ o , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
830
+ if err != nil {
831
+ return err
832
+ }
833
+ oldObj , err := meta .Accessor (o )
834
+ if err != nil {
835
+ return err
836
+ }
837
+
838
+ // Apply patch without updating object.
839
+ // To remain in accordance with the behavior of k8s api behavior,
840
+ // a patch must not allow for changes to the deletionTimestamp of an object.
841
+ // The reaction() function applies the patch to the object and calls Update(),
842
+ // whereas dryPatch() replicates this behavior but skips the call to Update().
843
+ // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
844
+ // to updating the object.
845
+ action := testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data )
846
+ o , err = dryPatch (action , c .tracker )
847
+ if err != nil {
848
+ return err
849
+ }
850
+ newObj , err := meta .Accessor (o )
851
+ if err != nil {
852
+ return err
853
+ }
854
+
855
+ // Validate that deletionTimestamp has not been changed
856
+ if ! validateTimestampDifference (newObj , oldObj ) {
857
+ return fmt .Errorf ("rejected patch, metadata.deletionTimestamp immutable" )
858
+ }
859
+
813
860
reaction := testing .ObjectReaction (c .tracker )
814
- handled , o , err := reaction (testing . NewPatchAction ( gvr , accessor . GetNamespace (), accessor . GetName (), patch . Type (), data ) )
861
+ handled , o , err := reaction (action )
815
862
if err != nil {
816
863
return err
817
864
}
@@ -835,6 +882,81 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
835
882
return err
836
883
}
837
884
885
+ // Applying a patch results in a deletionTimestamp that is truncated to the nearest second.
886
+ // Check that the diff between a new and old deletion timestamp is within a reasonable threshold
887
+ // to be considered unchanged.
888
+ func validateTimestampDifference (newObj metav1.Object , obj metav1.Object ) bool {
889
+ newTime := newObj .GetDeletionTimestamp ()
890
+ oldTime := obj .GetDeletionTimestamp ()
891
+
892
+ if newTime == nil || oldTime == nil {
893
+ return newTime == oldTime
894
+ }
895
+ return newTime .Time .Sub (oldTime .Time ).Abs () < time .Second
896
+ }
897
+
898
+ // The behavior of applying the patch is pulled out into dryPatch(),
899
+ // which applies the patch and returns an object, but does not Update() the object.
900
+ // This function returns a patched runtime object that may then be validated before a call to Update() is executed.
901
+ // This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
902
+ // and easier than refactoring the k8s client-go method upstream.
903
+ // Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
904
+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
905
+ ns := action .GetNamespace ()
906
+ gvr := action .GetResource ()
907
+
908
+ obj , err := tracker .Get (gvr , ns , action .GetName ())
909
+ if err != nil {
910
+ return nil , err
911
+ }
912
+
913
+ old , err := json .Marshal (obj )
914
+ if err != nil {
915
+ return nil , err
916
+ }
917
+
918
+ // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields
919
+ // in obj that are removed by patch are cleared
920
+ value := reflect .ValueOf (obj )
921
+ value .Elem ().Set (reflect .New (value .Type ().Elem ()).Elem ())
922
+
923
+ switch action .GetPatchType () {
924
+ case types .JSONPatchType :
925
+ patch , err := jsonpatch .DecodePatch (action .GetPatch ())
926
+ if err != nil {
927
+ return nil , err
928
+ }
929
+ modified , err := patch .Apply (old )
930
+ if err != nil {
931
+ return nil , err
932
+ }
933
+
934
+ if err = json .Unmarshal (modified , obj ); err != nil {
935
+ return nil , err
936
+ }
937
+ case types .MergePatchType :
938
+ modified , err := jsonpatch .MergePatch (old , action .GetPatch ())
939
+ if err != nil {
940
+ return nil , err
941
+ }
942
+
943
+ if err := json .Unmarshal (modified , obj ); err != nil {
944
+ return nil , err
945
+ }
946
+ case types .StrategicMergePatchType , types .ApplyPatchType :
947
+ mergedByte , err := strategicpatch .StrategicMergePatch (old , action .GetPatch (), obj )
948
+ if err != nil {
949
+ return nil , err
950
+ }
951
+ if err = json .Unmarshal (mergedByte , obj ); err != nil {
952
+ return nil , err
953
+ }
954
+ default :
955
+ return nil , fmt .Errorf ("PatchType is not supported" )
956
+ }
957
+ return obj , nil
958
+ }
959
+
838
960
func copyNonStatusFrom (old , new runtime.Object ) error {
839
961
newClientObject , ok := new .(client.Object )
840
962
if ! ok {
@@ -942,7 +1064,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
942
1064
if len (oldAccessor .GetFinalizers ()) > 0 {
943
1065
now := metav1 .Now ()
944
1066
oldAccessor .SetDeletionTimestamp (& now )
945
- return c .tracker .Update (gvr , old , accessor .GetNamespace ())
1067
+ // Call update directly with mutability parameter set to true to allow
1068
+ // changes to deletionTimestamp
1069
+ return c .tracker .update (gvr , old , accessor .GetNamespace (), false , true )
946
1070
}
947
1071
}
948
1072
}
0 commit comments