Skip to content

Commit 75f2d93

Browse files
committed
🐛 Fakeclient: Fix some SSA-related bugs
* We didn't pass on the options in Create and Patch, resulting in an incorrect fieldOwner of `unknown` being set * The `dryPatch` logic had side-effects in the case of SSA and the validation we use it for doesn't apply to SSA * We never stripped `managedFields` from objects in a List
1 parent e8c5c54 commit 75f2d93

File tree

2 files changed

+154
-36
lines changed

2 files changed

+154
-36
lines changed

pkg/client/fake/client.go

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
https://github.com/kubernetes/kubernetes/pull/120326 (v5.6.0+incompatible
4242
missing a critical fix)
4343
*/
44+
4445
jsonpatch "gopkg.in/evanphx/json-patch.v4"
4546
appsv1 "k8s.io/api/apps/v1"
4647
authenticationv1 "k8s.io/api/authentication/v1"
@@ -717,6 +718,10 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
717718
if err := ensureTypeMeta(o, gvk); err != nil {
718719
return err
719720
}
721+
722+
if !c.returnManagedFields {
723+
o.(metav1.Object).SetManagedFields(nil)
724+
}
720725
}
721726

722727
if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil {
@@ -870,7 +875,7 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
870875
c.trackerWriteLock.Lock()
871876
defer c.trackerWriteLock.Unlock()
872877

873-
if err := c.tracker.Create(gvr, obj, accessor.GetNamespace()); err != nil {
878+
if err := c.tracker.Create(gvr, obj, accessor.GetNamespace(), *createOptions.AsCreateOptions()); err != nil {
874879
// The managed fields tracker sets gvk even on errors
875880
_ = ensureTypeMeta(obj, gvk)
876881
return err
@@ -1113,11 +1118,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
11131118
return err
11141119
}
11151120

1116-
data, err := patch.Data(obj)
1117-
if err != nil {
1118-
return err
1119-
}
1120-
1121+
var isApplyCreate bool
11211122
c.trackerWriteLock.Lock()
11221123
defer c.trackerWriteLock.Unlock()
11231124
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
@@ -1126,32 +1127,59 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
11261127
return err
11271128
}
11281129
oldObj = &unstructured.Unstructured{}
1130+
isApplyCreate = true
11291131
}
11301132
oldAccessor, err := meta.Accessor(oldObj)
11311133
if err != nil {
11321134
return err
11331135
}
11341136

1135-
// Apply patch without updating object.
1136-
// To remain in accordance with the behavior of k8s api behavior,
1137-
// a patch must not allow for changes to the deletionTimestamp of an object.
1138-
// The reaction() function applies the patch to the object and calls Update(),
1139-
// whereas dryPatch() replicates this behavior but skips the call to Update().
1140-
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
1141-
// to updating the object.
1142-
action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)
1143-
o, err := dryPatch(action, c.tracker, obj)
1144-
if err != nil {
1145-
return err
1137+
// SSA deletionTimestamp updates are silently ignored
1138+
if patch.Type() == types.ApplyPatchType && !isApplyCreate {
1139+
obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp())
11461140
}
1147-
newObj, err := meta.Accessor(o)
1141+
1142+
data, err := patch.Data(obj)
11481143
if err != nil {
11491144
return err
11501145
}
11511146

1152-
// Validate that deletionTimestamp has not been changed
1153-
if !deletionTimestampEqual(newObj, oldAccessor) {
1154-
return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
1147+
action := testing.NewPatchActionWithOptions(
1148+
gvr,
1149+
accessor.GetNamespace(),
1150+
accessor.GetName(),
1151+
patch.Type(),
1152+
data,
1153+
*patchOptions.AsPatchOptions(),
1154+
)
1155+
1156+
// Apply is implemented in the tracker and calling it has side-effects
1157+
// such as bumping RV and updating managedFields timestamps, hence we
1158+
// can not dry-run it. Luckily, the only validation we use it for
1159+
// doesn't apply to SSA - Creating objects with non-nil deletionTimestamp
1160+
// through SSA is possible and updating the deletionTimestamp is valid,
1161+
// but has no effect.
1162+
if patch.Type() != types.ApplyPatchType {
1163+
// Apply patch without updating object.
1164+
// To remain in accordance with the behavior of k8s api behavior,
1165+
// a patch must not allow for changes to the deletionTimestamp of an object.
1166+
// The reaction() function applies the patch to the object and calls Update(),
1167+
// whereas dryPatch() replicates this behavior but skips the call to Update().
1168+
// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
1169+
// to updating the object.
1170+
o, err := dryPatch(action, c.tracker, obj)
1171+
if err != nil {
1172+
return err
1173+
}
1174+
newObj, err := meta.Accessor(o)
1175+
if err != nil {
1176+
return err
1177+
}
1178+
1179+
// Validate that deletionTimestamp has not been changed
1180+
if !deletionTimestampEqual(newObj, oldAccessor) {
1181+
return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
1182+
}
11551183
}
11561184

11571185
reaction := testing.ObjectReaction(c.tracker)
@@ -1206,7 +1234,7 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
12061234
// This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
12071235
// and easier than refactoring the k8s client-go method upstream.
12081236
// Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
1209-
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, newObj runtime.Object) (runtime.Object, error) {
1237+
func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) {
12101238
ns := action.GetNamespace()
12111239
gvr := action.GetResource()
12121240

@@ -1262,20 +1290,7 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, new
12621290
case types.ApplyCBORPatchType:
12631291
return nil, errors.New("apply CBOR patches are not supported in the fake client")
12641292
case types.ApplyPatchType:
1265-
// There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
1266-
// We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
1267-
defer func() {
1268-
if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured && unstructured.GetKind() == "" {
1269-
unstructured.SetGroupVersionKind(newObj.GetObjectKind().GroupVersionKind())
1270-
}
1271-
if err := tracker.Add(obj); err != nil {
1272-
panic(err)
1273-
}
1274-
}()
1275-
if err := tracker.Apply(gvr, newObj, ns, action.PatchOptions); err != nil {
1276-
return nil, err
1277-
}
1278-
return tracker.Get(gvr, ns, action.GetName())
1293+
return nil, errors.New("bug in controller-runtime: should not end up in dryPatch for SSA")
12791294
default:
12801295
return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
12811296
}

pkg/client/fake/client_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,6 +2668,27 @@ var _ = Describe("Fake client", func() {
26682668
_, exists, err := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields")
26692669
Expect(err).NotTo(HaveOccurred())
26702670
Expect(exists).To(BeTrue())
2671+
2672+
var list corev1.ConfigMapList
2673+
Expect(cl.List(ctx, &list)).NotTo(HaveOccurred())
2674+
for _, item := range list.Items {
2675+
Expect(item.ManagedFields).NotTo(BeNil())
2676+
}
2677+
})
2678+
2679+
It("clears managedFields from objects in a list", func(ctx SpecContext) {
2680+
cl := NewClientBuilder().Build()
2681+
obj := corev1applyconfigurations.
2682+
ConfigMap("foo", "default").
2683+
WithData(map[string]string{"some": "data"})
2684+
2685+
Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
2686+
2687+
var list corev1.ConfigMapList
2688+
Expect(cl.List(ctx, &list)).NotTo(HaveOccurred())
2689+
for _, item := range list.Items {
2690+
Expect(item.ManagedFields).To(BeNil())
2691+
}
26712692
})
26722693

26732694
It("supports server-side apply of a client-go resource via Apply method", func(ctx SpecContext) {
@@ -2736,6 +2757,88 @@ var _ = Describe("Fake client", func() {
27362757
Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"}))
27372758
})
27382759

2760+
It("sets managed fields through all methods", func(ctx SpecContext) {
2761+
owner := "test-owner"
2762+
cl := client.WithFieldOwner(
2763+
NewClientBuilder().WithReturnManagedFields().Build(),
2764+
owner,
2765+
)
2766+
2767+
obj := &corev1.ConfigMap{
2768+
ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "foo"},
2769+
Data: map[string]string{"method": "create"},
2770+
}
2771+
Expect(cl.Create(ctx, obj)).NotTo(HaveOccurred())
2772+
2773+
Expect(obj.ManagedFields).NotTo(BeEmpty())
2774+
for _, f := range obj.ManagedFields {
2775+
Expect(f.Manager).To(BeEquivalentTo(owner))
2776+
}
2777+
2778+
originalObj := obj.DeepCopy()
2779+
obj.Data["method"] = "patch"
2780+
Expect(cl.Patch(ctx, obj, client.MergeFrom(originalObj))).NotTo(HaveOccurred())
2781+
Expect(obj.ManagedFields).NotTo(BeEmpty())
2782+
for _, f := range obj.ManagedFields {
2783+
Expect(f.Manager).To(BeEquivalentTo(owner))
2784+
}
2785+
2786+
obj.Data["method"] = "update"
2787+
Expect(cl.Update(ctx, obj)).NotTo(HaveOccurred())
2788+
Expect(obj.ManagedFields).NotTo(BeEmpty())
2789+
for _, f := range obj.ManagedFields {
2790+
Expect(f.Manager).To(BeEquivalentTo(owner))
2791+
}
2792+
})
2793+
2794+
// GH-3267
2795+
It("Doesn't leave stale data when updating an object through SSA", func(ctx SpecContext) {
2796+
obj := corev1applyconfigurations.
2797+
ConfigMap("foo", "default").
2798+
WithData(map[string]string{"some": "data"})
2799+
2800+
cl := NewClientBuilder().Build()
2801+
Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
2802+
2803+
obj.WithData(map[string]string{"bar": "baz"})
2804+
Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
2805+
var cms corev1.ConfigMapList
2806+
Expect(cl.List(ctx, &cms)).NotTo(HaveOccurred())
2807+
Expect(len(cms.Items)).To(BeEquivalentTo(1))
2808+
})
2809+
2810+
It("allows to set deletionTimestamp on an object during SSA create", func(ctx SpecContext) {
2811+
now := metav1.Now()
2812+
obj := corev1applyconfigurations.
2813+
ConfigMap("foo", "default").
2814+
WithDeletionTimestamp(now).
2815+
WithData(map[string]string{"some": "data"})
2816+
2817+
cl := NewClientBuilder().Build()
2818+
Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
2819+
2820+
Expect(obj.DeletionTimestamp).To(BeEquivalentTo(&now))
2821+
})
2822+
2823+
It("will silently ignore a deletionTimestamp update through SSA", func(ctx SpecContext) {
2824+
Skip("the apply logic in the managedFieldObjectTracker seems to override this")
2825+
now := metav1.Now()
2826+
obj := corev1applyconfigurations.
2827+
ConfigMap("foo", "default").
2828+
WithDeletionTimestamp(now).
2829+
WithFinalizers("foo.bar").
2830+
WithData(map[string]string{"some": "data"})
2831+
2832+
cl := NewClientBuilder().Build()
2833+
Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
2834+
Expect(obj.DeletionTimestamp).To(BeEquivalentTo(&now))
2835+
2836+
later := metav1.Time{Time: now.Add(time.Second)}
2837+
obj.DeletionTimestamp = &later
2838+
Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
2839+
Expect(*obj.DeletionTimestamp).To(BeEquivalentTo(now))
2840+
})
2841+
27392842
scalableObjs := []client.Object{
27402843
&appsv1.Deployment{
27412844
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)