@@ -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