Skip to content

Commit 2a9ec2c

Browse files
committed
Implement finalizer / deletion utilities
1 parent 332157d commit 2a9ec2c

File tree

3 files changed

+200
-2
lines changed

3 files changed

+200
-2
lines changed

clientutils/clientutils.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"k8s.io/apimachinery/pkg/conversion"
2929
"k8s.io/apimachinery/pkg/runtime/schema"
3030
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3132
)
3233

3334
// IgnoreAlreadyExists returns nil if the given error matches apierrors.IsAlreadyExists.
@@ -545,3 +546,73 @@ func CreateOrUseWithObjectSlicePointer(ctx context.Context, c client.Client, sli
545546

546547
return res, metautils.SetObjectSlice(slicePtr, items)
547548
}
549+
550+
// DeleteIfExists deletes the given object, if it exists. It returns any non apierrors.IsNotFound error
551+
// and whether the object actually existed or not.
552+
func DeleteIfExists(ctx context.Context, c client.Client, obj client.Object, opts ...client.DeleteOption) (existed bool, err error) {
553+
if err := c.Delete(ctx, obj, opts...); err != nil {
554+
if !apierrors.IsNotFound(err) {
555+
return false, err
556+
}
557+
return false, nil
558+
}
559+
return true, nil
560+
}
561+
562+
// DeleteMultipleIfExist deletes the given objects, if they exist. It returns any non apierrors.IsNotFound error
563+
// and any object that existed before issuing the delete request.
564+
func DeleteMultipleIfExist(ctx context.Context, c client.Client, objs []client.Object, opts ...client.DeleteOption) (existed []client.Object, err error) {
565+
for i, obj := range objs {
566+
ok, err := DeleteIfExists(ctx, c, obj, opts...)
567+
if err != nil {
568+
return existed, fmt.Errorf("[object %d]: error deleting %v: %w", i, obj, err)
569+
}
570+
if ok {
571+
obj := obj
572+
existed = append(existed, obj)
573+
}
574+
}
575+
return existed, nil
576+
}
577+
578+
// PatchAddFinalizer issues a patch to add the given finalizer to the given object.
579+
// The client.Patch method will be called regardless whether the finalizer was already present or not.
580+
func PatchAddFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) error {
581+
baseObj := obj.DeepCopyObject().(client.Object)
582+
controllerutil.AddFinalizer(obj, finalizer)
583+
return c.Patch(ctx, obj, client.MergeFrom(baseObj))
584+
}
585+
586+
// PatchRemoveFinalizer issues a patch to remove the given finalizer from the given object.
587+
// The client.Patch method will be called regardless whether the finalizer was already gone or not.
588+
func PatchRemoveFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) error {
589+
baseObj := obj.DeepCopyObject().(client.Object)
590+
controllerutil.RemoveFinalizer(obj, finalizer)
591+
return c.Patch(ctx, obj, client.MergeFrom(baseObj))
592+
}
593+
594+
// PatchEnsureFinalizer checks if the given object has the given finalizer and, if not, issues a patch request
595+
// to add it. The modified result reports whether the object had to be modified.
596+
func PatchEnsureFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) (modified bool, err error) {
597+
if controllerutil.ContainsFinalizer(obj, finalizer) {
598+
return false, nil
599+
}
600+
601+
if err := PatchAddFinalizer(ctx, c, obj, finalizer); err != nil {
602+
return false, err
603+
}
604+
return true, nil
605+
}
606+
607+
// PatchEnsureNoFinalizer checks if the given object has the given finalizer and, if yes, issues a patch request
608+
// to remove it. The modified result reports whether the object had to be modified.
609+
func PatchEnsureNoFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) (modified bool, err error) {
610+
if !controllerutil.ContainsFinalizer(obj, finalizer) {
611+
return false, nil
612+
}
613+
614+
if err := PatchRemoveFinalizer(ctx, c, obj, finalizer); err != nil {
615+
return false, err
616+
}
617+
return true, nil
618+
}

clientutils/clientutils_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/onmetal/controller-utils/testdata"
2828
. "github.com/onsi/ginkgo"
2929
. "github.com/onsi/gomega"
30+
"github.com/stretchr/testify/mock"
3031
corev1 "k8s.io/api/core/v1"
3132
apierrors "k8s.io/apimachinery/pkg/api/errors"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -41,6 +42,7 @@ import (
4142
var _ = Describe("Clientutils", func() {
4243
const (
4344
objectsPath = "../testdata/bases/objects.yaml"
45+
finalizer = "my-finalizer"
4446
)
4547

4648
var (
@@ -820,4 +822,128 @@ var _ = Describe("Clientutils", func() {
820822
}))
821823
})
822824
})
825+
826+
Describe("DeleteIfExists", func() {
827+
It("should delete the existing object and return true", func() {
828+
c.EXPECT().Delete(ctx, cm)
829+
existed, err := DeleteIfExists(ctx, c, cm)
830+
Expect(err).NotTo(HaveOccurred())
831+
Expect(existed).To(BeTrue(), "object should have existed")
832+
})
833+
834+
It("should catch the not-found error when deleting and return false", func() {
835+
c.EXPECT().Delete(ctx, cm).Return(apierrors.NewNotFound(schema.GroupResource{}, ""))
836+
existed, err := DeleteIfExists(ctx, c, cm)
837+
Expect(err).NotTo(HaveOccurred())
838+
Expect(existed).To(BeFalse(), "object should not have")
839+
})
840+
841+
It("should forward any unknown errors", func() {
842+
expectedErr := fmt.Errorf("custom")
843+
c.EXPECT().Delete(ctx, cm).Return(expectedErr)
844+
_, err := DeleteIfExists(ctx, c, cm)
845+
Expect(err).To(Equal(expectedErr))
846+
})
847+
})
848+
849+
Describe("DeleteMultipleIfExist", func() {
850+
It("should delete the multiple objects and return the ones that existed", func() {
851+
gomock.InOrder(
852+
c.EXPECT().Delete(ctx, cm),
853+
c.EXPECT().Delete(ctx, secret).Return(apierrors.NewNotFound(schema.GroupResource{}, "")),
854+
)
855+
856+
existed, err := DeleteMultipleIfExist(ctx, c, []client.Object{cm, secret})
857+
Expect(err).NotTo(HaveOccurred())
858+
Expect(existed).To(Equal([]client.Object{cm}))
859+
})
860+
861+
It("should forward any unknown errors but still return the objects that existed", func() {
862+
expectedErr := fmt.Errorf("custom error")
863+
gomock.InOrder(
864+
c.EXPECT().Delete(ctx, cm),
865+
c.EXPECT().Delete(ctx, secret).Return(expectedErr),
866+
)
867+
868+
existed, err := DeleteMultipleIfExist(ctx, c, []client.Object{cm, secret})
869+
Expect(err).To(SatisfyAll(
870+
HaveOccurred(),
871+
WithTransform(func(err error) bool {
872+
return errors.Is(err, expectedErr)
873+
}, BeTrue()),
874+
))
875+
Expect(existed).To(Equal([]client.Object{cm}))
876+
})
877+
})
878+
879+
Context("Finalizer utilities", func() {
880+
var (
881+
addFinalizerPatchData []byte
882+
removeFinalizerPatchData []byte
883+
cmWithFinalizer *corev1.ConfigMap
884+
)
885+
BeforeEach(func() {
886+
cmWithFinalizer = cm.DeepCopy()
887+
cmWithFinalizer.Finalizers = []string{finalizer}
888+
889+
var err error
890+
addFinalizerPatchData, err = client.MergeFrom(cm).Data(cmWithFinalizer)
891+
Expect(err).NotTo(HaveOccurred())
892+
893+
removeFinalizerPatchData, err = client.MergeFrom(cmWithFinalizer).Data(cm)
894+
Expect(err).NotTo(HaveOccurred())
895+
})
896+
897+
Describe("PatchAddFinalizer", func() {
898+
It("should issue a patch adding the finalizer", func() {
899+
c.EXPECT().Patch(ctx, cm, mock.MatchedBy(func(p client.Patch) bool {
900+
return Expect(p.Data(cm)).To(Equal(addFinalizerPatchData))
901+
}))
902+
Expect(PatchAddFinalizer(ctx, c, cm, finalizer)).To(Succeed())
903+
})
904+
})
905+
906+
Describe("PatchRemoveFinalizer", func() {
907+
It("should issue a patch removing the finalizer", func() {
908+
c.EXPECT().Patch(ctx, cmWithFinalizer, mock.MatchedBy(func(p client.Patch) bool {
909+
return Expect(p.Data(cm)).To(Equal(removeFinalizerPatchData))
910+
}))
911+
Expect(PatchRemoveFinalizer(ctx, c, cmWithFinalizer, finalizer)).To(Succeed())
912+
})
913+
})
914+
915+
Describe("PatchEnsureFinalizer", func() {
916+
It("should add the finalizer if it is not present and report that it was modified", func() {
917+
c.EXPECT().Patch(ctx, cm, mock.MatchedBy(func(p client.Patch) bool {
918+
return Expect(p.Data(cm)).To(Equal(addFinalizerPatchData))
919+
}))
920+
modified, err := PatchEnsureFinalizer(ctx, c, cm, finalizer)
921+
Expect(err).NotTo(HaveOccurred())
922+
Expect(modified).To(BeTrue(), "cm should be modified: %v", cm)
923+
})
924+
925+
It("should not add the finalizer if it is already present and report that it was not modified", func() {
926+
modified, err := PatchEnsureFinalizer(ctx, c, cmWithFinalizer, finalizer)
927+
Expect(err).NotTo(HaveOccurred())
928+
Expect(modified).To(BeFalse(), "cm should not be modified")
929+
})
930+
})
931+
932+
Describe("PatchEnsureNoFinalizer", func() {
933+
It("should remove the finalizer if it is present and report that it was modified", func() {
934+
c.EXPECT().Patch(ctx, cmWithFinalizer, mock.MatchedBy(func(p client.Patch) bool {
935+
return Expect(p.Data(cmWithFinalizer)).To(Equal(removeFinalizerPatchData))
936+
}))
937+
modified, err := PatchEnsureNoFinalizer(ctx, c, cmWithFinalizer, finalizer)
938+
Expect(err).NotTo(HaveOccurred())
939+
Expect(modified).To(BeTrue(), "cm should be modified: %v", cm)
940+
})
941+
942+
It("should not remove the finalizer if it is already not present and report that it was not modified", func() {
943+
modified, err := PatchEnsureNoFinalizer(ctx, c, cm, finalizer)
944+
Expect(err).NotTo(HaveOccurred())
945+
Expect(modified).To(BeFalse(), "cm should not be modified")
946+
})
947+
})
948+
})
823949
})

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ require (
88
github.com/onsi/ginkgo v1.16.5
99
github.com/onsi/gomega v1.18.1
1010
github.com/spf13/pflag v1.0.5
11+
github.com/stretchr/testify v1.7.0
1112
k8s.io/api v0.23.4
1213
k8s.io/apiextensions-apiserver v0.23.4
1314
k8s.io/apimachinery v0.23.4
1415
k8s.io/client-go v0.23.4
16+
k8s.io/utils v0.0.0-20211116205334-6203023598ed
1517
sigs.k8s.io/controller-runtime v0.11.1
1618
sigs.k8s.io/kustomize/api v0.11.2
1719
sigs.k8s.io/kustomize/kyaml v0.13.3
@@ -45,7 +47,7 @@ require (
4547
github.com/nxadm/tail v1.4.8 // indirect
4648
github.com/pkg/errors v0.9.1 // indirect
4749
github.com/pmezard/go-difflib v1.0.0 // indirect
48-
github.com/stretchr/testify v1.7.0 // indirect
50+
github.com/stretchr/objx v0.2.0 // indirect
4951
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
5052
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
5153
golang.org/x/mod v0.5.0 // indirect
@@ -66,7 +68,6 @@ require (
6668
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
6769
k8s.io/klog/v2 v2.30.0 // indirect
6870
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
69-
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
7071
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
7172
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
7273
sigs.k8s.io/yaml v1.3.0 // indirect

0 commit comments

Comments
 (0)