@@ -35,6 +35,7 @@ import (
3535 "k8s.io/apimachinery/pkg/runtime"
3636 "k8s.io/apimachinery/pkg/runtime/schema"
3737 utilrand "k8s.io/apimachinery/pkg/util/rand"
38+ "k8s.io/apimachinery/pkg/util/sets"
3839 "k8s.io/apimachinery/pkg/util/validation/field"
3940 "k8s.io/apimachinery/pkg/watch"
4041 "k8s.io/client-go/kubernetes/scheme"
@@ -48,13 +49,15 @@ import (
4849
4950type versionedTracker struct {
5051 testing.ObjectTracker
51- scheme * runtime.Scheme
52+ scheme * runtime.Scheme
53+ withStatusSubresource sets.Set [schema.GroupVersionKind ]
5254}
5355
5456type fakeClient struct {
55- tracker versionedTracker
56- scheme * runtime.Scheme
57- restMapper meta.RESTMapper
57+ tracker versionedTracker
58+ scheme * runtime.Scheme
59+ restMapper meta.RESTMapper
60+ withStatusSubresource sets.Set [schema.GroupVersionKind ]
5861
5962 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
6063 // The inner map maps from index name to IndexerFunc.
@@ -95,12 +98,13 @@ func NewClientBuilder() *ClientBuilder {
9598
9699// ClientBuilder builds a fake client.
97100type 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
101+ scheme * runtime.Scheme
102+ restMapper meta.RESTMapper
103+ initObject []client.Object
104+ initLists []client.ObjectList
105+ initRuntimeObjects []runtime.Object
106+ withStatusSubresource []client.Object
107+ objectTracker testing.ObjectTracker
104108
105109 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
106110 // The inner map maps from index name to IndexerFunc.
@@ -185,6 +189,13 @@ func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue
185189 return f
186190}
187191
192+ // WithStatusSubresource configures the passed object with a status subresource, which means
193+ // calls to Update and Patch will not alters its status.
194+ func (f * ClientBuilder ) WithStatusSubresource (o ... client.Object ) * ClientBuilder {
195+ f .withStatusSubresource = append (f .withStatusSubresource , o ... )
196+ return f
197+ }
198+
188199// Build builds and returns a new fake client.
189200func (f * ClientBuilder ) Build () client.WithWatch {
190201 if f .scheme == nil {
@@ -196,10 +207,19 @@ func (f *ClientBuilder) Build() client.WithWatch {
196207
197208 var tracker versionedTracker
198209
210+ withStatusSubResource := sets .New (inTreeResourcesWithStatus ()... )
211+ for _ , o := range f .withStatusSubresource {
212+ gvk , err := apiutil .GVKForObject (o , f .scheme )
213+ if err != nil {
214+ panic (fmt .Errorf ("failed to get gvk for object %T: %w" , withStatusSubResource , err ))
215+ }
216+ withStatusSubResource .Insert (gvk )
217+ }
218+
199219 if f .objectTracker == nil {
200- tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme }
220+ tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme , withStatusSubresource : withStatusSubResource }
201221 } else {
202- tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme }
222+ tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme , withStatusSubresource : withStatusSubResource }
203223 }
204224
205225 for _ , obj := range f .initObject {
@@ -217,11 +237,13 @@ func (f *ClientBuilder) Build() client.WithWatch {
217237 panic (fmt .Errorf ("failed to add runtime object %v to fake client: %w" , obj , err ))
218238 }
219239 }
240+
220241 return & fakeClient {
221- tracker : tracker ,
222- scheme : f .scheme ,
223- restMapper : f .restMapper ,
224- indexes : f .indexes ,
242+ tracker : tracker ,
243+ scheme : f .scheme ,
244+ restMapper : f .restMapper ,
245+ indexes : f .indexes ,
246+ withStatusSubresource : withStatusSubResource ,
225247 }
226248}
227249
@@ -318,6 +340,10 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
318340}
319341
320342func (t versionedTracker ) Update (gvr schema.GroupVersionResource , obj runtime.Object , ns string ) error {
343+ return t .update (gvr , obj , ns , false )
344+ }
345+
346+ func (t versionedTracker ) update (gvr schema.GroupVersionResource , obj runtime.Object , ns string , isStatus bool ) error {
321347 accessor , err := meta .Accessor (obj )
322348 if err != nil {
323349 return fmt .Errorf ("failed to get accessor for object: %w" , err )
@@ -337,6 +363,9 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
337363 return err
338364 }
339365 }
366+ if ! isStatus && t .withStatusSubresource .Has (gvk ) {
367+ clearObjectStatus (obj )
368+ }
340369
341370 oldObject , err := t .ObjectTracker .Get (gvr , ns , accessor .GetName ())
342371 if err != nil {
@@ -689,6 +718,10 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
689718}
690719
691720func (c * fakeClient ) Update (ctx context.Context , obj client.Object , opts ... client.UpdateOption ) error {
721+ return c .update (obj , false , opts ... )
722+ }
723+
724+ func (c * fakeClient ) update (obj client.Object , isStatus bool , opts ... client.UpdateOption ) error {
692725 updateOptions := & client.UpdateOptions {}
693726 updateOptions .ApplyOptions (opts )
694727
@@ -706,10 +739,14 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
706739 if err != nil {
707740 return err
708741 }
709- return c .tracker .Update (gvr , obj , accessor .GetNamespace ())
742+ return c .tracker .update (gvr , obj , accessor .GetNamespace (), isStatus )
710743}
711744
712745func (c * fakeClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
746+ return c .patch (obj , patch , false , opts ... )
747+ }
748+
749+ func (c * fakeClient ) patch (obj client.Object , patch client.Patch , isStatus bool , opts ... client.PatchOption ) error {
713750 patchOptions := & client.PatchOptions {}
714751 patchOptions .ApplyOptions (opts )
715752
@@ -732,6 +769,18 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
732769 return err
733770 }
734771
772+ gvk , err := apiutil .GVKForObject (obj , c .scheme )
773+ if err != nil {
774+ return err
775+ }
776+
777+ if ! isStatus && c .withStatusSubresource .Has (gvk ) {
778+ data , err = clearStatus (data )
779+ if err != nil {
780+ return fmt .Errorf ("failed to clear status: %w" , err )
781+ }
782+ }
783+
735784 reaction := testing .ObjectReaction (c .tracker )
736785 handled , o , err := reaction (testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data ))
737786 if err != nil {
@@ -741,10 +790,6 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
741790 panic ("tracker could not handle patch method" )
742791 }
743792
744- gvk , err := apiutil .GVKForObject (obj , c .scheme )
745- if err != nil {
746- return err
747- }
748793 ta , err := meta .TypeAccessor (o )
749794 if err != nil {
750795 return err
@@ -762,6 +807,34 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
762807 return err
763808}
764809
810+ func clearObjectStatus (o runtime.Object ) {
811+ if unstructured , isUnstructured := o .(* unstructured.Unstructured ); isUnstructured {
812+ delete (unstructured .Object , "status" )
813+ return
814+ }
815+ tp := reflect .TypeOf (o )
816+ value := reflect .ValueOf (o )
817+ if tp .Kind () == reflect .Pointer {
818+ tp = tp .Elem ()
819+ value = value .Elem ()
820+ }
821+ statusType , found := tp .FieldByName ("Status" )
822+ if ! found {
823+ return
824+ }
825+ value .FieldByName ("Status" ).Set (reflect .New (statusType .Type ).Elem ())
826+ }
827+
828+ func clearStatus (in []byte ) ([]byte , error ) {
829+ m := map [string ]any {}
830+ if err := json .Unmarshal (in , & m ); err != nil {
831+ return nil , err
832+ }
833+ delete (m , "status" )
834+
835+ return json .Marshal (m )
836+ }
837+
765838func (c * fakeClient ) Status () client.SubResourceWriter {
766839 return c .SubResource ("status" )
767840}
@@ -818,7 +891,7 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
818891 if updateOptions .SubResourceBody != nil {
819892 body = updateOptions .SubResourceBody
820893 }
821- return sw .client .Update ( ctx , body , & updateOptions .UpdateOptions )
894+ return sw .client .update ( body , true , & updateOptions .UpdateOptions )
822895}
823896
824897func (sw * fakeSubResourceClient ) Patch (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.SubResourcePatchOption ) error {
@@ -833,7 +906,7 @@ func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, p
833906 body = patchOptions .SubResourceBody
834907 }
835908
836- return sw .client .Patch ( ctx , body , patch , & patchOptions .PatchOptions )
909+ return sw .client .patch ( body , patch , true , & patchOptions .PatchOptions )
837910}
838911
839912func allowsUnconditionalUpdate (gvk schema.GroupVersionKind ) bool {
@@ -933,6 +1006,42 @@ func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
9331006 return false
9341007}
9351008
1009+ func inTreeResourcesWithStatus () []schema.GroupVersionKind {
1010+ return []schema.GroupVersionKind {
1011+ {Version : "v1" , Kind : "Namespace" },
1012+ {Version : "v1" , Kind : "Node" },
1013+ {Version : "v1" , Kind : "PersistentVolumeClaim" },
1014+ {Version : "v1" , Kind : "PersistentVolume" },
1015+ {Version : "v1" , Kind : "Pod" },
1016+ {Version : "v1" , Kind : "ReplicationController" },
1017+ {Version : "v1" , Kind : "Service" },
1018+
1019+ {Group : "apps" , Version : "v1" , Kind : "Deployment" },
1020+ {Group : "apps" , Version : "v1" , Kind : "DaemonSet" },
1021+ {Group : "apps" , Version : "v1" , Kind : "ReplicaSet" },
1022+ {Group : "apps" , Version : "v1" , Kind : "StatefulSet" },
1023+
1024+ {Group : "autoscaling" , Version : "v1" , Kind : "HorizontalPodAutoscaler" },
1025+
1026+ {Group : "batch" , Version : "v1" , Kind : "CronJob" },
1027+ {Group : "batch" , Version : "v1" , Kind : "Job" },
1028+
1029+ {Group : "certificates.k8s.io" , Version : "v1" , Kind : "CertificateSigningRequest" },
1030+
1031+ {Group : "networking.k8s.io" , Version : "v1" , Kind : "Ingress" },
1032+ {Group : "networking.k8s.io" , Version : "v1" , Kind : "NetworkPolicy" },
1033+
1034+ {Group : "policy" , Version : "v1" , Kind : "PodDisruptionBudget" },
1035+
1036+ {Group : "storage.k8s.io" , Version : "v1" , Kind : "VolumeAttachment" },
1037+
1038+ {Group : "apiextensions.k8s.io" , Version : "v1" , Kind : "CustomResourceDefinition" },
1039+
1040+ {Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "FlowSchema" },
1041+ {Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "PriorityLevelConfiguration" },
1042+ }
1043+ }
1044+
9361045// zero zeros the value of a pointer.
9371046func zero (x interface {}) {
9381047 if x == nil {
0 commit comments