Skip to content

Commit d495b4e

Browse files
🌱 Foreground deletion for MachineDeployments and MachineSets (#11174)
* foreground deletion for machine deployments * review fixes * fix unit tests * fix e2e finalizer assertions * machinedeployment: rename helper function * review fixes * add unit tests for reconcileDelete functions * review fixes * review fix * test: implement e2e test for cluster deletion * review fixes * review fixes * Review Fixes * lint fixes * fix e2e test * lint fix * Review Fixes --------- Co-authored-by: Dhairya-Arora01 <dhairya.arora@syself.com>
1 parent 404084f commit d495b4e

File tree

10 files changed

+866
-56
lines changed

10 files changed

+866
-56
lines changed

api/v1beta1/machinedeployment_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const (
2525
// MachineDeploymentTopologyFinalizer is the finalizer used by the topology MachineDeployment controller to
2626
// clean up referenced template resources if necessary when a MachineDeployment is being deleted.
2727
MachineDeploymentTopologyFinalizer = "machinedeployment.topology.cluster.x-k8s.io"
28+
29+
// MachineDeploymentFinalizer is the finalizer used by the MachineDeployment controller to
30+
// ensure ordered cleanup of corresponding MachineSets when a MachineDeployment is being deleted.
31+
MachineDeploymentFinalizer = "cluster.x-k8s.io/machinedeployment"
2832
)
2933

3034
// MachineDeploymentStrategyType defines the type of MachineDeployment rollout strategies.

api/v1beta1/machineset_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const (
2929
// MachineSetTopologyFinalizer is the finalizer used by the topology MachineDeployment controller to
3030
// clean up referenced template resources if necessary when a MachineSet is being deleted.
3131
MachineSetTopologyFinalizer = "machineset.topology.cluster.x-k8s.io"
32+
33+
// MachineSetFinalizer is the finalizer used by the MachineSet controller to
34+
// ensure ordered cleanup of corresponding Machines when a Machineset is being deleted.
35+
MachineSetFinalizer = "cluster.x-k8s.io/machineset"
3236
)
3337

3438
// ANCHOR: MachineSetSpec

internal/controllers/machinedeployment/machinedeployment_controller.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"sigs.k8s.io/controller-runtime/pkg/builder"
3434
"sigs.k8s.io/controller-runtime/pkg/client"
3535
"sigs.k8s.io/controller-runtime/pkg/controller"
36+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3637
"sigs.k8s.io/controller-runtime/pkg/handler"
3738

3839
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -42,6 +43,7 @@ import (
4243
"sigs.k8s.io/cluster-api/util/annotations"
4344
"sigs.k8s.io/cluster-api/util/conditions"
4445
utilconversion "sigs.k8s.io/cluster-api/util/conversion"
46+
clog "sigs.k8s.io/cluster-api/util/log"
4547
"sigs.k8s.io/cluster-api/util/patch"
4648
"sigs.k8s.io/cluster-api/util/predicates"
4749
)
@@ -158,9 +160,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
158160
}
159161
}()
160162

161-
// Ignore deleted MachineDeployments, this can happen when foregroundDeletion
162-
// is enabled
163+
// Handle deletion reconciliation loop.
163164
if !deployment.DeletionTimestamp.IsZero() {
165+
return ctrl.Result{}, r.reconcileDelete(ctx, deployment)
166+
}
167+
168+
// Add finalizer first if not set to avoid the race condition between init and delete.
169+
// Note: Finalizers in general can only be added when the deletionTimestamp is not set.
170+
if !controllerutil.ContainsFinalizer(deployment, clusterv1.MachineDeploymentFinalizer) {
171+
controllerutil.AddFinalizer(deployment, clusterv1.MachineDeploymentFinalizer)
164172
return ctrl.Result{}, nil
165173
}
166174

@@ -225,7 +233,7 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster,
225233
}
226234
}
227235

228-
msList, err := r.getMachineSetsForDeployment(ctx, md)
236+
msList, err := r.getAndAdoptMachineSetsForDeployment(ctx, md)
229237
if err != nil {
230238
return err
231239
}
@@ -286,8 +294,36 @@ func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster,
286294
return errors.Errorf("unexpected deployment strategy type: %s", md.Spec.Strategy.Type)
287295
}
288296

289-
// getMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment.
290-
func (r *Reconciler) getMachineSetsForDeployment(ctx context.Context, md *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) {
297+
func (r *Reconciler) reconcileDelete(ctx context.Context, md *clusterv1.MachineDeployment) error {
298+
log := ctrl.LoggerFrom(ctx)
299+
msList, err := r.getAndAdoptMachineSetsForDeployment(ctx, md)
300+
if err != nil {
301+
return err
302+
}
303+
304+
// If all the descendant machinesets are deleted, then remove the machinedeployment's finalizer.
305+
if len(msList) == 0 {
306+
controllerutil.RemoveFinalizer(md, clusterv1.MachineDeploymentFinalizer)
307+
return nil
308+
}
309+
310+
log.Info("Waiting for MachineSets to be deleted", "MachineSets", clog.ObjNamesString(msList))
311+
312+
// else delete owned machinesets.
313+
for _, ms := range msList {
314+
if ms.DeletionTimestamp.IsZero() {
315+
log.Info("Deleting MachineSet", "MachineSet", klog.KObj(ms))
316+
if err := r.Client.Delete(ctx, ms); err != nil && !apierrors.IsNotFound(err) {
317+
return errors.Wrapf(err, "failed to delete MachineSet %s", klog.KObj(ms))
318+
}
319+
}
320+
}
321+
322+
return nil
323+
}
324+
325+
// getAndAdoptMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment.
326+
func (r *Reconciler) getAndAdoptMachineSetsForDeployment(ctx context.Context, md *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) {
291327
log := ctrl.LoggerFrom(ctx)
292328

293329
// List all MachineSets to find those we own but that no longer match our selector.
@@ -299,7 +335,8 @@ func (r *Reconciler) getMachineSetsForDeployment(ctx context.Context, md *cluste
299335
filtered := make([]*clusterv1.MachineSet, 0, len(machineSets.Items))
300336
for idx := range machineSets.Items {
301337
ms := &machineSets.Items[idx]
302-
log.WithValues("MachineSet", klog.KObj(ms))
338+
log := log.WithValues("MachineSet", klog.KObj(ms))
339+
ctx := ctrl.LoggerInto(ctx, log)
303340
selector, err := metav1.LabelSelectorAsSelector(&md.Spec.Selector)
304341
if err != nil {
305342
log.Error(err, "Skipping MachineSet, failed to get label selector from spec selector")

internal/controllers/machinedeployment/machinedeployment_controller_test.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3636
"sigs.k8s.io/cluster-api/controllers/external"
37+
"sigs.k8s.io/cluster-api/internal/test/builder"
3738
"sigs.k8s.io/cluster-api/internal/util/ssa"
3839
"sigs.k8s.io/cluster-api/util"
3940
"sigs.k8s.io/cluster-api/util/conditions"
@@ -962,7 +963,7 @@ func TestGetMachineSetsForDeployment(t *testing.T) {
962963
recorder: record.NewFakeRecorder(32),
963964
}
964965

965-
got, err := r.getMachineSetsForDeployment(ctx, &tc.machineDeployment)
966+
got, err := r.getAndAdoptMachineSetsForDeployment(ctx, &tc.machineDeployment)
966967
g.Expect(err).ToNot(HaveOccurred())
967968
g.Expect(got).To(HaveLen(len(tc.expected)))
968969

@@ -993,3 +994,90 @@ func updateMachineDeployment(ctx context.Context, c client.Client, md *clusterv1
993994
return patchHelper.Patch(ctx, md)
994995
})
995996
}
997+
998+
func TestReconciler_reconcileDelete(t *testing.T) {
999+
labels := map[string]string{
1000+
"some": "labelselector",
1001+
}
1002+
md := builder.MachineDeployment("default", "md0").WithClusterName("test").Build()
1003+
md.Finalizers = []string{
1004+
clusterv1.MachineDeploymentFinalizer,
1005+
}
1006+
md.DeletionTimestamp = ptr.To(metav1.Now())
1007+
md.Spec.Selector = metav1.LabelSelector{
1008+
MatchLabels: labels,
1009+
}
1010+
mdWithoutFinalizer := md.DeepCopy()
1011+
mdWithoutFinalizer.Finalizers = []string{}
1012+
tests := []struct {
1013+
name string
1014+
machineDeployment *clusterv1.MachineDeployment
1015+
want *clusterv1.MachineDeployment
1016+
objs []client.Object
1017+
wantMachineSets []clusterv1.MachineSet
1018+
expectError bool
1019+
}{
1020+
{
1021+
name: "Should do nothing when no descendant MachineSets exist and finalizer is already gone",
1022+
machineDeployment: mdWithoutFinalizer.DeepCopy(),
1023+
want: mdWithoutFinalizer.DeepCopy(),
1024+
objs: nil,
1025+
wantMachineSets: nil,
1026+
expectError: false,
1027+
},
1028+
{
1029+
name: "Should remove finalizer when no descendant MachineSets exist",
1030+
machineDeployment: md.DeepCopy(),
1031+
want: mdWithoutFinalizer.DeepCopy(),
1032+
objs: nil,
1033+
wantMachineSets: nil,
1034+
expectError: false,
1035+
},
1036+
{
1037+
name: "Should keep finalizer when descendant MachineSets exist and trigger deletion only for descendant MachineSets",
1038+
machineDeployment: md.DeepCopy(),
1039+
want: md.DeepCopy(),
1040+
objs: []client.Object{
1041+
builder.MachineSet("default", "ms0").WithClusterName("test").WithLabels(labels).Build(),
1042+
builder.MachineSet("default", "ms1").WithClusterName("test").WithLabels(labels).Build(),
1043+
builder.MachineSet("default", "ms2-not-part-of-md").WithClusterName("test").Build(),
1044+
builder.MachineSet("default", "ms3-not-part-of-md").WithClusterName("test").Build(),
1045+
},
1046+
wantMachineSets: []clusterv1.MachineSet{
1047+
*builder.MachineSet("default", "ms2-not-part-of-md").WithClusterName("test").Build(),
1048+
*builder.MachineSet("default", "ms3-not-part-of-md").WithClusterName("test").Build(),
1049+
},
1050+
expectError: false,
1051+
},
1052+
}
1053+
for _, tt := range tests {
1054+
t.Run(tt.name, func(t *testing.T) {
1055+
g := NewWithT(t)
1056+
1057+
c := fake.NewClientBuilder().WithObjects(tt.objs...).Build()
1058+
r := &Reconciler{
1059+
Client: c,
1060+
recorder: record.NewFakeRecorder(32),
1061+
}
1062+
1063+
err := r.reconcileDelete(ctx, tt.machineDeployment)
1064+
if tt.expectError {
1065+
g.Expect(err).To(HaveOccurred())
1066+
} else {
1067+
g.Expect(err).ToNot(HaveOccurred())
1068+
}
1069+
1070+
g.Expect(tt.machineDeployment).To(BeComparableTo(tt.want))
1071+
1072+
machineSetList := &clusterv1.MachineSetList{}
1073+
g.Expect(c.List(ctx, machineSetList, client.InNamespace("default"))).ToNot(HaveOccurred())
1074+
1075+
// Remove ResourceVersion so we can actually compare.
1076+
for i := range machineSetList.Items {
1077+
machineSetList.Items[i].ResourceVersion = ""
1078+
}
1079+
1080+
g.Expect(machineSetList.Items).To(ConsistOf(tt.wantMachineSets))
1081+
})
1082+
}
1083+
}

0 commit comments

Comments
 (0)