@@ -17,11 +17,13 @@ limitations under the License.
1717package  fake
1818
1919import  (
20+ 	"bytes" 
2021	"context" 
2122	"encoding/json" 
2223	"errors" 
2324	"fmt" 
2425	"reflect" 
26+ 	"runtime/debug" 
2527	"strconv" 
2628	"strings" 
2729	"sync" 
@@ -35,6 +37,7 @@ import (
3537	"k8s.io/apimachinery/pkg/runtime" 
3638	"k8s.io/apimachinery/pkg/runtime/schema" 
3739	utilrand "k8s.io/apimachinery/pkg/util/rand" 
40+ 	"k8s.io/apimachinery/pkg/util/sets" 
3841	"k8s.io/apimachinery/pkg/util/validation/field" 
3942	"k8s.io/apimachinery/pkg/watch" 
4043	"k8s.io/client-go/kubernetes/scheme" 
@@ -48,13 +51,15 @@ import (
4851
4952type  versionedTracker  struct  {
5053	testing.ObjectTracker 
51- 	scheme  * runtime.Scheme 
54+ 	scheme                 * runtime.Scheme 
55+ 	withStatusSubresource  sets.Set [schema.GroupVersionKind ]
5256}
5357
5458type  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 ]
5863
5964	// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. 
6065	// The inner map maps from index name to IndexerFunc. 
@@ -95,12 +100,13 @@ func NewClientBuilder() *ClientBuilder {
95100
96101// ClientBuilder builds a fake client. 
97102type  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 
104110
105111	// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. 
106112	// The inner map maps from index name to IndexerFunc. 
@@ -185,6 +191,13 @@ func (f *ClientBuilder) WithIndex(obj runtime.Object, field string, extractValue
185191	return  f 
186192}
187193
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+ 
188201// Build builds and returns a new fake client. 
189202func  (f  * ClientBuilder ) Build () client.WithWatch  {
190203	if  f .scheme  ==  nil  {
@@ -196,10 +209,19 @@ func (f *ClientBuilder) Build() client.WithWatch {
196209
197210	var  tracker  versionedTracker 
198211
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+ 
199221	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 }
201223	} else  {
202- 		tracker  =  versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme }
224+ 		tracker  =  versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme ,  withStatusSubresource :  withStatusSubResource }
203225	}
204226
205227	for  _ , obj  :=  range  f .initObject  {
@@ -217,11 +239,13 @@ func (f *ClientBuilder) Build() client.WithWatch {
217239			panic (fmt .Errorf ("failed to add runtime object %v to fake client: %w" , obj , err ))
218240		}
219241	}
242+ 
220243	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 ,
225249	}
226250}
227251
@@ -320,6 +344,16 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
320344}
321345
322346func  (t  versionedTracker ) Update (gvr  schema.GroupVersionResource , obj  runtime.Object , ns  string ) error  {
347+ 	isStatus  :=  false 
348+ 	// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change 
349+ 	// that reaction, we use the callstack to figure out if this originated from the status client. 
350+ 	if  bytes .Contains (debug .Stack (), []byte ("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).Patch" )) {
351+ 		isStatus  =  true 
352+ 	}
353+ 	return  t .update (gvr , obj , ns , isStatus )
354+ }
355+ 
356+ func  (t  versionedTracker ) update (gvr  schema.GroupVersionResource , obj  runtime.Object , ns  string , isStatus  bool ) error  {
323357	accessor , err  :=  meta .Accessor (obj )
324358	if  err  !=  nil  {
325359		return  fmt .Errorf ("failed to get accessor for object: %w" , err )
@@ -350,6 +384,20 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
350384		return  err 
351385	}
352386
387+ 	if  t .withStatusSubresource .Has (gvk ) {
388+ 		if  isStatus  { // copy everything but status and metadata.ResourceVersion from original object 
389+ 			if  err  :=  copyNonStatusFrom (oldObject , obj ); err  !=  nil  {
390+ 				return  fmt .Errorf ("failed to copy non-status field for object with status subresouce: %w" , err )
391+ 			}
392+ 		} else  { // copy status from original object 
393+ 			if  err  :=  copyStatusFrom (oldObject , obj ); err  !=  nil  {
394+ 				return  fmt .Errorf ("failed to copy the status for object with status subresource: %w" , err )
395+ 			}
396+ 		}
397+ 	} else  if  isStatus  {
398+ 		return  apierrors .NewNotFound (gvr .GroupResource (), accessor .GetName ())
399+ 	}
400+ 
353401	oldAccessor , err  :=  meta .Accessor (oldObject )
354402	if  err  !=  nil  {
355403		return  err 
@@ -691,6 +739,10 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
691739}
692740
693741func  (c  * fakeClient ) Update (ctx  context.Context , obj  client.Object , opts  ... client.UpdateOption ) error  {
742+ 	return  c .update (obj , false , opts ... )
743+ }
744+ 
745+ func  (c  * fakeClient ) update (obj  client.Object , isStatus  bool , opts  ... client.UpdateOption ) error  {
694746	updateOptions  :=  & client.UpdateOptions {}
695747	updateOptions .ApplyOptions (opts )
696748
@@ -708,10 +760,14 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie
708760	if  err  !=  nil  {
709761		return  err 
710762	}
711- 	return  c .tracker .Update (gvr , obj , accessor .GetNamespace ())
763+ 	return  c .tracker .update (gvr , obj , accessor .GetNamespace (),  isStatus )
712764}
713765
714766func  (c  * fakeClient ) Patch (ctx  context.Context , obj  client.Object , patch  client.Patch , opts  ... client.PatchOption ) error  {
767+ 	return  c .patch (obj , patch , opts ... )
768+ }
769+ 
770+ func  (c  * fakeClient ) patch (obj  client.Object , patch  client.Patch , opts  ... client.PatchOption ) error  {
715771	patchOptions  :=  & client.PatchOptions {}
716772	patchOptions .ApplyOptions (opts )
717773
@@ -734,6 +790,11 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
734790		return  err 
735791	}
736792
793+ 	gvk , err  :=  apiutil .GVKForObject (obj , c .scheme )
794+ 	if  err  !=  nil  {
795+ 		return  err 
796+ 	}
797+ 
737798	reaction  :=  testing .ObjectReaction (c .tracker )
738799	handled , o , err  :=  reaction (testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data ))
739800	if  err  !=  nil  {
@@ -742,11 +803,6 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
742803	if  ! handled  {
743804		panic ("tracker could not handle patch method" )
744805	}
745- 
746- 	gvk , err  :=  apiutil .GVKForObject (obj , c .scheme )
747- 	if  err  !=  nil  {
748- 		return  err 
749- 	}
750806	ta , err  :=  meta .TypeAccessor (o )
751807	if  err  !=  nil  {
752808		return  err 
@@ -764,6 +820,97 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.
764820	return  err 
765821}
766822
823+ func  copyNonStatusFrom (old , new  runtime.Object ) error  {
824+ 	newClientObject , ok  :=  new .(client.Object )
825+ 	if  ! ok  {
826+ 		return  fmt .Errorf ("%T is not a client.Object" , new )
827+ 	}
828+ 	// The only thing other than status we have to retain 
829+ 	rv  :=  newClientObject .GetResourceVersion ()
830+ 
831+ 	oldMapStringAny , err  :=  toMapStringAny (old )
832+ 	if  err  !=  nil  {
833+ 		return  fmt .Errorf ("failed to convert old to *unstructured.Unstructured: %w" , err )
834+ 	}
835+ 	newMapStringAny , err  :=  toMapStringAny (new )
836+ 	if  err  !=  nil  {
837+ 		return  fmt .Errorf ("failed to convert new to *unststructured.Unstructured: %w" , err )
838+ 	}
839+ 
840+ 	// delete everything other than status in case it has fields that were not present in 
841+ 	// the old object 
842+ 	for  k  :=  range  newMapStringAny  {
843+ 		if  k  !=  "status"  {
844+ 			delete (newMapStringAny , k )
845+ 		}
846+ 	}
847+ 	// copy everything other than status from the old object 
848+ 	for  k  :=  range  oldMapStringAny  {
849+ 		if  k  !=  "status"  {
850+ 			newMapStringAny [k ] =  oldMapStringAny [k ]
851+ 		}
852+ 	}
853+ 
854+ 	newClientObject .SetResourceVersion (rv )
855+ 
856+ 	if  err  :=  fromMapStringAny (newMapStringAny , new ); err  !=  nil  {
857+ 		return  fmt .Errorf ("failed to convert back from map[string]any: %w" , err )
858+ 	}
859+ 	return  nil 
860+ }
861+ 
862+ // copyStatusFrom copies the status from old into new 
863+ func  copyStatusFrom (old , new  runtime.Object ) error  {
864+ 	oldMapStringAny , err  :=  toMapStringAny (old )
865+ 	if  err  !=  nil  {
866+ 		return  fmt .Errorf ("failed to convert old to *unstructured.Unstructured: %w" , err )
867+ 	}
868+ 	newMapStringAny , err  :=  toMapStringAny (new )
869+ 	if  err  !=  nil  {
870+ 		return  fmt .Errorf ("failed to convert new to *unststructured.Unstructured: %w" , err )
871+ 	}
872+ 
873+ 	newMapStringAny ["status" ] =  oldMapStringAny ["status" ]
874+ 
875+ 	if  err  :=  fromMapStringAny (newMapStringAny , new ); err  !=  nil  {
876+ 		return  fmt .Errorf ("failed to convert back from map[string]any: %w" , err )
877+ 	}
878+ 
879+ 	return  nil 
880+ }
881+ 
882+ func  toMapStringAny (obj  runtime.Object ) (map [string ]any , error ) {
883+ 	if  unstructured , isUnstructured  :=  obj .(* unstructured.Unstructured ); isUnstructured  {
884+ 		return  unstructured .Object , nil 
885+ 	}
886+ 
887+ 	serialized , err  :=  json .Marshal (obj )
888+ 	if  err  !=  nil  {
889+ 		return  nil , err 
890+ 	}
891+ 
892+ 	u  :=  map [string ]any {}
893+ 	return  u , json .Unmarshal (serialized , & u )
894+ }
895+ 
896+ func  fromMapStringAny (u  map [string ]any , target  runtime.Object ) error  {
897+ 	if  targetUnstructured , isUnstructured  :=  target .(* unstructured.Unstructured ); isUnstructured  {
898+ 		targetUnstructured .Object  =  u 
899+ 		return  nil 
900+ 	}
901+ 
902+ 	serialized , err  :=  json .Marshal (u )
903+ 	if  err  !=  nil  {
904+ 		return  fmt .Errorf ("failed to serialize: %w" , err )
905+ 	}
906+ 
907+ 	if  err  :=  json .Unmarshal (serialized , & target ); err  !=  nil  {
908+ 		return  fmt .Errorf ("failed to deserialize: %w" , err )
909+ 	}
910+ 
911+ 	return  nil 
912+ }
913+ 
767914func  (c  * fakeClient ) Status () client.SubResourceWriter  {
768915	return  c .SubResource ("status" )
769916}
@@ -811,22 +958,17 @@ func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object,
811958}
812959
813960func  (sw  * fakeSubResourceClient ) Update (ctx  context.Context , obj  client.Object , opts  ... client.SubResourceUpdateOption ) error  {
814- 	// TODO(droot): This results in full update of the obj (spec + subresources). Need 
815- 	// a way to update subresource only. 
816961	updateOptions  :=  client.SubResourceUpdateOptions {}
817962	updateOptions .ApplyOptions (opts )
818963
819964	body  :=  obj 
820965	if  updateOptions .SubResourceBody  !=  nil  {
821966		body  =  updateOptions .SubResourceBody 
822967	}
823- 	return  sw .client .Update ( ctx ,  body , & updateOptions .UpdateOptions )
968+ 	return  sw .client .update ( body ,  true , & updateOptions .UpdateOptions )
824969}
825970
826971func  (sw  * fakeSubResourceClient ) Patch (ctx  context.Context , obj  client.Object , patch  client.Patch , opts  ... client.SubResourcePatchOption ) error  {
827- 	// TODO(droot): This results in full update of the obj (spec + subresources). Need 
828- 	// a way to update subresource only. 
829- 
830972	patchOptions  :=  client.SubResourcePatchOptions {}
831973	patchOptions .ApplyOptions (opts )
832974
@@ -835,7 +977,7 @@ func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, p
835977		body  =  patchOptions .SubResourceBody 
836978	}
837979
838- 	return  sw .client .Patch ( ctx ,  body , patch , & patchOptions .PatchOptions )
980+ 	return  sw .client .patch ( body , patch , & patchOptions .PatchOptions )
839981}
840982
841983func  allowsUnconditionalUpdate (gvk  schema.GroupVersionKind ) bool  {
@@ -935,6 +1077,42 @@ func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool {
9351077	return  false 
9361078}
9371079
1080+ func  inTreeResourcesWithStatus () []schema.GroupVersionKind  {
1081+ 	return  []schema.GroupVersionKind {
1082+ 		{Version : "v1" , Kind : "Namespace" },
1083+ 		{Version : "v1" , Kind : "Node" },
1084+ 		{Version : "v1" , Kind : "PersistentVolumeClaim" },
1085+ 		{Version : "v1" , Kind : "PersistentVolume" },
1086+ 		{Version : "v1" , Kind : "Pod" },
1087+ 		{Version : "v1" , Kind : "ReplicationController" },
1088+ 		{Version : "v1" , Kind : "Service" },
1089+ 
1090+ 		{Group : "apps" , Version : "v1" , Kind : "Deployment" },
1091+ 		{Group : "apps" , Version : "v1" , Kind : "DaemonSet" },
1092+ 		{Group : "apps" , Version : "v1" , Kind : "ReplicaSet" },
1093+ 		{Group : "apps" , Version : "v1" , Kind : "StatefulSet" },
1094+ 
1095+ 		{Group : "autoscaling" , Version : "v1" , Kind : "HorizontalPodAutoscaler" },
1096+ 
1097+ 		{Group : "batch" , Version : "v1" , Kind : "CronJob" },
1098+ 		{Group : "batch" , Version : "v1" , Kind : "Job" },
1099+ 
1100+ 		{Group : "certificates.k8s.io" , Version : "v1" , Kind : "CertificateSigningRequest" },
1101+ 
1102+ 		{Group : "networking.k8s.io" , Version : "v1" , Kind : "Ingress" },
1103+ 		{Group : "networking.k8s.io" , Version : "v1" , Kind : "NetworkPolicy" },
1104+ 
1105+ 		{Group : "policy" , Version : "v1" , Kind : "PodDisruptionBudget" },
1106+ 
1107+ 		{Group : "storage.k8s.io" , Version : "v1" , Kind : "VolumeAttachment" },
1108+ 
1109+ 		{Group : "apiextensions.k8s.io" , Version : "v1" , Kind : "CustomResourceDefinition" },
1110+ 
1111+ 		{Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "FlowSchema" },
1112+ 		{Group : "flowcontrol.apiserver.k8s.io" , Version : "v1beta2" , Kind : "PriorityLevelConfiguration" },
1113+ 	}
1114+ }
1115+ 
9381116// zero zeros the value of a pointer. 
9391117func  zero (x  interface {}) {
9401118	if  x  ==  nil  {
0 commit comments