Skip to content

Commit 8e536d1

Browse files
committed
Fakeclient Apply Update: strip status and other issues
1 parent 9cb8382 commit 8e536d1

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
@@ -137,11 +137,17 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
137137
if err != nil {
138138
return err
139139
}
140-
obj, err = t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun)
140+
obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, deleting, allowsCreateOnUpdate(gvk), opts.DryRun)
141141
if err != nil {
142142
return err
143143
}
144-
if obj == nil {
144+
145+
if needsCreate {
146+
opts := metav1.CreateOptions{DryRun: opts.DryRun, FieldManager: opts.FieldManager}
147+
return t.Create(gvr, obj, ns, opts)
148+
}
149+
150+
if obj == nil { // Object was deleted in updateObject
145151
return nil
146152
}
147153

@@ -158,72 +164,94 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj
158164
return err
159165
}
160166

167+
gvk, err := apiutil.GVKForObject(obj, t.scheme)
168+
if err != nil {
169+
return err
170+
}
171+
161172
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
162173
// that reaction, we use the callstack to figure out if this originated from the status client.
163174
isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch"))
164175

165-
obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun)
176+
obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, false, allowsCreateOnUpdate(gvk), patchOptions.DryRun)
166177
if err != nil {
167178
return err
168179
}
169-
if obj == nil {
180+
if needsCreate {
181+
opts := metav1.CreateOptions{DryRun: patchOptions.DryRun, FieldManager: patchOptions.FieldManager}
182+
return t.Create(gvr, obj, ns, opts)
183+
}
184+
185+
if obj == nil { // Object was deleted in updateObject
170186
return nil
171187
}
172188

173189
return t.upstream.Patch(gvr, obj, ns, patchOptions)
174190
}
175191

176-
func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, dryRun []string) (runtime.Object, error) {
192+
// updateObject performs a number of validations and changes before the
193+
// related to object updates, such as checking and updating the resourceVersion.
194+
func (t versionedTracker) updateObject(
195+
gvr schema.GroupVersionResource,
196+
gvk schema.GroupVersionKind,
197+
obj runtime.Object,
198+
ns string,
199+
isStatus bool,
200+
deleting bool,
201+
allowCreateOnUpdate bool,
202+
dryRun []string,
203+
) (result runtime.Object, needsCreate bool, _ error) {
177204
accessor, err := meta.Accessor(obj)
178205
if err != nil {
179-
return nil, fmt.Errorf("failed to get accessor for object: %w", err)
206+
return nil, false, fmt.Errorf("failed to get accessor for object: %w", err)
180207
}
181208

182209
if accessor.GetName() == "" {
183-
gvk, _ := apiutil.GVKForObject(obj, t.scheme)
184-
return nil, apierrors.NewInvalid(
210+
return nil, false, apierrors.NewInvalid(
185211
gvk.GroupKind(),
186212
accessor.GetName(),
187213
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
188214
}
189215

190-
gvk, err := apiutil.GVKForObject(obj, t.scheme)
191-
if err != nil {
192-
return nil, err
193-
}
194-
195216
oldObject, err := t.Get(gvr, ns, accessor.GetName())
196217
if err != nil {
197218
// If the resource is not found and the resource allows create on update, issue a
198219
// create instead.
199-
if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) {
200-
return nil, t.Create(gvr, obj, ns)
220+
if apierrors.IsNotFound(err) && allowCreateOnUpdate {
221+
// Pass this info to the caller rather than create, because in the SSA case it
222+
// must be created by calling Apply in the upstream tracker, not Create.
223+
// This is because SSA considers Apply and Non-Apply operations to be different
224+
// even then they use the same fieldManager. This behavior is also observable
225+
// with a real Kubernetes apiserver.
226+
//
227+
// Ref https://kubernetes.slack.com/archives/C0EG7JC6T/p1757868204458989?thread_ts=1757808656.002569&cid=C0EG7JC6T
228+
return obj, true, nil
201229
}
202-
return nil, err
230+
return obj, false, err
203231
}
204232

205233
if t.withStatusSubresource.Has(gvk) {
206234
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
207235
if err := copyStatusFrom(obj, oldObject); err != nil {
208-
return nil, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
236+
return nil, false, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
209237
}
210238
passedRV := accessor.GetResourceVersion()
211239
if err := copyFrom(oldObject, obj); err != nil {
212-
return nil, fmt.Errorf("failed to restore non-status fields: %w", err)
240+
return nil, false, fmt.Errorf("failed to restore non-status fields: %w", err)
213241
}
214242
accessor.SetResourceVersion(passedRV)
215243
} else { // copy status from original object
216244
if err := copyStatusFrom(oldObject, obj); err != nil {
217-
return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
245+
return nil, false, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
218246
}
219247
}
220248
} else if isStatus {
221-
return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
249+
return nil, false, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
222250
}
223251

224252
oldAccessor, err := meta.Accessor(oldObject)
225253
if err != nil {
226-
return nil, err
254+
return nil, false, err
227255
}
228256

229257
// If the new object does not have the resource version set and it allows unconditional update,
@@ -246,29 +274,48 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt
246274
}
247275

248276
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
249-
return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
277+
return nil, false, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
250278
}
251279
if oldAccessor.GetResourceVersion() == "" {
252280
oldAccessor.SetResourceVersion("0")
253281
}
254282
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
255283
if err != nil {
256-
return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
284+
return nil, false, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
257285
}
258286
intResourceVersion++
259287
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
260288

261289
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
262-
return nil, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
290+
return nil, false, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
263291
}
264292

265293
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
266-
return nil, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
294+
return nil, false, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
267295
}
268-
return convertFromUnstructuredIfNecessary(t.scheme, obj)
296+
297+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
298+
return obj, false, err
269299
}
270300

271301
func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error {
302+
patchOptions, err := getSingleOrZeroOptions(opts)
303+
if err != nil {
304+
return err
305+
}
306+
gvk, err := apiutil.GVKForObject(applyConfiguration, t.scheme)
307+
if err != nil {
308+
return err
309+
}
310+
applyConfiguration, _, err = t.updateObject(gvr, gvk, applyConfiguration, ns, false, false, true, patchOptions.DryRun)
311+
if err != nil {
312+
return err
313+
}
314+
315+
if applyConfiguration == nil { // Object was deleted in updateObject
316+
return nil
317+
}
318+
272319
return t.upstream.Apply(gvr, applyConfiguration, ns, opts...)
273320
}
274321

0 commit comments

Comments
 (0)