@@ -56,14 +56,18 @@ import (
56
56
"k8s.io/apimachinery/pkg/labels"
57
57
"k8s.io/apimachinery/pkg/runtime"
58
58
"k8s.io/apimachinery/pkg/runtime/schema"
59
+ "k8s.io/apimachinery/pkg/runtime/serializer"
59
60
"k8s.io/apimachinery/pkg/types"
60
61
"k8s.io/apimachinery/pkg/util/json"
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 {
@@ -926,6 +963,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
926
963
if err != nil {
927
964
return err
928
965
}
966
+
967
+ // otherwise the merge logic in the tracker complains
968
+ if patch .Type () == types .ApplyPatchType {
969
+ obj .SetManagedFields (nil )
970
+ }
971
+
929
972
data , err := patch .Data (obj )
930
973
if err != nil {
931
974
return err
@@ -940,7 +983,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
940
983
defer c .trackerWriteLock .Unlock ()
941
984
oldObj , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
942
985
if err != nil {
943
- return err
986
+ if patch .Type () != types .ApplyPatchType {
987
+ return err
988
+ }
989
+ oldObj = & unstructured.Unstructured {}
944
990
}
945
991
oldAccessor , err := meta .Accessor (oldObj )
946
992
if err != nil {
@@ -955,7 +1001,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
955
1001
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
956
1002
// to updating the object.
957
1003
action := testing .NewPatchAction (gvr , accessor .GetNamespace (), accessor .GetName (), patch .Type (), data )
958
- o , err := dryPatch (action , c .tracker )
1004
+ o , err := dryPatch (action , c .tracker , obj )
959
1005
if err != nil {
960
1006
return err
961
1007
}
@@ -1014,12 +1060,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
1014
1060
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
1015
1061
// and easier than refactoring the k8s client-go method upstream.
1016
1062
// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
1017
- func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker ) (runtime.Object , error ) {
1063
+ func dryPatch (action testing.PatchActionImpl , tracker testing.ObjectTracker , newObj runtime. Object ) (runtime.Object , error ) {
1018
1064
ns := action .GetNamespace ()
1019
1065
gvr := action .GetResource ()
1020
1066
1021
1067
obj , err := tracker .Get (gvr , ns , action .GetName ())
1022
1068
if err != nil {
1069
+ if action .GetPatchType () == types .ApplyPatchType {
1070
+ return & unstructured.Unstructured {}, nil
1071
+ }
1023
1072
return nil , err
1024
1073
}
1025
1074
@@ -1064,10 +1113,20 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
1064
1113
if err = json .Unmarshal (mergedByte , obj ); err != nil {
1065
1114
return nil , err
1066
1115
}
1067
- case types .ApplyPatchType :
1068
- 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" )
1069
1116
case types .ApplyCBORPatchType :
1070
1117
return nil , errors .New ("apply CBOR patches are not supported in the fake client" )
1118
+ case types .ApplyPatchType :
1119
+ // There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
1120
+ // We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
1121
+ defer func () {
1122
+ if err := tracker .Add (obj ); err != nil {
1123
+ panic (err )
1124
+ }
1125
+ }()
1126
+ if err := tracker .Apply (gvr , newObj , ns , action .PatchOptions ); err != nil {
1127
+ return nil , err
1128
+ }
1129
+ return tracker .Get (gvr , ns , action .GetName ())
1071
1130
default :
1072
1131
return nil , fmt .Errorf ("%s PatchType is not supported" , action .GetPatchType ())
1073
1132
}
0 commit comments