@@ -57,13 +57,17 @@ import (
5757 "k8s.io/apimachinery/pkg/labels"
5858 "k8s.io/apimachinery/pkg/runtime"
5959 "k8s.io/apimachinery/pkg/runtime/schema"
60+ "k8s.io/apimachinery/pkg/runtime/serializer"
6061 "k8s.io/apimachinery/pkg/types"
62+ "k8s.io/apimachinery/pkg/util/managedfields"
6163 utilrand "k8s.io/apimachinery/pkg/util/rand"
6264 "k8s.io/apimachinery/pkg/util/sets"
6365 "k8s.io/apimachinery/pkg/util/strategicpatch"
6466 "k8s.io/apimachinery/pkg/util/validation/field"
6567 "k8s.io/apimachinery/pkg/watch"
68+ clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
6669 "k8s.io/client-go/kubernetes/scheme"
70+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
6771 "k8s.io/client-go/testing"
6872 "k8s.io/utils/ptr"
6973
@@ -131,6 +135,7 @@ type ClientBuilder struct {
131135 withStatusSubresource []client.Object
132136 objectTracker testing.ObjectTracker
133137 interceptorFuncs * interceptor.Funcs
138+ typeConverters []managedfields.TypeConverter
134139
135140 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
136141 // The inner map maps from index name to IndexerFunc.
@@ -172,6 +177,8 @@ func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *C
172177}
173178
174179// WithObjectTracker can be optionally used to initialize this fake client with testing.ObjectTracker.
180+ // Setting this is incompatible with setting WithTypeConverters, as they are a setting on the
181+ // tracker.
175182func (f * ClientBuilder ) WithObjectTracker (ot testing.ObjectTracker ) * ClientBuilder {
176183 f .objectTracker = ot
177184 return f
@@ -228,6 +235,18 @@ func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs)
228235 return f
229236}
230237
238+ // WithTypeConverters sets the type converters for the fake client. The list is ordered and the first
239+ // non-erroring converter is used.
240+ // This setting is incompatible with WithObjectTracker, as the type converters are a setting on the tracker.
241+ //
242+ // If unset, this defaults to:
243+ // * clientgoapplyconfigurations.NewTypeConverter(clientgoscheme.Scheme),
244+ // * managedfields.NewDeducedTypeConverter(),
245+ func (f * ClientBuilder ) WithTypeConverters (typeConverters ... managedfields.TypeConverter ) * ClientBuilder {
246+ f .typeConverters = append (f .typeConverters , typeConverters ... )
247+ return f
248+ }
249+
231250// Build builds and returns a new fake client.
232251func (f * ClientBuilder ) Build () client.WithWatch {
233252 if f .scheme == nil {
@@ -248,11 +267,29 @@ func (f *ClientBuilder) Build() client.WithWatch {
248267 withStatusSubResource .Insert (gvk )
249268 }
250269
270+ if f .objectTracker != nil && len (f .typeConverters ) > 0 {
271+ panic (errors .New ("WithObjectTracker and WithTypeConverters are incompatible" ))
272+ }
273+
251274 if f .objectTracker == nil {
252- tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme , withStatusSubresource : withStatusSubResource }
253- } else {
254- tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme , withStatusSubresource : withStatusSubResource }
275+ if len (f .typeConverters ) == 0 {
276+ f .typeConverters = []managedfields.TypeConverter {
277+ // Use corresponding scheme to ensure the converter error
278+ // for types it can't handle.
279+ clientgoapplyconfigurations .NewTypeConverter (clientgoscheme .Scheme ),
280+ managedfields .NewDeducedTypeConverter (),
281+ }
282+ }
283+ f .objectTracker = testing .NewFieldManagedObjectTracker (
284+ f .scheme ,
285+ serializer .NewCodecFactory (f .scheme ).UniversalDecoder (),
286+ multiTypeConverter {upstream : f .typeConverters },
287+ )
255288 }
289+ tracker = versionedTracker {
290+ ObjectTracker : f .objectTracker ,
291+ scheme : f .scheme ,
292+ withStatusSubresource : withStatusSubResource }
256293
257294 for _ , obj := range f .initObject {
258295 if err := tracker .Add (obj ); err != nil {
@@ -929,6 +966,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
929966 if err != nil {
930967 return err
931968 }
969+
970+ // otherwise the merge logic in the tracker complains
971+ if patch .Type () == types .ApplyPatchType {
972+ obj .SetManagedFields (nil )
973+ }
974+
932975 data , err := patch .Data (obj )
933976 if err != nil {
934977 return err
@@ -943,7 +986,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
943986 defer c .trackerWriteLock .Unlock ()
944987 oldObj , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
945988 if err != nil {
946- return err
989+ if patch .Type () != types .ApplyPatchType {
990+ return err
991+ }
992+ oldObj = & unstructured.Unstructured {}
947993 }
948994 oldAccessor , err := meta .Accessor (oldObj )
949995 if err != nil {
@@ -958,7 +1004,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
9581004 // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
9591005 // to updating the object.
9601006 action := testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data )
961- o , err := dryPatch (action , c .tracker )
1007+ o , err := dryPatch (action , c .tracker , obj )
9621008 if err != nil {
9631009 return err
9641010 }
@@ -1017,12 +1063,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
10171063// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
10181064// and easier than refactoring the k8s client-go method upstream.
10191065// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
1020- func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
1066+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker , newObj runtime. Object ) (runtime.Object , error ) {
10211067 ns := action .GetNamespace ()
10221068 gvr := action .GetResource ()
10231069
10241070 obj , err := tracker .Get (gvr , ns , action .GetName ())
10251071 if err != nil {
1072+ if action .GetPatchType () == types .ApplyPatchType {
1073+ return & unstructured.Unstructured {}, nil
1074+ }
10261075 return nil , err
10271076 }
10281077
@@ -1067,10 +1116,20 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
10671116 if err = json .Unmarshal (mergedByte , obj ); err != nil {
10681117 return nil , err
10691118 }
1070- case types .ApplyPatchType :
1071- return nil , errors .New ("apply patches are not supported in the fake client. Follow https://github.com/kubernetes/kubernetes/issues/115598 for the current status" )
10721119 case types .ApplyCBORPatchType :
10731120 return nil , errors .New ("apply CBOR patches are not supported in the fake client" )
1121+ case types .ApplyPatchType :
1122+ // There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
1123+ // We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
1124+ defer func () {
1125+ if err := tracker .Add (obj ); err != nil {
1126+ panic (err )
1127+ }
1128+ }()
1129+ if err := tracker .Apply (gvr , newObj , ns , action .PatchOptions ); err != nil {
1130+ return nil , err
1131+ }
1132+ return tracker .Get (gvr , ns , action .GetName ())
10741133 default :
10751134 return nil , fmt .Errorf ("%s PatchType is not supported" , action .GetPatchType ())
10761135 }
0 commit comments