Skip to content

Commit 83e7363

Browse files
committed
Ensure infrastructure is destroyed before deleting the node
If there were an edge case where we deleted an unreachable node and therefore removing any pod from etcd, but an stateful process were still running on the underlying host, we'll be immediately freeing up the pod name from the apiserver. "This would let the StatefulSet controller create a replacement Pod with that same identity; this can lead to the duplication of a still-running Pod, and if said Pod can still communicate with the other members of the StatefulSet, will violate the at most one semantics that StatefulSet is designed to guarantee." To mitigate this it'd be safer to delete the node only after the owned infraMachine is gone. This would give stronger guarantees that the underlying host is destroyed and so there's no chance of a stateful "leaked" process still running. kubernetes-sigs#2565
1 parent 88506f8 commit 83e7363

File tree

2 files changed

+184
-9
lines changed

2 files changed

+184
-9
lines changed

controllers/machine_controller.go

+18-9
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ func (r *MachineReconciler) reconcileDelete(ctx context.Context, cluster *cluste
261261
logger := r.Log.WithValues("machine", m.Name, "namespace", m.Namespace)
262262
logger = logger.WithValues("cluster", cluster.Name)
263263

264-
if err := r.isDeleteNodeAllowed(ctx, m); err != nil {
264+
err := r.isDeleteNodeAllowed(ctx, m)
265+
isDeleteNodeAllowed := err == nil
266+
if err != nil {
265267
switch err {
266268
case errNilNodeRef:
267269
logger.Error(err, "Deleting node is not allowed")
@@ -271,7 +273,9 @@ func (r *MachineReconciler) reconcileDelete(ctx context.Context, cluster *cluste
271273
logger.Error(err, "IsDeleteNodeAllowed check failed")
272274
return ctrl.Result{}, err
273275
}
274-
} else {
276+
}
277+
278+
if isDeleteNodeAllowed {
275279
// Drain node before deletion
276280
if _, exists := m.ObjectMeta.Annotations[clusterv1.ExcludeNodeDrainingAnnotation]; !exists {
277281
logger.Info("Draining node", "node", m.Status.NodeRef.Name)
@@ -281,6 +285,17 @@ func (r *MachineReconciler) reconcileDelete(ctx context.Context, cluster *cluste
281285
}
282286
r.recorder.Eventf(m, corev1.EventTypeNormal, "SuccessfulDrainNode", "success draining Machine's node %q", m.Status.NodeRef.Name)
283287
}
288+
}
289+
290+
if ok, err := r.reconcileDeleteExternal(ctx, m); !ok || err != nil {
291+
// Return early and don't remove the finalizer if we got an error or
292+
// the external reconciliation deletion isn't ready.
293+
return ctrl.Result{}, err
294+
}
295+
296+
// We only delete the node after the underlying infrastructure is gone.
297+
// https://github.com/kubernetes-sigs/cluster-api/issues/2565
298+
if isDeleteNodeAllowed {
284299
logger.Info("Deleting node", "node", m.Status.NodeRef.Name)
285300

286301
var deleteNodeErr error
@@ -296,17 +311,11 @@ func (r *MachineReconciler) reconcileDelete(ctx context.Context, cluster *cluste
296311
}
297312
}
298313

299-
if ok, err := r.reconcileDeleteExternal(ctx, m); !ok || err != nil {
300-
// Return early and don't remove the finalizer if we got an error or
301-
// the external reconciliation deletion isn't ready.
302-
return ctrl.Result{}, err
303-
}
304-
305314
controllerutil.RemoveFinalizer(m, clusterv1.MachineFinalizer)
306315
return ctrl.Result{}, nil
307316
}
308317

309-
// isDeleteNodeAllowed returns nil only if the Machine's NodeRef is not nil
318+
// isDeleteNodeAllowed returns true only if the Machine's NodeRef is not nil
310319
// and if the Machine is not the last control plane node in the cluster.
311320
func (r *MachineReconciler) isDeleteNodeAllowed(ctx context.Context, machine *clusterv1.Machine) error {
312321
// Cannot delete something that doesn't exist.

controllers/machine_controller_test.go

+166
Original file line numberDiff line numberDiff line change
@@ -784,3 +784,169 @@ func Test_clusterToActiveMachines(t *testing.T) {
784784
g.Expect(got).To(Equal(tt.want))
785785
}
786786
}
787+
788+
func TestIsDeleteNodeAllowed(t *testing.T) {
789+
testCases := []struct {
790+
name string
791+
machine *clusterv1.Machine
792+
expectedError error
793+
}{
794+
{
795+
name: "machine without nodeRef",
796+
machine: &clusterv1.Machine{
797+
ObjectMeta: metav1.ObjectMeta{
798+
Name: "created",
799+
Namespace: "default",
800+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
801+
},
802+
Spec: clusterv1.MachineSpec{
803+
ClusterName: "test-cluster",
804+
InfrastructureRef: corev1.ObjectReference{},
805+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
806+
},
807+
Status: clusterv1.MachineStatus{},
808+
},
809+
expectedError: errNilNodeRef,
810+
},
811+
{
812+
name: "no control plane members",
813+
machine: &clusterv1.Machine{
814+
ObjectMeta: metav1.ObjectMeta{
815+
Name: "created",
816+
Namespace: "default",
817+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
818+
},
819+
Spec: clusterv1.MachineSpec{
820+
ClusterName: "test-cluster",
821+
InfrastructureRef: corev1.ObjectReference{},
822+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
823+
},
824+
Status: clusterv1.MachineStatus{
825+
NodeRef: &corev1.ObjectReference{
826+
Name: "test",
827+
},
828+
},
829+
},
830+
expectedError: errNoControlPlaneNodes,
831+
},
832+
{
833+
name: "is last control plane members",
834+
machine: &clusterv1.Machine{
835+
ObjectMeta: metav1.ObjectMeta{
836+
Name: "created",
837+
Namespace: "default",
838+
Labels: map[string]string{
839+
clusterv1.ClusterLabelName: "test",
840+
clusterv1.MachineControlPlaneLabelName: "",
841+
},
842+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
843+
},
844+
Spec: clusterv1.MachineSpec{
845+
ClusterName: "test-cluster",
846+
InfrastructureRef: corev1.ObjectReference{},
847+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
848+
},
849+
Status: clusterv1.MachineStatus{
850+
NodeRef: &corev1.ObjectReference{
851+
Name: "test",
852+
},
853+
},
854+
},
855+
expectedError: errLastControlPlaneNode,
856+
},
857+
{
858+
name: "has nodeRef and control plane is healthy",
859+
machine: &clusterv1.Machine{
860+
ObjectMeta: metav1.ObjectMeta{
861+
Name: "created",
862+
Namespace: "default",
863+
Labels: map[string]string{
864+
clusterv1.ClusterLabelName: "test",
865+
},
866+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
867+
},
868+
Spec: clusterv1.MachineSpec{
869+
ClusterName: "test-cluster",
870+
InfrastructureRef: corev1.ObjectReference{},
871+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
872+
},
873+
Status: clusterv1.MachineStatus{
874+
NodeRef: &corev1.ObjectReference{
875+
Name: "test",
876+
},
877+
},
878+
},
879+
expectedError: nil,
880+
},
881+
}
882+
883+
for _, tc := range testCases {
884+
t.Run(tc.name, func(t *testing.T) {
885+
g := NewWithT(t)
886+
887+
m1 := &clusterv1.Machine{
888+
ObjectMeta: metav1.ObjectMeta{
889+
Name: "cp1",
890+
Namespace: "default",
891+
Labels: map[string]string{
892+
clusterv1.ClusterLabelName: "test",
893+
},
894+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
895+
},
896+
Spec: clusterv1.MachineSpec{
897+
ClusterName: "test-cluster",
898+
InfrastructureRef: corev1.ObjectReference{},
899+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
900+
},
901+
Status: clusterv1.MachineStatus{
902+
NodeRef: &corev1.ObjectReference{
903+
Name: "test1",
904+
},
905+
},
906+
}
907+
m2 := &clusterv1.Machine{
908+
ObjectMeta: metav1.ObjectMeta{
909+
Name: "cp2",
910+
Namespace: "default",
911+
Labels: map[string]string{
912+
clusterv1.ClusterLabelName: "test",
913+
},
914+
Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
915+
},
916+
Spec: clusterv1.MachineSpec{
917+
ClusterName: "test-cluster",
918+
InfrastructureRef: corev1.ObjectReference{},
919+
Bootstrap: clusterv1.Bootstrap{Data: pointer.StringPtr("data")},
920+
},
921+
Status: clusterv1.MachineStatus{
922+
NodeRef: &corev1.ObjectReference{
923+
Name: "test2",
924+
},
925+
},
926+
}
927+
// For isDeleteNodeAllowed to be true we assume a healthy control plane.
928+
if tc.expectedError == nil {
929+
m1.Labels[clusterv1.MachineControlPlaneLabelName] = ""
930+
m2.Labels[clusterv1.MachineControlPlaneLabelName] = ""
931+
}
932+
933+
mr := &MachineReconciler{
934+
Client: fake.NewFakeClientWithScheme(
935+
scheme.Scheme,
936+
tc.machine,
937+
m1,
938+
m2,
939+
),
940+
Log: log.Log,
941+
scheme: scheme.Scheme,
942+
}
943+
944+
err := mr.isDeleteNodeAllowed(context.TODO(), tc.machine)
945+
if tc.expectedError == nil {
946+
g.Expect(err).To(BeNil())
947+
} else {
948+
g.Expect(err).To(Equal(tc.expectedError))
949+
}
950+
})
951+
}
952+
}

0 commit comments

Comments
 (0)