@@ -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 , mutable 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,9 +442,14 @@ 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 ! oldAccessor .GetDeletionTimestamp ().IsZero () && len (accessor .GetFinalizers ()) == 0 {
439
447
return t .ObjectTracker .Delete (gvr , accessor .GetNamespace (), accessor .GetName ())
440
448
}
449
+
450
+ if oldAccessor .GetDeletionTimestamp () != accessor .GetDeletionTimestamp () && ! mutable {
451
+ return fmt .Errorf ("error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
452
+ }
441
453
obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
442
454
if err != nil {
443
455
return err
@@ -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 disregards 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 ! validTimestampDifference (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,80 @@ 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 validTimestampDifference (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 the data
902
+ // or updating the k8s client-go methods directly.
903
+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
904
+ ns := action .GetNamespace ()
905
+ gvr := action .GetResource ()
906
+
907
+ obj , err := tracker .Get (gvr , ns , action .GetName ())
908
+ if err != nil {
909
+ return nil , err
910
+ }
911
+
912
+ old , err := json .Marshal (obj )
913
+ if err != nil {
914
+ return nil , err
915
+ }
916
+
917
+ // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields
918
+ // in obj that are removed by patch are cleared
919
+ value := reflect .ValueOf (obj )
920
+ value .Elem ().Set (reflect .New (value .Type ().Elem ()).Elem ())
921
+
922
+ switch action .GetPatchType () {
923
+ case types .JSONPatchType :
924
+ patch , err := jsonpatch .DecodePatch (action .GetPatch ())
925
+ if err != nil {
926
+ return nil , err
927
+ }
928
+ modified , err := patch .Apply (old )
929
+ if err != nil {
930
+ return nil , err
931
+ }
932
+
933
+ if err = json .Unmarshal (modified , obj ); err != nil {
934
+ return nil , err
935
+ }
936
+ case types .MergePatchType :
937
+ modified , err := jsonpatch .MergePatch (old , action .GetPatch ())
938
+ if err != nil {
939
+ return nil , err
940
+ }
941
+
942
+ if err := json .Unmarshal (modified , obj ); err != nil {
943
+ return nil , err
944
+ }
945
+ case types .StrategicMergePatchType , types .ApplyPatchType :
946
+ mergedByte , err := strategicpatch .StrategicMergePatch (old , action .GetPatch (), obj )
947
+ if err != nil {
948
+ return nil , err
949
+ }
950
+ if err = json .Unmarshal (mergedByte , obj ); err != nil {
951
+ return nil , err
952
+ }
953
+ default :
954
+ return nil , fmt .Errorf ("PatchType is not supported" )
955
+ }
956
+ return obj , nil
957
+ }
958
+
838
959
func copyNonStatusFrom (old , new runtime.Object ) error {
839
960
newClientObject , ok := new .(client.Object )
840
961
if ! ok {
@@ -942,7 +1063,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
942
1063
if len (oldAccessor .GetFinalizers ()) > 0 {
943
1064
now := metav1 .Now ()
944
1065
oldAccessor .SetDeletionTimestamp (& now )
945
- return c .tracker .Update (gvr , old , accessor .GetNamespace ())
1066
+ // Call update directly with mutability parameter set to true to allow
1067
+ // changes to deletionTimestamp
1068
+ return c .tracker .update (gvr , old , accessor .GetNamespace (), false , true )
946
1069
}
947
1070
}
948
1071
}
0 commit comments