@@ -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,15 @@ 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
+
452
+ return fmt .Errorf ("Error: Unable to edit %s: metadata.deletionTimestamp field is immutable" , accessor .GetName ())
453
+ }
441
454
obj , err = convertFromUnstructuredIfNecessary (t .scheme , obj )
442
455
if err != nil {
443
456
return err
@@ -664,6 +677,10 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
664
677
}
665
678
accessor .SetName (fmt .Sprintf ("%s%s" , base , utilrand .String (randomLength )))
666
679
}
680
+ // Ignore attempts to set deletion timestamp
681
+ if ! accessor .GetDeletionTimestamp ().IsZero () {
682
+ accessor .SetDeletionTimestamp (nil )
683
+ }
667
684
668
685
return c .tracker .Create (gvr , obj , accessor .GetNamespace ())
669
686
}
@@ -775,7 +792,7 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
775
792
if err != nil {
776
793
return err
777
794
}
778
- return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus )
795
+ return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus , false )
779
796
}
780
797
781
798
func (c * fakeClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
@@ -810,8 +827,36 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
810
827
return err
811
828
}
812
829
830
+ o , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
831
+ if err != nil {
832
+ return err
833
+ }
834
+ oldObj , err := meta .Accessor (o )
835
+ if err != nil {
836
+ return err
837
+ }
838
+
839
+ // Apply patch without updating object.
840
+ // To remain in accordance with the behavior of k8s api behavior,
841
+ // a patch must not allow for changes to the deletionTimestamp of an object.
842
+ // The reaction() function applies the patch to the object and calls Update(),
843
+ // whereas dryPatch() replicates this behavior but disregards the call to Update().
844
+ // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
845
+ // to updating the object.
846
+ action := testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data )
847
+ o , err = dryPatch (action , c .tracker )
848
+ if err != nil {
849
+ return err
850
+ }
851
+ newObj , err := meta .Accessor (o )
852
+
853
+ // Validate that deletionTimestamp has not been changed
854
+ if ! validTimestampDifference (newObj , oldObj ) {
855
+ return fmt .Errorf ("Rejected patch, metadata.deletionTimestamp immutable." )
856
+ }
857
+
813
858
reaction := testing .ObjectReaction (c .tracker )
814
- handled , o , err := reaction (testing . NewPatchAction ( gvr , accessor . GetNamespace (), accessor . GetName (), patch . Type (), data ) )
859
+ handled , o , err := reaction (action )
815
860
if err != nil {
816
861
return err
817
862
}
@@ -835,6 +880,84 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
835
880
return err
836
881
}
837
882
883
+ // Applying a patch results in a deletionTimestamp that is truncated to the nearest second.
884
+ // Check that the diff between a new and old deletion timestamp is within a reasonable threshold
885
+ // to be considered unchanged.
886
+ func validTimestampDifference (newObj metav1.Object , obj metav1.Object ) bool {
887
+
888
+ new_t := newObj .GetDeletionTimestamp ()
889
+ old_t := obj .GetDeletionTimestamp ()
890
+
891
+ if new_t == nil || old_t == nil {
892
+ return new_t == old_t
893
+ }
894
+ return newObj .GetDeletionTimestamp ().Sub (obj .GetDeletionTimestamp ().Time ).Abs () < time .Second
895
+ }
896
+
897
+ // The behavior of applying the patch is pulled out into dryPatch(),
898
+ // which applies the patch and returns an object, but does not Update() the object.
899
+ // This function returns a patched runtime object that may then be validated before a call to Update() is executed.
900
+ // This results in some code duplication, but was found to be a cleaner alternative than unmarshalling the data
901
+ // or updating the k8s client-go methods directly.
902
+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
903
+
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
+
957
+ return obj , nil
958
+
959
+ }
960
+
838
961
func copyNonStatusFrom (old , new runtime.Object ) error {
839
962
newClientObject , ok := new .(client.Object )
840
963
if ! ok {
@@ -942,7 +1065,9 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
942
1065
if len (oldAccessor .GetFinalizers ()) > 0 {
943
1066
now := metav1 .Now ()
944
1067
oldAccessor .SetDeletionTimestamp (& now )
945
- return c .tracker .Update (gvr , old , accessor .GetNamespace ())
1068
+ // Call update directly with mutability parameter set to true to allow
1069
+ // changes to deletionTimestamp
1070
+ return c .tracker .update (gvr , old , accessor .GetNamespace (), false , true )
946
1071
}
947
1072
}
948
1073
}
0 commit comments