Skip to content

Commit 854c5e8

Browse files
authored
⚠ Handle finalizers in the fake client (#1399)
* Handle finalizers in the fake client The fake client differs significantly from the real implementation. With the real client implementation, upon the deletion of a resource, when finalizers are set the DeletionTimestamp is filled with a non-null value. This triggers a number of reconciling loops for all controllers that then can take the opportunity to inspect the resource being deleted, perform the required cleaning actions and then update the resource by removing their finalizers as the cleaning job have been done. In the current implementation, the Delete function implements a straight delete and hence triggers a false positive or negative test when trying to run tests at the client level, without interacting with the objects. For example: ``` client.CreateObject() controller.Reconcile() Expect(Something).To(BeDone()) client.DeleteObject() controller.Reconcile() Expect(SomethingElse).To(BeDone()) ``` In the real life, SomethingElse would actually be done as the controller would still have access to the resource. In the current fake implementation, this is not the case as the controller does not have access to the resource any longer. * Handle finalizers when deleting collections
1 parent 85527df commit 854c5e8

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

pkg/client/fake/client.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob
248248
}
249249
intResourceVersion++
250250
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
251+
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
252+
return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
253+
}
251254
return t.ObjectTracker.Update(gvr, obj, ns)
252255
}
253256

@@ -389,8 +392,7 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
389392
delOptions := client.DeleteOptions{}
390393
delOptions.ApplyOptions(opts)
391394

392-
//TODO: implement propagation
393-
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
395+
return c.deleteObject(gvr, accessor)
394396
}
395397

396398
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
@@ -421,7 +423,7 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
421423
if err != nil {
422424
return err
423425
}
424-
err = c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
426+
err = c.deleteObject(gvr, accessor)
425427
if err != nil {
426428
return err
427429
}
@@ -506,6 +508,23 @@ func (c *fakeClient) Status() client.StatusWriter {
506508
return &fakeStatusWriter{client: c}
507509
}
508510

511+
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
512+
old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
513+
if err == nil {
514+
oldAccessor, err := meta.Accessor(old)
515+
if err == nil {
516+
if len(oldAccessor.GetFinalizers()) > 0 {
517+
now := metav1.Now()
518+
oldAccessor.SetDeletionTimestamp(&now)
519+
return c.tracker.Update(gvr, old, accessor.GetNamespace())
520+
}
521+
}
522+
}
523+
524+
//TODO: implement propagation
525+
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
526+
}
527+
509528
func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) {
510529
gvk, err := apiutil.GVKForObject(obj, scheme)
511530
if err != nil {

pkg/client/fake/client_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fake
1919
import (
2020
"context"
2121
"encoding/json"
22+
"fmt"
2223

2324
. "github.com/onsi/ginkgo"
2425
. "github.com/onsi/gomega"
@@ -443,6 +444,46 @@ var _ = Describe("Fake client", func() {
443444
Expect(list.Items).To(ConsistOf(*dep2))
444445
})
445446

447+
It("should handle finalizers on Update", func() {
448+
namespacedName := types.NamespacedName{
449+
Name: "test-cm",
450+
Namespace: "delete-with-finalizers",
451+
}
452+
By("Updating a new object")
453+
newObj := &corev1.ConfigMap{
454+
ObjectMeta: metav1.ObjectMeta{
455+
Name: namespacedName.Name,
456+
Namespace: namespacedName.Namespace,
457+
Finalizers: []string{"finalizers.sigs.k8s.io/test"},
458+
},
459+
Data: map[string]string{
460+
"test-key": "new-value",
461+
},
462+
}
463+
err := cl.Create(context.Background(), newObj)
464+
Expect(err).To(BeNil())
465+
466+
By("Deleting the object")
467+
err = cl.Delete(context.Background(), newObj)
468+
Expect(err).To(BeNil())
469+
470+
By("Getting the object")
471+
obj := &corev1.ConfigMap{}
472+
err = cl.Get(context.Background(), namespacedName, obj)
473+
Expect(err).To(BeNil())
474+
Expect(obj.DeletionTimestamp).NotTo(BeNil())
475+
476+
By("Removing the finalizer")
477+
obj.Finalizers = []string{}
478+
err = cl.Update(context.Background(), obj)
479+
Expect(err).To(BeNil())
480+
481+
By("Getting the object")
482+
obj = &corev1.ConfigMap{}
483+
err = cl.Get(context.Background(), namespacedName, obj)
484+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
485+
})
486+
446487
It("should be able to Delete a Collection", func() {
447488
By("Deleting a deploymentList")
448489
err := cl.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace("ns1"))
@@ -455,6 +496,41 @@ var _ = Describe("Fake client", func() {
455496
Expect(list.Items).To(BeEmpty())
456497
})
457498

499+
It("should handle finalizers deleting a collection", func() {
500+
for i := 0; i < 5; i++ {
501+
namespacedName := types.NamespacedName{
502+
Name: fmt.Sprintf("test-cm-%d", i),
503+
Namespace: "delete-collection-with-finalizers",
504+
}
505+
By("Creating a new object")
506+
newObj := &corev1.ConfigMap{
507+
ObjectMeta: metav1.ObjectMeta{
508+
Name: namespacedName.Name,
509+
Namespace: namespacedName.Namespace,
510+
Finalizers: []string{"finalizers.sigs.k8s.io/test"},
511+
},
512+
Data: map[string]string{
513+
"test-key": "new-value",
514+
},
515+
}
516+
err := cl.Create(context.Background(), newObj)
517+
Expect(err).To(BeNil())
518+
}
519+
520+
By("Deleting the object")
521+
err := cl.DeleteAllOf(context.Background(), &corev1.ConfigMap{}, client.InNamespace("delete-collection-with-finalizers"))
522+
Expect(err).To(BeNil())
523+
524+
configmaps := corev1.ConfigMapList{}
525+
err = cl.List(context.Background(), &configmaps, client.InNamespace("delete-collection-with-finalizers"))
526+
Expect(err).To(BeNil())
527+
528+
Expect(len(configmaps.Items)).To(Equal(5))
529+
for _, cm := range configmaps.Items {
530+
Expect(cm.DeletionTimestamp).NotTo(BeNil())
531+
}
532+
})
533+
458534
Context("with the DryRun option", func() {
459535
It("should not create a new object", func() {
460536
By("Creating a new configmap with DryRun")
@@ -531,6 +607,46 @@ var _ = Describe("Fake client", func() {
531607
Expect(obj.Annotations["foo"]).To(Equal("bar"))
532608
Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1000"))
533609
})
610+
611+
It("should handle finalizers on Patch", func() {
612+
namespacedName := types.NamespacedName{
613+
Name: "test-cm",
614+
Namespace: "delete-with-finalizers",
615+
}
616+
By("Updating a new object")
617+
now := metav1.Now()
618+
newObj := &corev1.ConfigMap{
619+
ObjectMeta: metav1.ObjectMeta{
620+
Name: namespacedName.Name,
621+
Namespace: namespacedName.Namespace,
622+
Finalizers: []string{"finalizers.sigs.k8s.io/test"},
623+
DeletionTimestamp: &now,
624+
},
625+
Data: map[string]string{
626+
"test-key": "new-value",
627+
},
628+
}
629+
err := cl.Create(context.Background(), newObj)
630+
Expect(err).To(BeNil())
631+
632+
By("Removing the finalizer")
633+
obj := &corev1.ConfigMap{
634+
ObjectMeta: metav1.ObjectMeta{
635+
Name: namespacedName.Name,
636+
Namespace: namespacedName.Namespace,
637+
Finalizers: []string{},
638+
DeletionTimestamp: &now,
639+
},
640+
}
641+
obj.Finalizers = []string{}
642+
err = cl.Patch(context.Background(), obj, client.MergeFrom(newObj))
643+
Expect(err).To(BeNil())
644+
645+
By("Getting the object")
646+
obj = &corev1.ConfigMap{}
647+
err = cl.Get(context.Background(), namespacedName, obj)
648+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
649+
})
534650
}
535651

536652
Context("with default scheme.Scheme", func() {

0 commit comments

Comments
 (0)