Skip to content

Commit 7aee523

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

File tree

3 files changed

+154
-27
lines changed

3 files changed

+154
-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: 70 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,91 @@ 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 seems to use the tuple (fieldManager, operation) to differentiate
208+
// fieldManagers. This is also observable with a real kubernetes apiserver.
209+
return obj, true, nil
185210
}
186-
return nil, err
211+
return obj, false, err
187212
}
188213

189214
if t.withStatusSubresource.Has(gvk) {
190215
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
191216
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)
217+
return nil, false, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
193218
}
194219
passedRV := accessor.GetResourceVersion()
195220
if err := copyFrom(oldObject, obj); err != nil {
196-
return nil, fmt.Errorf("failed to restore non-status fields: %w", err)
221+
return nil, false, fmt.Errorf("failed to restore non-status fields: %w", err)
197222
}
198223
accessor.SetResourceVersion(passedRV)
199224
} else { // copy status from original object
200225
if err := copyStatusFrom(oldObject, obj); err != nil {
201-
return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
226+
return nil, false, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
202227
}
203228
}
204229
} else if isStatus {
205-
return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
230+
return nil, false, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
206231
}
207232

208233
oldAccessor, err := meta.Accessor(oldObject)
209234
if err != nil {
210-
return nil, err
235+
return nil, false, err
211236
}
212237

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

232257
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
233-
return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
258+
return nil, false, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
234259
}
235260
if oldAccessor.GetResourceVersion() == "" {
236261
oldAccessor.SetResourceVersion("0")
237262
}
238263
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
239264
if err != nil {
240-
return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
265+
return nil, false, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
241266
}
242267
intResourceVersion++
243268
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
244269

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

249274
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
250-
return nil, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
275+
return nil, false, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
251276
}
252-
return convertFromUnstructuredIfNecessary(t.scheme, obj)
277+
278+
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
279+
return obj, false, err
253280
}
254281
func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error {
282+
patchOptions, err := getSingleOrZeroOptions(opts)
283+
if err != nil {
284+
return err
285+
}
286+
gvk, err := apiutil.GVKForObject(applyConfiguration, t.scheme)
287+
if err != nil {
288+
return err
289+
}
290+
applyConfiguration, _, err = t.updateObject(gvr, gvk, applyConfiguration, ns, false, false, true, patchOptions.DryRun)
291+
if err != nil {
292+
return err
293+
}
294+
295+
if applyConfiguration == nil { // Object was deleted in updateObject
296+
return nil
297+
}
298+
255299
return t.upstream.Apply(gvr, applyConfiguration, ns, opts...)
256300
}
257301
func (t versionedTracker) Delete(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.DeleteOptions) error {

0 commit comments

Comments
 (0)