Skip to content

Commit e5e79f5

Browse files
committed
Fakeclient Apply Update: strip status and other issues
1 parent 40a98a2 commit e5e79f5

File tree

3 files changed

+157
-27
lines changed

3 files changed

+157
-27
lines changed

pkg/client/fake/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,15 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
10001000
reaction := testing.ObjectReaction(c.tracker)
10011001
handled, o, err := reaction(action)
10021002
if err != nil {
1003+
// The reaction calls tracker.Get after tracker.Apply to return the object,
1004+
// but we may have deleted it in tracker.Apply if there was no finalizer
1005+
// left.
1006+
if apierrors.IsNotFound(err) &&
1007+
patch.Type() == types.ApplyPatchType &&
1008+
oldAccessor.GetDeletionTimestamp() != nil &&
1009+
len(obj.GetFinalizers()) == 0 {
1010+
return nil
1011+
}
10031012
return err
10041013
}
10051014
if !handled {

pkg/client/fake/client_test.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,19 @@ var _ = Describe("Fake client", func() {
660660
Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion))
661661
})
662662

663+
It("should reject apply with non-matching ResourceVersion", func(ctx SpecContext) {
664+
cl := NewClientBuilder().WithRuntimeObjects(cm).Build()
665+
applyCM := corev1applyconfigurations.ConfigMap(cm.Name, cm.Namespace).WithResourceVersion("0")
666+
err := cl.Apply(ctx, applyCM, client.FieldOwner("test"))
667+
Expect(apierrors.IsConflict(err)).To(BeTrue())
668+
669+
obj := &corev1.ConfigMap{}
670+
err = cl.Get(ctx, client.ObjectKeyFromObject(cm), obj)
671+
Expect(err).ToNot(HaveOccurred())
672+
Expect(obj).To(Equal(cm))
673+
Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion))
674+
})
675+
663676
It("should reject Delete with a mismatched ResourceVersion", func(ctx SpecContext) {
664677
bogusRV := "bogus"
665678
By("Deleting with a mismatched ResourceVersion Precondition")
@@ -714,6 +727,35 @@ var _ = Describe("Fake client", func() {
714727
Expect(list.Items).To(ConsistOf(*dep2))
715728
})
716729

730+
It("should handle finalizers in Apply ", func(ctx SpecContext) {
731+
cl := client.WithFieldOwner(cl, "test")
732+
733+
By("Creating the object with a finalizer")
734+
cm := corev1applyconfigurations.ConfigMap("test-cm", "delete-with-finalizers").
735+
WithFinalizers("finalizers.sigs.k8s.io/test")
736+
Expect(cl.Apply(ctx, cm)).NotTo(HaveOccurred())
737+
738+
By("Deleting the object")
739+
obj := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
740+
Name: *cm.Name,
741+
Namespace: *cm.Namespace,
742+
}}
743+
Expect(cl.Delete(ctx, obj)).NotTo(HaveOccurred())
744+
745+
By("Getting the object")
746+
Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).NotTo(HaveOccurred())
747+
Expect(obj.DeletionTimestamp).NotTo(BeNil())
748+
749+
By("Removing the finalizer through SSA")
750+
cm.ResourceVersion = nil
751+
cm.Finalizers = nil
752+
Expect(cl.Apply(ctx, cm)).NotTo(HaveOccurred())
753+
754+
By("Getting the object")
755+
err := cl.Get(ctx, client.ObjectKeyFromObject(obj), &corev1.ConfigMap{})
756+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
757+
})
758+
717759
It("should handle finalizers on Update", func(ctx SpecContext) {
718760
namespacedName := types.NamespacedName{
719761
Name: "test-cm",
@@ -1733,6 +1775,24 @@ var _ = Describe("Fake client", func() {
17331775
Expect(cmp.Diff(objOriginal, actual)).To(BeEmpty())
17341776
})
17351777

1778+
It("should not change the status of typed objects that have a status subresource on apply ", func(ctx SpecContext) {
1779+
1780+
cl := NewClientBuilder().WithStatusSubresource(&corev1.Pod{}).Build()
1781+
pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}
1782+
Expect(cl.Create(ctx, pod)).NotTo(HaveOccurred())
1783+
1784+
obj := corev1applyconfigurations.
1785+
Pod(pod.Name, "").
1786+
WithStatus(
1787+
corev1applyconfigurations.PodStatus().WithPhase("Running"),
1788+
)
1789+
Expect(cl.Apply(ctx, obj, client.FieldOwner("test"))).To(Succeed())
1790+
1791+
Expect(cl.Get(ctx, client.ObjectKeyFromObject(pod), pod)).To(Succeed())
1792+
1793+
Expect(pod.Status).To(BeComparableTo(corev1.PodStatus{}))
1794+
})
1795+
17361796
It("should Unmarshal the schemaless object with int64 to preserve ints", func(ctx SpecContext) {
17371797
schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}}
17381798
schemeBuilder.Register(&WithSchemalessSpec{})
@@ -2827,7 +2887,7 @@ var _ = Describe("Fake client", func() {
28272887
Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"}))
28282888
})
28292889

2830-
It("sets managed fields through all methods", func(ctx SpecContext) {
2890+
It("sets the fieldManager in create, patch and update", func(ctx SpecContext) {
28312891
owner := "test-owner"
28322892
cl := client.WithFieldOwner(
28332893
NewClientBuilder().WithReturnManagedFields().Build(),
@@ -2861,6 +2921,20 @@ var _ = Describe("Fake client", func() {
28612921
}
28622922
})
28632923

2924+
It("sets the fieldManager when creating through update", func(ctx SpecContext) {
2925+
owner := "test-owner"
2926+
cl := client.WithFieldOwner(
2927+
NewClientBuilder().WithReturnManagedFields().Build(),
2928+
owner,
2929+
)
2930+
2931+
obj := &corev1.Event{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
2932+
Expect(cl.Update(ctx, obj, client.FieldOwner(owner))).NotTo(HaveOccurred())
2933+
for _, f := range obj.ManagedFields {
2934+
Expect(f.Manager).To(BeEquivalentTo(owner))
2935+
}
2936+
})
2937+
28642938
// GH-3267
28652939
It("Doesn't leave stale data when updating an object through SSA", func(ctx SpecContext) {
28662940
obj := corev1applyconfigurations.

pkg/client/fake/versioned_tracker.go

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
121121
if err != nil {
122122
return err
123123
}
124-
obj, err = t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun)
124+
obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, deleting, allowsCreateOnUpdate(gvk), opts.DryRun)
125125
if err != nil {
126126
return err
127127
}
128-
if obj == nil {
128+
129+
if needsCreate {
130+
opts := metav1.CreateOptions{DryRun: opts.DryRun, FieldManager: opts.FieldManager}
131+
return t.Create(gvr, obj, ns, opts)
132+
}
133+
134+
if obj == nil { // Object was deleted in updateObject
129135
return nil
130136
}
131137

@@ -142,72 +148,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
142148
return err
143149
}
144150

151+
gvk, err := apiutil.GVKForObject(obj, t.scheme)
152+
if err != nil {
153+
return err
154+
}
155+
145156
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
146157
// that reaction, we use the callstack to figure out if this originated from the status client.
147158
isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch"))
148159

149-
obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun)
160+
obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, false, allowsCreateOnUpdate(gvk), patchOptions.DryRun)
150161
if err != nil {
151162
return err
152163
}
153-
if obj == nil {
164+
if needsCreate {
165+
opts := metav1.CreateOptions{DryRun: patchOptions.DryRun, FieldManager: patchOptions.FieldManager}
166+
return t.Create(gvr, obj, ns, opts)
167+
}
168+
169+
if obj == nil { // Object was deleted in updateObject
154170
return nil
155171
}
156172

157173
return t.upstream.Patch(gvr, obj, ns, patchOptions)
158174
}
159175

160-
func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, dryRun []string) (runtime.Object, error) {
176+
// updateObject performs a number of validations and changes before the
177+
// related to object updates, such as checking and updating the resourceVersion.
178+
func (t versionedTracker) updateObject(
179+
gvr schema.GroupVersionResource,
180+
gvk schema.GroupVersionKind,
181+
obj runtime.Object,
182+
ns string,
183+
isStatus bool,
184+
deleting bool,
185+
allowCreateOnUpdate bool,
186+
dryRun []string,
187+
) (result runtime.Object, needsCreate bool, _ error) {
161188
accessor, err := meta.Accessor(obj)
162189
if err != nil {
163-
return nil, fmt.Errorf("failed to get accessor for object: %w", err)
190+
return nil, false, fmt.Errorf("failed to get accessor for object: %w", err)
164191
}
165192

166193
if accessor.GetName() == "" {
167-
gvk, _ := apiutil.GVKForObject(obj, t.scheme)
168-
return nil, apierrors.NewInvalid(
194+
return nil, false, apierrors.NewInvalid(
169195
gvk.GroupKind(),
170196
accessor.GetName(),
171197
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
172198
}
173199

174-
gvk, err := apiutil.GVKForObject(obj, t.scheme)
175-
if err != nil {
176-
return nil, err
177-
}
178-
179200
oldObject, err := t.Get(gvr, ns, accessor.GetName())
180201
if err != nil {
181202
// If the resource is not found and the resource allows create on update, issue a
182203
// create instead.
183-
if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) {
184-
return nil, t.Create(gvr, obj, ns)
204+
if apierrors.IsNotFound(err) && allowCreateOnUpdate {
205+
// Pass this info to the caller rather than create, because in the SSA case it
206+
// must be created by calling Apply in the upstream tracker, not Create.
207+
// This is because SSA considers Apply and Non-Apply operations to be different
208+
// even then they use the same fieldManager. This behavior is also observable
209+
// with a real Kubernetes apiserver.
210+
//
211+
// Ref https://kubernetes.slack.com/archives/C0EG7JC6T/p1757868204458989?thread_ts=1757808656.002569&cid=C0EG7JC6T
212+
return obj, true, nil
185213
}
186-
return nil, err
214+
return obj, false, err
187215
}
188216

189217
if t.withStatusSubresource.Has(gvk) {
190218
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
191219
if err := copyStatusFrom(obj, oldObject); err != nil {
192-
return nil, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
220+
return nil, false, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
193221
}
194222
passedRV := accessor.GetResourceVersion()
195223
if err := copyFrom(oldObject, obj); err != nil {
196-
return nil, fmt.Errorf("failed to restore non-status fields: %w", err)
224+
return nil, false, fmt.Errorf("failed to restore non-status fields: %w", err)
197225
}
198226
accessor.SetResourceVersion(passedRV)
199227
} else { // copy status from original object
200228
if err := copyStatusFrom(oldObject, obj); err != nil {
201-
return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
229+
return nil, false, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
202230
}
203231
}
204232
} else if isStatus {
205-
return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
233+
return nil, false, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
206234
}
207235

208236
oldAccessor, err := meta.Accessor(oldObject)
209237
if err != nil {
210-
return nil, err
238+
return nil, false, err
211239
}
212240

213241
// If the new object does not have the resource version set and it allows unconditional update,
@@ -230,28 +258,47 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
230258
}
231259

232260
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
233-
return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
261+
return nil, false, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
234262
}
235263
if oldAccessor.GetResourceVersion() == "" {
236264
oldAccessor.SetResourceVersion("0")
237265
}
238266
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
239267
if err != nil {
240-
return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
268+
return nil, false, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
241269
}
242270
intResourceVersion++
243271
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
244272

245273
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
246-
return nil, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
274+
return nil, false, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
247275
}
248276

249277
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
250-
return nil, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
278+
return nil, false, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
251279
}
252-
return convertFromUnstructuredIfNecessary(t.scheme, obj)
280+
281+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
282+
return obj, false, err
253283
}
254284
func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error {
285+
patchOptions, err := getSingleOrZeroOptions(opts)
286+
if err != nil {
287+
return err
288+
}
289+
gvk, err := apiutil.GVKForObject(applyConfiguration, t.scheme)
290+
if err != nil {
291+
return err
292+
}
293+
applyConfiguration, _, err = t.updateObject(gvr, gvk, applyConfiguration, ns, false, false, true, patchOptions.DryRun)
294+
if err != nil {
295+
return err
296+
}
297+
298+
if applyConfiguration == nil { // Object was deleted in updateObject
299+
return nil
300+
}
301+
255302
return t.upstream.Apply(gvr, applyConfiguration, ns, opts...)
256303
}
257304
func (t versionedTracker) Delete(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.DeleteOptions) error {

0 commit comments

Comments
 (0)