Skip to content

Commit ac5705d

Browse files
committed
✨ Fakeclient: Add apply support
This change is a POC for adding apply patch support to the fake client. This relies on the upstream support for this which is implemented in a new [FieldManagedObjectTracker][0]. There are two major problems with this for us though: 1. It requires a [type converter][1] which gets initialized with a [parser][2] that knows how the type look like. It doesn't look like it is possible to pass multiple parsers, which means the resulting client can only deals with the set of types the parser knows about, for example what is in client-go but it will not work with CRDs 2. We have some code that wants to look at the object after it was patched to check if its valid - Since this is implemented in the tracker, it doesn't look like its possible to dry run the patch [0]: https://github.com/kubernetes/kubernetes/blob/4dc7a48ac6fb631a84e1974772bf7b8fd0bb9c59/staging/src/k8s.io/client-go/testing/fixture.go#L643 [1]: https://github.com/kubernetes/kubernetes/blob/4dc7a48ac6fb631a84e1974772bf7b8fd0bb9c59/staging/src/k8s.io/client-go/applyconfigurations/utils.go#L1739 [2]: https://github.com/kubernetes/kubernetes/blob/4dc7a48ac6fb631a84e1974772bf7b8fd0bb9c59/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go#L28
1 parent 5af6ffa commit ac5705d

File tree

2 files changed

+57
-5
lines changed

2 files changed

+57
-5
lines changed

pkg/client/fake/client.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ 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"
4950
utilrand "k8s.io/apimachinery/pkg/util/rand"
5051
"k8s.io/apimachinery/pkg/util/sets"
5152
"k8s.io/apimachinery/pkg/util/strategicpatch"
5253
"k8s.io/apimachinery/pkg/util/validation/field"
5354
"k8s.io/apimachinery/pkg/watch"
55+
clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations"
5456
"k8s.io/client-go/kubernetes/scheme"
5557
"k8s.io/client-go/testing"
5658
"k8s.io/utils/ptr"
@@ -230,7 +232,11 @@ func (f *ClientBuilder) Build() client.WithWatch {
230232
}
231233

232234
if f.objectTracker == nil {
233-
tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme, withStatusSubresource: withStatusSubResource}
235+
tracker = versionedTracker{ObjectTracker: testing.NewFieldManagedObjectTracker(
236+
f.scheme,
237+
serializer.NewCodecFactory(f.scheme).UniversalDecoder(),
238+
clientgoapplyconfigurations.NewTypeConverter(f.scheme),
239+
)}
234240
} else {
235241
tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource}
236242
}
@@ -868,6 +874,12 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
868874
if err != nil {
869875
return err
870876
}
877+
878+
// otherwise the merge logic in the tracker complains
879+
if patch.Type() == types.ApplyPatchType {
880+
obj.SetManagedFields(nil)
881+
}
882+
871883
data, err := patch.Data(obj)
872884
if err != nil {
873885
return err
@@ -880,7 +892,11 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
880892

881893
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
882894
if err != nil {
883-
return err
895+
if patch.Type() == types.ApplyPatchType {
896+
oldObj = &unstructured.Unstructured{}
897+
} else {
898+
return err
899+
}
884900
}
885901
oldAccessor, err := meta.Accessor(oldObj)
886902
if err != nil {
@@ -895,7 +911,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
895911
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
896912
// to updating the object.
897913
action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)
898-
o, err := dryPatch(action, c.tracker)
914+
o, err := dryPatch(action, c.tracker, obj)
899915
if err != nil {
900916
return err
901917
}
@@ -954,12 +970,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
954970
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
955971
// and easier than refactoring the k8s client-go method upstream.
956972
// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
957-
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) {
973+
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, newObj runtime.Object) (runtime.Object, error) {
958974
ns := action.GetNamespace()
959975
gvr := action.GetResource()
960976

961977
obj, err := tracker.Get(gvr, ns, action.GetName())
962978
if err != nil {
979+
if action.GetPatchType() == types.ApplyPatchType {
980+
return &unstructured.Unstructured{}, nil
981+
}
963982
return nil, err
964983
}
965984

@@ -1005,7 +1024,18 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
10051024
return nil, err
10061025
}
10071026
case types.ApplyPatchType:
1008-
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")
1027+
// There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
1028+
// We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
1029+
defer tracker.Add(obj)
1030+
defer func() {
1031+
if err := tracker.Add(obj); err != nil {
1032+
panic(err)
1033+
}
1034+
}()
1035+
if err := tracker.Apply(gvr, newObj, ns, action.PatchOptions); err != nil {
1036+
return nil, err
1037+
}
1038+
return tracker.Get(gvr, ns, action.GetName())
10091039
default:
10101040
return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
10111041
}

pkg/client/fake/client_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,28 @@ var _ = Describe("Fake client", func() {
21692169
Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr))
21702170
})
21712171

2172+
FIt("supports server-side apply", func() {
2173+
cl := NewClientBuilder().Build()
2174+
obj := &unstructured.Unstructured{}
2175+
obj.SetAPIVersion("v1")
2176+
obj.SetKind("ConfigMap")
2177+
obj.SetName("foo")
2178+
unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")
2179+
2180+
Expect(cl.Patch(context.Background(), obj, client.Apply)).To(Succeed())
2181+
2182+
cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
2183+
2184+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(cm), cm)).To(Succeed())
2185+
Expect(cm.Data).To(Equal(map[string]string{"some": "data"}))
2186+
2187+
unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")
2188+
Expect(cl.Patch(context.Background(), obj, client.Apply)).To(Succeed())
2189+
2190+
Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(cm), cm)).To(Succeed())
2191+
Expect(cm.Data).To(Equal(map[string]string{"other": "data"}))
2192+
})
2193+
21722194
scalableObjs := []client.Object{
21732195
&appsv1.Deployment{
21742196
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)