@@ -17,11 +17,13 @@ limitations under the License.
17
17
package fake
18
18
19
19
import (
20
+ "bytes"
20
21
"context"
21
22
"encoding/json"
22
23
"errors"
23
24
"fmt"
24
25
"reflect"
26
+ "runtime/debug"
25
27
"strconv"
26
28
"strings"
27
29
"sync"
@@ -35,6 +37,7 @@ import (
35
37
"k8s.io/apimachinery/pkg/runtime"
36
38
"k8s.io/apimachinery/pkg/runtime/schema"
37
39
utilrand "k8s.io/apimachinery/pkg/util/rand"
40
+ "k8s.io/apimachinery/pkg/util/sets"
38
41
"k8s.io/apimachinery/pkg/util/validation/field"
39
42
"k8s.io/apimachinery/pkg/watch"
40
43
"k8s.io/client-go/kubernetes/scheme"
@@ -48,13 +51,15 @@ import (
48
51
49
52
type versionedTracker struct {
50
53
testing.ObjectTracker
51
- scheme * runtime.Scheme
54
+ scheme * runtime.Scheme
55
+ withStatusSubresource sets.Set [schema.GroupVersionKind ]
52
56
}
53
57
54
58
type fakeClient struct {
55
- tracker versionedTracker
56
- scheme * runtime.Scheme
57
- restMapper meta.RESTMapper
59
+ tracker versionedTracker
60
+ scheme * runtime.Scheme
61
+ restMapper meta.RESTMapper
62
+ withStatusSubresource sets.Set [schema.GroupVersionKind ]
58
63
59
64
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
60
65
// The inner map maps from index name to IndexerFunc.
@@ -95,12 +100,13 @@ func NewClientBuilder() *ClientBuilder {
95
100
96
101
// ClientBuilder builds a fake client.
97
102
type ClientBuilder struct {
98
- scheme * runtime.Scheme
99
- restMapper meta.RESTMapper
100
- initObject []client.Object
101
- initLists []client.ObjectList
102
- initRuntimeObjects []runtime.Object
103
- objectTracker testing.ObjectTracker
103
+ scheme * runtime.Scheme
104
+ restMapper meta.RESTMapper
105
+ initObject []client.Object
106
+ initLists []client.ObjectList
107
+ initRuntimeObjects []runtime.Object
108
+ withStatusSubresource []client.Object
109
+ objectTracker testing.ObjectTracker
104
110
105
111
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
106
112
// The inner map maps from index name to IndexerFunc.
@@ -185,6 +191,13 @@ func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue
185
191
return f
186
192
}
187
193
194
+ // WithStatusSubresource configures the passed object with a status subresource, which means
195
+ // calls to Update and Patch will not alters its status.
196
+ func (f * ClientBuilder ) WithStatusSubresource (o ... client.Object ) * ClientBuilder {
197
+ f .withStatusSubresource = append (f .withStatusSubresource , o ... )
198
+ return f
199
+ }
200
+
188
201
// Build builds and returns a new fake client.
189
202
func (f * ClientBuilder ) Build () client.WithWatch {
190
203
if f .scheme == nil {
@@ -196,10 +209,19 @@ func (f *ClientBuilder) Build() client.WithWatch {
196
209
197
210
var tracker versionedTracker
198
211
212
+ withStatusSubResource := sets .New (inTreeResourcesWithStatus ()... )
213
+ for _ , o := range f .withStatusSubresource {
214
+ gvk , err := apiutil .GVKForObject (o , f .scheme )
215
+ if err != nil {
216
+ panic (fmt .Errorf ("failed to get gvk for object %T: %w" , withStatusSubResource , err ))
217
+ }
218
+ withStatusSubResource .Insert (gvk )
219
+ }
220
+
199
221
if f .objectTracker == nil {
200
- tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme }
222
+ tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme , withStatusSubresource : withStatusSubResource }
201
223
} else {
202
- tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme }
224
+ tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme , withStatusSubresource : withStatusSubResource }
203
225
}
204
226
205
227
for _ , obj := range f .initObject {
@@ -217,11 +239,13 @@ func (f *ClientBuilder) Build() client.WithWatch {
217
239
panic (fmt .Errorf ("failed to add runtime object %v to fake client: %w" , obj , err ))
218
240
}
219
241
}
242
+
220
243
return & fakeClient {
221
- tracker : tracker ,
222
- scheme : f .scheme ,
223
- restMapper : f .restMapper ,
224
- indexes : f .indexes ,
244
+ tracker : tracker ,
245
+ scheme : f .scheme ,
246
+ restMapper : f .restMapper ,
247
+ indexes : f .indexes ,
248
+ withStatusSubresource : withStatusSubResource ,
225
249
}
226
250
}
227
251
@@ -318,6 +342,16 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
318
342
}
319
343
320
344
func (t versionedTracker ) Update (gvr schema.GroupVersionResource , obj runtime.Object , ns string ) error {
345
+ isStatus := false
346
+ // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
347
+ // that reaction, we use the callstack to figure out if this originated from the status client.
348
+ if bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).Patch" )) {
349
+ isStatus = true
350
+ }
351
+ return t .update (gvr , obj , ns , isStatus )
352
+ }
353
+
354
+ func (t versionedTracker ) update (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus bool ) error {
321
355
accessor , err := meta .Accessor (obj )
322
356
if err != nil {
323
357
return fmt .Errorf ("failed to get accessor for object: %w" , err )
@@ -348,6 +382,20 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
348
382
return err
349
383
}
350
384
385
+ if t .withStatusSubresource .Has (gvk ) {
386
+ if isStatus { // copy everything but status and metadata.ResourceVersion from original object
387
+ if err := copyNonStatusFrom (oldObject , obj ); err != nil {
388
+ return fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
389
+ }
390
+ } else { // copy status from original object
391
+ if err := copyStatusFrom (oldObject , obj ); err != nil {
392
+ return fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
393
+ }
394
+ }
395
+ } else if isStatus {
396
+ return apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
397
+ }
398
+
351
399
oldAccessor , err := meta .Accessor (oldObject )
352
400
if err != nil {
353
401
return err
@@ -689,6 +737,10 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
689
737
}
690
738
691
739
func (c * fakeClient ) Update (ctx context.Context , obj client.Object , opts ... client.UpdateOption ) error {
740
+ return c .update (obj , false , opts ... )
741
+ }
742
+
743
+ func (c * fakeClient ) update (obj client.Object , isStatus bool , opts ... client.UpdateOption ) error {
692
744
updateOptions := & client.UpdateOptions {}
693
745
updateOptions .ApplyOptions (opts )
694
746
@@ -706,10 +758,14 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
706
758
if err != nil {
707
759
return err
708
760
}
709
- return c .tracker .Update (gvr , obj , accessor .GetNamespace ())
761
+ return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus )
710
762
}
711
763
712
764
func (c * fakeClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
765
+ return c .patch (obj , patch , opts ... )
766
+ }
767
+
768
+ func (c * fakeClient ) patch (obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
713
769
patchOptions := & client.PatchOptions {}
714
770
patchOptions .ApplyOptions (opts )
715
771
@@ -732,6 +788,11 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
732
788
return err
733
789
}
734
790
791
+ gvk , err := apiutil .GVKForObject (obj , c .scheme )
792
+ if err != nil {
793
+ return err
794
+ }
795
+
735
796
reaction := testing .ObjectReaction (c .tracker )
736
797
handled , o , err := reaction (testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data ))
737
798
if err != nil {
@@ -740,11 +801,6 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
740
801
if ! handled {
741
802
panic ("tracker could not handle patch method" )
742
803
}
743
-
744
- gvk , err := apiutil .GVKForObject (obj , c .scheme )
745
- if err != nil {
746
- return err
747
- }
748
804
ta , err := meta .TypeAccessor (o )
749
805
if err != nil {
750
806
return err
@@ -762,6 +818,97 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
762
818
return err
763
819
}
764
820
821
+ func copyNonStatusFrom (old , new runtime.Object ) error {
822
+ newClientObject , ok := new .(client.Object )
823
+ if ! ok {
824
+ return fmt .Errorf ("%T is not a client.Object" , new )
825
+ }
826
+ // The only thing other than status we have to retain
827
+ rv := newClientObject .GetResourceVersion ()
828
+
829
+ oldMapStringAny , err := toMapStringAny (old )
830
+ if err != nil {
831
+ return fmt .Errorf ("failed to convert old to *unstructured.Unstructured: %w" , err )
832
+ }
833
+ newMapStringAny , err := toMapStringAny (new )
834
+ if err != nil {
835
+ return fmt .Errorf ("failed to convert new to *unststructured.Unstructured: %w" , err )
836
+ }
837
+
838
+ // delete everything other than status in case it has fields that were not present in
839
+ // the old object
840
+ for k := range newMapStringAny {
841
+ if k != "status" {
842
+ delete (newMapStringAny , k )
843
+ }
844
+ }
845
+ // copy everything other than status from the old object
846
+ for k := range oldMapStringAny {
847
+ if k != "status" {
848
+ newMapStringAny [k ] = oldMapStringAny [k ]
849
+ }
850
+ }
851
+
852
+ newClientObject .SetResourceVersion (rv )
853
+
854
+ if err := fromMapStringAny (newMapStringAny , new ); err != nil {
855
+ return fmt .Errorf ("failed to convert back from map[string]any: %w" , err )
856
+ }
857
+ return nil
858
+ }
859
+
860
+ // copyStatusFrom copies the status from old into new
861
+ func copyStatusFrom (old , new runtime.Object ) error {
862
+ oldMapStringAny , err := toMapStringAny (old )
863
+ if err != nil {
864
+ return fmt .Errorf ("failed to convert old to *unstructured.Unstructured: %w" , err )
865
+ }
866
+ newMapStringAny , err := toMapStringAny (new )
867
+ if err != nil {
868
+ return fmt .Errorf ("failed to convert new to *unststructured.Unstructured: %w" , err )
869
+ }
870
+
871
+ newMapStringAny ["status" ] = oldMapStringAny ["status" ]
872
+
873
+ if err := fromMapStringAny (newMapStringAny , new ); err != nil {
874
+ return fmt .Errorf ("failed to convert back from map[string]any: %w" , err )
875
+ }
876
+
877
+ return nil
878
+ }
879
+
880
+ func toMapStringAny (obj runtime.Object ) (map [string ]any , error ) {
881
+ if unstructured , isUnstructured := obj .(* unstructured.Unstructured ); isUnstructured {
882
+ return unstructured .Object , nil
883
+ }
884
+
885
+ serialized , err := json .Marshal (obj )
886
+ if err != nil {
887
+ return nil , err
888
+ }
889
+
890
+ u := map [string ]any {}
891
+ return u , json .Unmarshal (serialized , & u )
892
+ }
893
+
894
+ func fromMapStringAny (u map [string ]any , target runtime.Object ) error {
895
+ if targetUnstructured , isUnstructured := target .(* unstructured.Unstructured ); isUnstructured {
896
+ targetUnstructured .Object = u
897
+ return nil
898
+ }
899
+
900
+ serialized , err := json .Marshal (u )
901
+ if err != nil {
902
+ return fmt .Errorf ("failed to serialize: %w" , err )
903
+ }
904
+
905
+ if err := json .Unmarshal (serialized , & target ); err != nil {
906
+ return fmt .Errorf ("failed to deserialize: %w" , err )
907
+ }
908
+
909
+ return nil
910
+ }
911
+
765
912
func (c * fakeClient ) Status () client.SubResourceWriter {
766
913
return c .SubResource ("status" )
767
914
}
@@ -809,22 +956,17 @@ func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object,
809
956
}
810
957
811
958
func (sw * fakeSubResourceClient ) Update (ctx context.Context , obj client.Object , opts ... client.SubResourceUpdateOption ) error {
812
- // TODO(droot): This results in full update of the obj (spec + subresources). Need
813
- // a way to update subresource only.
814
959
updateOptions := client.SubResourceUpdateOptions {}
815
960
updateOptions .ApplyOptions (opts )
816
961
817
962
body := obj
818
963
if updateOptions .SubResourceBody != nil {
819
964
body = updateOptions .SubResourceBody
820
965
}
821
- return sw .client .Update ( ctx , body , & updateOptions .UpdateOptions )
966
+ return sw .client .update ( body , true , & updateOptions .UpdateOptions )
822
967
}
823
968
824
969
func (sw * fakeSubResourceClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.SubResourcePatchOption ) error {
825
- // TODO(droot): This results in full update of the obj (spec + subresources). Need
826
- // a way to update subresource only.
827
-
828
970
patchOptions := client.SubResourcePatchOptions {}
829
971
patchOptions .ApplyOptions (opts )
830
972
@@ -833,7 +975,7 @@ func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, p
833
975
body = patchOptions .SubResourceBody
834
976
}
835
977
836
- return sw .client .Patch ( ctx , body , patch , & patchOptions .PatchOptions )
978
+ return sw .client .patch ( body , patch , & patchOptions .PatchOptions )
837
979
}
838
980
839
981
func allowsUnconditionalUpdate (gvk schema.GroupVersionKind ) bool {
@@ -933,6 +1075,42 @@ func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
933
1075
return false
934
1076
}
935
1077
1078
+ func inTreeResourcesWithStatus () []schema.GroupVersionKind {
1079
+ return []schema.GroupVersionKind {
1080
+ {Version : "v1" , Kind : "Namespace" },
1081
+ {Version : "v1" , Kind : "Node" },
1082
+ {Version : "v1" , Kind : "PersistentVolumeClaim" },
1083
+ {Version : "v1" , Kind : "PersistentVolume" },
1084
+ {Version : "v1" , Kind : "Pod" },
1085
+ {Version : "v1" , Kind : "ReplicationController" },
1086
+ {Version : "v1" , Kind : "Service" },
1087
+
1088
+ {Group : "apps" , Version : "v1" , Kind : "Deployment" },
1089
+ {Group : "apps" , Version : "v1" , Kind : "DaemonSet" },
1090
+ {Group : "apps" , Version : "v1" , Kind : "ReplicaSet" },
1091
+ {Group : "apps" , Version : "v1" , Kind : "StatefulSet" },
1092
+
1093
+ {Group : "autoscaling" , Version : "v1" , Kind : "HorizontalPodAutoscaler" },
1094
+
1095
+ {Group : "batch" , Version : "v1" , Kind : "CronJob" },
1096
+ {Group : "batch" , Version : "v1" , Kind : "Job" },
1097
+
1098
+ {Group : "certificates.k8s.io" , Version : "v1" , Kind : "CertificateSigningRequest" },
1099
+
1100
+ {Group : "networking.k8s.io" , Version : "v1" , Kind : "Ingress" },
1101
+ {Group : "networking.k8s.io" , Version : "v1" , Kind : "NetworkPolicy" },
1102
+
1103
+ {Group : "policy" , Version : "v1" , Kind : "PodDisruptionBudget" },
1104
+
1105
+ {Group : "storage.k8s.io" , Version : "v1" , Kind : "VolumeAttachment" },
1106
+
1107
+ {Group : "apiextensions.k8s.io" , Version : "v1" , Kind : "CustomResourceDefinition" },
1108
+
1109
+ {Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "FlowSchema" },
1110
+ {Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "PriorityLevelConfiguration" },
1111
+ }
1112
+ }
1113
+
936
1114
// zero zeros the value of a pointer.
937
1115
func zero (x interface {}) {
938
1116
if x == nil {
0 commit comments