@@ -45,13 +45,17 @@ import (
4545 "k8s.io/apimachinery/pkg/labels"
4646 "k8s.io/apimachinery/pkg/runtime"
4747 "k8s.io/apimachinery/pkg/runtime/schema"
48+ "k8s.io/apimachinery/pkg/runtime/serializer"
4849 "k8s.io/apimachinery/pkg/types"
50+ "k8s.io/apimachinery/pkg/util/managedfields"
4951 utilrand "k8s.io/apimachinery/pkg/util/rand"
5052 "k8s.io/apimachinery/pkg/util/sets"
5153 "k8s.io/apimachinery/pkg/util/strategicpatch"
5254 "k8s.io/apimachinery/pkg/util/validation/field"
5355 "k8s.io/apimachinery/pkg/watch"
56+ clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
5457 "k8s.io/client-go/kubernetes/scheme"
58+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
5559 "k8s.io/client-go/testing"
5660 "k8s.io/utils/ptr"
5761
@@ -119,6 +123,7 @@ type ClientBuilder struct {
119123 withStatusSubresource []client.Object
120124 objectTracker testing.ObjectTracker
121125 interceptorFuncs * interceptor.Funcs
126+ typeConverters []managedfields.TypeConverter
122127
123128 // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
124129 // The inner map maps from index name to IndexerFunc.
@@ -160,6 +165,8 @@ func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *C
160165}
161166
162167// WithObjectTracker can be optionally used to initialize this fake client with testing.ObjectTracker.
168+ // Setting this is incompatible with setting WithTypeConverters, as they are a setting on the
169+ // tracker.
163170func (f * ClientBuilder ) WithObjectTracker (ot testing.ObjectTracker ) * ClientBuilder {
164171 f .objectTracker = ot
165172 return f
@@ -216,6 +223,18 @@ func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs)
216223 return f
217224}
218225
226+ // WithTypeConverters sets the type converters for the fake client. The list is ordered and the first
227+ // non-erroring converter is used.
228+ // This setting is incompatible with WithObjectTracker, as the type converters are a setting on the tracker.
229+ //
230+ // If unset, this defaults to:
231+ // * clientgoapplyconfigurations.NewTypeConverter(clientgoscheme.Scheme),
232+ // * managedfields.NewDeducedTypeConverter(),
233+ func (f * ClientBuilder ) WithTypeConverters (typeConverters ... managedfields.TypeConverter ) * ClientBuilder {
234+ f .typeConverters = append (f .typeConverters , typeConverters ... )
235+ return f
236+ }
237+
219238// Build builds and returns a new fake client.
220239func (f * ClientBuilder ) Build () client.WithWatch {
221240 if f .scheme == nil {
@@ -236,11 +255,29 @@ func (f *ClientBuilder) Build() client.WithWatch {
236255 withStatusSubResource .Insert (gvk )
237256 }
238257
258+ if f .objectTracker != nil && len (f .typeConverters ) > 0 {
259+ panic (errors .New ("WithObjectTracker and WithTypeConverters are incompatible" ))
260+ }
261+
239262 if f .objectTracker == nil {
240- tracker = versionedTracker {ObjectTracker : testing .NewObjectTracker (f .scheme , scheme .Codecs .UniversalDecoder ()), scheme : f .scheme , withStatusSubresource : withStatusSubResource }
241- } else {
242- tracker = versionedTracker {ObjectTracker : f .objectTracker , scheme : f .scheme , withStatusSubresource : withStatusSubResource }
263+ if len (f .typeConverters ) == 0 {
264+ f .typeConverters = []managedfields.TypeConverter {
265+ // Use corresponding scheme to ensure the converter error
266+ // for types it can't handle.
267+ clientgoapplyconfigurations .NewTypeConverter (clientgoscheme .Scheme ),
268+ managedfields .NewDeducedTypeConverter (),
269+ }
270+ }
271+ f .objectTracker = testing .NewFieldManagedObjectTracker (
272+ f .scheme ,
273+ serializer .NewCodecFactory (f .scheme ).UniversalDecoder (),
274+ multiTypeConverter {upstream : f .typeConverters },
275+ )
243276 }
277+ tracker = versionedTracker {
278+ ObjectTracker : f .objectTracker ,
279+ scheme : f .scheme ,
280+ withStatusSubresource : withStatusSubResource }
244281
245282 for _ , obj := range f .initObject {
246283 if err := tracker .Add (obj ); err != nil {
@@ -901,6 +938,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
901938 if err != nil {
902939 return err
903940 }
941+
942+ // otherwise the merge logic in the tracker complains
943+ if patch .Type () == types .ApplyPatchType {
944+ obj .SetManagedFields (nil )
945+ }
946+
904947 data , err := patch .Data (obj )
905948 if err != nil {
906949 return err
@@ -915,7 +958,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
915958 defer c .trackerWriteLock .Unlock ()
916959 oldObj , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
917960 if err != nil {
918- return err
961+ if patch .Type () != types .ApplyPatchType {
962+ return err
963+ }
964+ oldObj = & unstructured.Unstructured {}
919965 }
920966 oldAccessor , err := meta .Accessor (oldObj )
921967 if err != nil {
@@ -930,7 +976,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
930976 // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
931977 // to updating the object.
932978 action := testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data )
933- o , err := dryPatch (action , c .tracker )
979+ o , err := dryPatch (action , c .tracker , obj )
934980 if err != nil {
935981 return err
936982 }
@@ -989,12 +1035,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
9891035// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
9901036// and easier than refactoring the k8s client-go method upstream.
9911037// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
992- func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
1038+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker , newObj runtime. Object ) (runtime.Object , error ) {
9931039 ns := action .GetNamespace ()
9941040 gvr := action .GetResource ()
9951041
9961042 obj , err := tracker .Get (gvr , ns , action .GetName ())
9971043 if err != nil {
1044+ if action .GetPatchType () == types .ApplyPatchType {
1045+ return & unstructured.Unstructured {}, nil
1046+ }
9981047 return nil , err
9991048 }
10001049
@@ -1039,10 +1088,20 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
10391088 if err = json .Unmarshal (mergedByte , obj ); err != nil {
10401089 return nil , err
10411090 }
1042- case types .ApplyPatchType :
1043- 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" )
10441091 case types .ApplyCBORPatchType :
10451092 return nil , errors .New ("apply CBOR patches are not supported in the fake client" )
1093+ case types .ApplyPatchType :
1094+ // There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
1095+ // We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
1096+ defer func () {
1097+ if err := tracker .Add (obj ); err != nil {
1098+ panic (err )
1099+ }
1100+ }()
1101+ if err := tracker .Apply (gvr , newObj , ns , action .PatchOptions ); err != nil {
1102+ return nil , err
1103+ }
1104+ return tracker .Get (gvr , ns , action .GetName ())
10461105 default :
10471106 return nil , fmt .Errorf ("%s PatchType is not supported" , action .GetPatchType ())
10481107 }
0 commit comments