@@ -45,13 +45,17 @@ import (
45
45
"k8s.io/apimachinery/pkg/labels"
46
46
"k8s.io/apimachinery/pkg/runtime"
47
47
"k8s.io/apimachinery/pkg/runtime/schema"
48
+ "k8s.io/apimachinery/pkg/runtime/serializer"
48
49
"k8s.io/apimachinery/pkg/types"
50
+ "k8s.io/apimachinery/pkg/util/managedfields"
49
51
utilrand "k8s.io/apimachinery/pkg/util/rand"
50
52
"k8s.io/apimachinery/pkg/util/sets"
51
53
"k8s.io/apimachinery/pkg/util/strategicpatch"
52
54
"k8s.io/apimachinery/pkg/util/validation/field"
53
55
"k8s.io/apimachinery/pkg/watch"
56
+ clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
54
57
"k8s.io/client-go/kubernetes/scheme"
58
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
55
59
"k8s.io/client-go/testing"
56
60
"k8s.io/utils/ptr"
57
61
@@ -119,6 +123,7 @@ type ClientBuilder struct {
119
123
withStatusSubresource []client.Object
120
124
objectTracker testing.ObjectTracker
121
125
interceptorFuncs * interceptor.Funcs
126
+ typeConverters []managedfields.TypeConverter
122
127
123
128
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
124
129
// The inner map maps from index name to IndexerFunc.
@@ -160,6 +165,8 @@ func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *C
160
165
}
161
166
162
167
// 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.
163
170
func (f * ClientBuilder ) WithObjectTracker (ot testing.ObjectTracker ) * ClientBuilder {
164
171
f .objectTracker = ot
165
172
return f
@@ -216,6 +223,18 @@ func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs)
216
223
return f
217
224
}
218
225
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
+
219
238
// Build builds and returns a new fake client.
220
239
func (f * ClientBuilder ) Build () client.WithWatch {
221
240
if f .scheme == nil {
@@ -236,11 +255,29 @@ func (f *ClientBuilder) Build() client.WithWatch {
236
255
withStatusSubResource .Insert (gvk )
237
256
}
238
257
258
+ if f .objectTracker != nil && len (f .typeConverters ) > 0 {
259
+ panic (errors .New ("WithObjectTracker and WithTypeConverters are incompatible" ))
260
+ }
261
+
239
262
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
+ )
243
276
}
277
+ tracker = versionedTracker {
278
+ ObjectTracker : f .objectTracker ,
279
+ scheme : f .scheme ,
280
+ withStatusSubresource : withStatusSubResource }
244
281
245
282
for _ , obj := range f .initObject {
246
283
if err := tracker .Add (obj ); err != nil {
@@ -901,6 +938,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
901
938
if err != nil {
902
939
return err
903
940
}
941
+
942
+ // otherwise the merge logic in the tracker complains
943
+ if patch .Type () == types .ApplyPatchType {
944
+ obj .SetManagedFields (nil )
945
+ }
946
+
904
947
data , err := patch .Data (obj )
905
948
if err != nil {
906
949
return err
@@ -915,7 +958,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
915
958
defer c .trackerWriteLock .Unlock ()
916
959
oldObj , err := c .tracker .Get (gvr , accessor .GetNamespace (), accessor .GetName ())
917
960
if err != nil {
918
- return err
961
+ if patch .Type () != types .ApplyPatchType {
962
+ return err
963
+ }
964
+ oldObj = & unstructured.Unstructured {}
919
965
}
920
966
oldAccessor , err := meta .Accessor (oldObj )
921
967
if err != nil {
@@ -930,7 +976,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
930
976
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
931
977
// to updating the object.
932
978
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 )
934
980
if err != nil {
935
981
return err
936
982
}
@@ -989,12 +1035,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
989
1035
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
990
1036
// and easier than refactoring the k8s client-go method upstream.
991
1037
// 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 ) {
993
1039
ns := action .GetNamespace ()
994
1040
gvr := action .GetResource ()
995
1041
996
1042
obj , err := tracker .Get (gvr , ns , action .GetName ())
997
1043
if err != nil {
1044
+ if action .GetPatchType () == types .ApplyPatchType {
1045
+ return & unstructured.Unstructured {}, nil
1046
+ }
998
1047
return nil , err
999
1048
}
1000
1049
@@ -1039,10 +1088,20 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
1039
1088
if err = json .Unmarshal (mergedByte , obj ); err != nil {
1040
1089
return nil , err
1041
1090
}
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" )
1044
1091
case types .ApplyCBORPatchType :
1045
1092
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 ())
1046
1105
default :
1047
1106
return nil , fmt .Errorf ("%s PatchType is not supported" , action .GetPatchType ())
1048
1107
}
0 commit comments