@@ -57,13 +57,17 @@ import (
57
57
"k8s.io/apimachinery/pkg/labels"
58
58
"k8s.io/apimachinery/pkg/runtime"
59
59
"k8s.io/apimachinery/pkg/runtime/schema"
60
+ "k8s.io/apimachinery/pkg/runtime/serializer"
60
61
"k8s.io/apimachinery/pkg/types"
62
+ "k8s.io/apimachinery/pkg/util/managedfields"
61
63
utilrand "k8s.io/apimachinery/pkg/util/rand"
62
64
"k8s.io/apimachinery/pkg/util/sets"
63
65
"k8s.io/apimachinery/pkg/util/strategicpatch"
64
66
"k8s.io/apimachinery/pkg/util/validation/field"
65
67
"k8s.io/apimachinery/pkg/watch"
68
+ clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
66
69
"k8s.io/client-go/kubernetes/scheme"
70
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
67
71
"k8s.io/client-go/testing"
68
72
"k8s.io/utils/ptr"
69
73
@@ -131,6 +135,7 @@ type ClientBuilder struct {
131
135
withStatusSubresource []client.Object
132
136
objectTracker testing.ObjectTracker
133
137
interceptorFuncs * interceptor.Funcs
138
+ typeConverters []managedfields.TypeConverter
134
139
135
140
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
136
141
// The inner map maps from index name to IndexerFunc.
@@ -172,6 +177,8 @@ func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *C
172
177
}
173
178
174
179
// 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.
175
182
func (f * ClientBuilder ) WithObjectTracker (ot testing.ObjectTracker ) * ClientBuilder {
176
183
f .objectTracker = ot
177
184
return f
@@ -228,6 +235,18 @@ func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs)
228
235
return f
229
236
}
230
237
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
+
231
250
// Build builds and returns a new fake client.
232
251
func (f * ClientBuilder ) Build () client.WithWatch {
233
252
if f .scheme == nil {
@@ -248,11 +267,29 @@ func (f *ClientBuilder) Build() client.WithWatch {
248
267
withStatusSubResource .Insert (gvk )
249
268
}
250
269
270
+ if f .objectTracker != nil && len (f .typeConverters ) > 0 {
271
+ panic (errors .New ("WithObjectTracker and WithTypeConverters are incompatible" ))
272
+ }
273
+
251
274
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
+ )
255
288
}
289
+ tracker = versionedTracker {
290
+ ObjectTracker : f .objectTracker ,
291
+ scheme : f .scheme ,
292
+ withStatusSubresource : withStatusSubResource }
256
293
257
294
for _ , obj := range f .initObject {
258
295
if err := tracker .Add (obj ); err != nil {
@@ -929,6 +966,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
929
966
if err != nil {
930
967
return err
931
968
}
969
+
970
+ // otherwise the merge logic in the tracker complains
971
+ if patch .Type () == types .ApplyPatchType {
972
+ obj .SetManagedFields (nil )
973
+ }
974
+
932
975
data , err := patch .Data (obj )
933
976
if err != nil {
934
977
return err
@@ -943,7 +986,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
943
986
defer c .trackerWriteLock .Unlock ()
944
987
oldObj , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
945
988
if err != nil {
946
- return err
989
+ if patch .Type () != types .ApplyPatchType {
990
+ return err
991
+ }
992
+ oldObj = & unstructured.Unstructured {}
947
993
}
948
994
oldAccessor , err := meta .Accessor (oldObj )
949
995
if err != nil {
@@ -958,7 +1004,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
958
1004
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
959
1005
// to updating the object.
960
1006
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 )
962
1008
if err != nil {
963
1009
return err
964
1010
}
@@ -1017,12 +1063,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
1017
1063
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
1018
1064
// and easier than refactoring the k8s client-go method upstream.
1019
1065
// 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 ) {
1021
1067
ns := action .GetNamespace ()
1022
1068
gvr := action .GetResource ()
1023
1069
1024
1070
obj , err := tracker .Get (gvr , ns , action .GetName ())
1025
1071
if err != nil {
1072
+ if action .GetPatchType () == types .ApplyPatchType {
1073
+ return & unstructured.Unstructured {}, nil
1074
+ }
1026
1075
return nil , err
1027
1076
}
1028
1077
@@ -1067,10 +1116,20 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
1067
1116
if err = json .Unmarshal (mergedByte , obj ); err != nil {
1068
1117
return nil , err
1069
1118
}
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" )
1072
1119
case types .ApplyCBORPatchType :
1073
1120
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 ())
1074
1133
default :
1075
1134
return nil , fmt .Errorf ("%s PatchType is not supported" , action .GetPatchType ())
1076
1135
}
0 commit comments