Skip to content

Commit 3eb4009

Browse files
authored
feat: wait with cluster deletion until foreign finalizers are removed (#132)
1 parent 61cdce0 commit 3eb4009

File tree

3 files changed

+96
-0
lines changed

3 files changed

+96
-0
lines changed

api/core/v1alpha1/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343

4444
ClusterConditionShootManagement = "ShootManagement"
4545
ClusterConditionClusterConfigurations = "ClusterConfigurations"
46+
ClusterConditionForeignFinalizers = "ForeignFinalizers"
4647

4748
ProviderConfigConditionCloudProfile = "CloudProfile"
4849
ProviderConfigConditionClusterProfileManagement = "ClusterProfileManagement"

internal/controllers/cluster/controller.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sigs.k8s.io/controller-runtime/pkg/builder"
1313
"sigs.k8s.io/controller-runtime/pkg/client"
1414
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
15+
"sigs.k8s.io/controller-runtime/pkg/event"
1516
"sigs.k8s.io/controller-runtime/pkg/handler"
1617
"sigs.k8s.io/controller-runtime/pkg/predicate"
1718
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -270,6 +271,20 @@ func (r *ClusterReconciler) reconcile(ctx context.Context, req reconcile.Request
270271
// DELETE
271272
log.Info("Deleting resource")
272273

274+
// check if there are any foreign finalizers on the Cluster resource
275+
foreignFinalizers := make([]string, 0, len(c.Finalizers))
276+
for _, fin := range c.Finalizers {
277+
if fin != providerv1alpha1.ClusterFinalizer {
278+
foreignFinalizers = append(foreignFinalizers, fin)
279+
}
280+
}
281+
if len(foreignFinalizers) > 0 {
282+
log.Info("Postponing shoot deletion until foreign finalizers are removed", "foreignFinalizers", foreignFinalizers)
283+
createCon(providerv1alpha1.ClusterConditionForeignFinalizers, metav1.ConditionFalse, cconst.ReasonWaitingForDeletion, "Waiting for foreign finalizers to be removed: "+fmt.Sprintf("%v", foreignFinalizers))
284+
return rr
285+
}
286+
createCon(providerv1alpha1.ClusterConditionForeignFinalizers, metav1.ConditionTrue, "", "")
287+
273288
if exists {
274289
// shoot is still there
275290
if shoot.DeletionTimestamp == nil {
@@ -346,6 +361,7 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
346361
predicate.Or(
347362
predicate.GenerationChangedPredicate{},
348363
ctrlutils.DeletionTimestampChangedPredicate{},
364+
finalizersLostWhileInDeletionPredicate{},
349365
ctrlutils.GotAnnotationPredicate(openmcpconst.OperationAnnotation, openmcpconst.OperationAnnotationValueReconcile),
350366
ctrlutils.LostAnnotationPredicate(openmcpconst.OperationAnnotation, openmcpconst.OperationAnnotationValueIgnore),
351367
),
@@ -549,3 +565,31 @@ func (r *ClusterReconciler) getClusterConfigs(ctx context.Context, c *clustersv1
549565

550566
return clusterConfigs, nil
551567
}
568+
569+
// TODO: move to controller-utils library
570+
type finalizersLostWhileInDeletionPredicate struct{}
571+
572+
var _ predicate.Predicate = finalizersLostWhileInDeletionPredicate{}
573+
574+
// Create implements predicate.TypedPredicate.
575+
func (f finalizersLostWhileInDeletionPredicate) Create(event.TypedCreateEvent[client.Object]) bool {
576+
return false
577+
}
578+
579+
// Delete implements predicate.TypedPredicate.
580+
func (f finalizersLostWhileInDeletionPredicate) Delete(event.TypedDeleteEvent[client.Object]) bool {
581+
return false
582+
}
583+
584+
// Generic implements predicate.TypedPredicate.
585+
func (f finalizersLostWhileInDeletionPredicate) Generic(event.TypedGenericEvent[client.Object]) bool {
586+
return false
587+
}
588+
589+
// Update implements predicate.TypedPredicate.
590+
func (f finalizersLostWhileInDeletionPredicate) Update(e event.TypedUpdateEvent[client.Object]) bool {
591+
if e.ObjectOld == nil || e.ObjectNew == nil {
592+
return false
593+
}
594+
return !e.ObjectNew.GetDeletionTimestamp().IsZero() && len(e.ObjectNew.GetFinalizers()) < len(e.ObjectOld.GetFinalizers())
595+
}

internal/controllers/cluster/controller_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
. "github.com/onsi/gomega/gstruct"
77

88
apierrors "k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"k8s.io/apimachinery/pkg/runtime"
1011
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1113
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1214

1315
clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1"
@@ -240,6 +242,55 @@ var _ = Describe("Cluster Controller", func() {
240242
Expect(env.Client(platformCluster).Get(env.Ctx, client.ObjectKeyFromObject(c), c)).To(MatchError(apierrors.IsNotFound, "cluster should be deleted"))
241243
})
242244

245+
It("should not start with shoot deletion if there are foreign finalizers on the Cluster", func() {
246+
env := defaultTestSetup("..", "cluster", "testdata", "test-05")
247+
248+
c := &clustersv1alpha1.Cluster{}
249+
c.SetName("advanced")
250+
c.SetNamespace("clusters")
251+
env.ShouldReconcile(cRec, testutils.RequestFromObject(c))
252+
Expect(env.Client(platformCluster).Get(env.Ctx, client.ObjectKeyFromObject(c), c)).To(Succeed())
253+
c.Finalizers = append(c.Finalizers, "example.com/finalizer")
254+
Expect(env.Client(platformCluster).Update(env.Ctx, c)).To(Succeed())
255+
256+
// verify shoot existence
257+
Expect(c.Status.ProviderStatus).ToNot(BeNil())
258+
cs := &providerv1alpha1.ClusterStatus{}
259+
Expect(c.Status.GetProviderStatus(cs)).To(Succeed())
260+
Expect(cs.Shoot).ToNot(BeNil())
261+
shoot := &gardenv1beta1.Shoot{}
262+
shoot.SetName(cs.Shoot.Name)
263+
shoot.SetNamespace(cs.Shoot.Namespace)
264+
Expect(env.Client(gardenCluster).Get(env.Ctx, client.ObjectKeyFromObject(shoot), shoot)).To(Succeed())
265+
266+
// delete the cluster
267+
Expect(env.Client(platformCluster).Delete(env.Ctx, c)).To(Succeed())
268+
env.ShouldReconcile(cRec, testutils.RequestFromObject(c))
269+
Expect(env.Client(platformCluster).Get(env.Ctx, client.ObjectKeyFromObject(c), c)).To(Succeed())
270+
271+
// shoot should not be deleted because of the unknown finalizer
272+
Expect(env.Client(gardenCluster).Get(env.Ctx, client.ObjectKeyFromObject(shoot), shoot)).To(Succeed())
273+
Expect(shoot.DeletionTimestamp.IsZero()).To(BeTrue())
274+
275+
// there should be a condition indicating that there are foreign finalizers
276+
Expect(c.Status.Conditions).To(ContainElement(MatchFields(IgnoreExtras, Fields{
277+
"Type": Equal(providerv1alpha1.ClusterConditionForeignFinalizers),
278+
"Status": Equal(metav1.ConditionFalse),
279+
"Message": ContainSubstring("example.com/finalizer"),
280+
})))
281+
282+
// remove the foreign finalizer
283+
Expect(controllerutil.RemoveFinalizer(c, "example.com/finalizer")).To(BeTrue())
284+
Expect(env.Client(platformCluster).Update(env.Ctx, c)).To(Succeed())
285+
env.ShouldReconcile(cRec, testutils.RequestFromObject(c))
286+
287+
// verify shoot deletion
288+
Expect(env.Client(gardenCluster).Get(env.Ctx, client.ObjectKeyFromObject(shoot), shoot)).To(MatchError(apierrors.IsNotFound, "shoot should be deleted"))
289+
// shoot is gone, reconcile again to remove cluster finalizer
290+
env.ShouldReconcile(cRec, testutils.RequestFromObject(c))
291+
Expect(env.Client(platformCluster).Get(env.Ctx, client.ObjectKeyFromObject(c), c)).To(MatchError(apierrors.IsNotFound, "cluster should be deleted"))
292+
})
293+
243294
It("should handle cluster configs correctly", func() {
244295
env := defaultTestSetup("..", "cluster", "testdata", "test-04")
245296

0 commit comments

Comments
 (0)