Skip to content

Commit aefb71d

Browse files
authored
Merge pull request kubernetes#110721 from jsafrane/fix-force-detach
Don't force detach volume from healthy nodes
2 parents d9e7f25 + 3b94ac2 commit aefb71d

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

pkg/controller/volume/attachdetach/reconciler/reconciler.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"k8s.io/kubernetes/pkg/features"
3838
kevents "k8s.io/kubernetes/pkg/kubelet/events"
3939
"k8s.io/kubernetes/pkg/util/goroutinemap/exponentialbackoff"
40+
nodeutil "k8s.io/kubernetes/pkg/util/node"
4041
"k8s.io/kubernetes/pkg/util/taints"
4142
"k8s.io/kubernetes/pkg/volume/util"
4243
"k8s.io/kubernetes/pkg/volume/util/operationexecutor"
@@ -154,6 +155,15 @@ func (rc *reconciler) hasOutOfServiceTaint(nodeName types.NodeName) (bool, error
154155
return false, nil
155156
}
156157

158+
// nodeIsHealthy returns true if the node looks healthy.
159+
func (rc *reconciler) nodeIsHealthy(nodeName types.NodeName) (bool, error) {
160+
node, err := rc.nodeLister.Get(string(nodeName))
161+
if err != nil {
162+
return false, err
163+
}
164+
return nodeutil.IsNodeReady(node), nil
165+
}
166+
157167
func (rc *reconciler) reconcile() {
158168
// Detaches are triggered before attaches so that volumes referenced by
159169
// pods that are rescheduled to a different node are detached first.
@@ -204,14 +214,22 @@ func (rc *reconciler) reconcile() {
204214
// Check whether timeout has reached the maximum waiting time
205215
timeout := elapsedTime > rc.maxWaitForUnmountDuration
206216

217+
isHealthy, err := rc.nodeIsHealthy(attachedVolume.NodeName)
218+
if err != nil {
219+
klog.Errorf("failed to get health of node %s: %s", attachedVolume.NodeName, err.Error())
220+
}
221+
222+
// Force detach volumes from unhealthy nodes after maxWaitForUnmountDuration.
223+
forceDetach := !isHealthy && timeout
224+
207225
hasOutOfServiceTaint, err := rc.hasOutOfServiceTaint(attachedVolume.NodeName)
208226
if err != nil {
209227
klog.Errorf("failed to get taint specs for node %s: %s", attachedVolume.NodeName, err.Error())
210228
}
211229

212-
// Check whether volume is still mounted. Skip detach if it is still mounted unless timeout
230+
// Check whether volume is still mounted. Skip detach if it is still mounted unless force detach timeout
213231
// or the node has `node.kubernetes.io/out-of-service` taint.
214-
if attachedVolume.MountedByNode && !timeout && !hasOutOfServiceTaint {
232+
if attachedVolume.MountedByNode && !forceDetach && !hasOutOfServiceTaint {
215233
klog.V(5).InfoS("Cannot detach volume because it is still mounted", "volume", attachedVolume)
216234
continue
217235
}

pkg/controller/volume/attachdetach/reconciler/reconciler_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func Test_Run_Positive_OneDesiredVolumeAttachThenDetachWithMountedVolume(t *test
235235
volumeSpec := controllervolumetesting.GetTestVolumeSpec(string(volumeName), volumeName)
236236
nodeName := k8stypes.NodeName("node-name")
237237
dsw.AddNode(nodeName, false /*keepTerminatedPodVolumes*/)
238+
238239
volumeExists := dsw.VolumeExists(volumeName, nodeName)
239240
if volumeExists {
240241
t.Fatalf(
@@ -936,6 +937,102 @@ func Test_Run_OneVolumeDetachOnNoOutOfServiceTaintedNode(t *testing.T) {
936937
waitForDetachCallCount(t, 0 /* expectedDetachCallCount */, fakePlugin)
937938
}
938939

940+
// Populates desiredStateOfWorld cache with one node/volume/pod tuple.
941+
// The node starts as healthy.
942+
//
943+
// Calls Run()
944+
// Verifies there is one attach call and no detach calls.
945+
// Deletes the pod from desiredStateOfWorld cache without first marking the node/volume as unmounted.
946+
// Verifies that the volume is NOT detached after maxWaitForUnmountDuration.
947+
// Marks the node as unhealthy.
948+
// Verifies that the volume is detached after maxWaitForUnmountDuration.
949+
func Test_Run_OneVolumeDetachOnUnhealthyNode(t *testing.T) {
950+
// Arrange
951+
volumePluginMgr, fakePlugin := volumetesting.GetTestVolumePluginMgr(t)
952+
dsw := cache.NewDesiredStateOfWorld(volumePluginMgr)
953+
asw := cache.NewActualStateOfWorld(volumePluginMgr)
954+
fakeKubeClient := controllervolumetesting.CreateTestClient()
955+
fakeRecorder := &record.FakeRecorder{}
956+
fakeHandler := volumetesting.NewBlockVolumePathHandler()
957+
ad := operationexecutor.NewOperationExecutor(operationexecutor.NewOperationGenerator(
958+
fakeKubeClient,
959+
volumePluginMgr,
960+
fakeRecorder,
961+
fakeHandler))
962+
informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
963+
nsu := statusupdater.NewFakeNodeStatusUpdater(false /* returnError */)
964+
nodeLister := informerFactory.Core().V1().Nodes().Lister()
965+
reconciler := NewReconciler(
966+
reconcilerLoopPeriod, maxWaitForUnmountDuration, syncLoopPeriod, false, dsw, asw, ad,
967+
nsu, nodeLister, fakeRecorder)
968+
podName1 := "pod-uid1"
969+
volumeName1 := v1.UniqueVolumeName("volume-name1")
970+
volumeSpec1 := controllervolumetesting.GetTestVolumeSpec(string(volumeName1), volumeName1)
971+
nodeName1 := k8stypes.NodeName("worker-0")
972+
node1 := &v1.Node{
973+
ObjectMeta: metav1.ObjectMeta{Name: string(nodeName1)},
974+
Status: v1.NodeStatus{
975+
Conditions: []v1.NodeCondition{
976+
{
977+
Type: v1.NodeReady,
978+
Status: v1.ConditionTrue,
979+
},
980+
},
981+
},
982+
}
983+
informerFactory.Core().V1().Nodes().Informer().GetStore().Add(node1)
984+
dsw.AddNode(nodeName1, false /*keepTerminatedPodVolumes*/)
985+
volumeExists := dsw.VolumeExists(volumeName1, nodeName1)
986+
if volumeExists {
987+
t.Fatalf(
988+
"Volume %q/node %q should not exist, but it does.",
989+
volumeName1,
990+
nodeName1)
991+
}
992+
993+
generatedVolumeName, podErr := dsw.AddPod(types.UniquePodName(podName1), controllervolumetesting.NewPod(podName1,
994+
podName1), volumeSpec1, nodeName1)
995+
if podErr != nil {
996+
t.Fatalf("AddPod failed. Expected: <no error> Actual: <%v>", podErr)
997+
}
998+
999+
// Act
1000+
ch := make(chan struct{})
1001+
go reconciler.Run(ch)
1002+
defer close(ch)
1003+
1004+
// Assert
1005+
waitForNewAttacherCallCount(t, 1 /* expectedCallCount */, fakePlugin)
1006+
verifyNewAttacherCallCount(t, false /* expectZeroNewAttacherCallCount */, fakePlugin)
1007+
waitForAttachCallCount(t, 1 /* expectedAttachCallCount */, fakePlugin)
1008+
verifyNewDetacherCallCount(t, true /* expectZeroNewDetacherCallCount */, fakePlugin)
1009+
waitForDetachCallCount(t, 0 /* expectedDetachCallCount */, fakePlugin)
1010+
1011+
// Act
1012+
// Delete the pod and the volume will be detached even after the maxWaitForUnmountDuration expires as volume is
1013+
// not unmounted and the node is healthy.
1014+
dsw.DeletePod(types.UniquePodName(podName1), generatedVolumeName, nodeName1)
1015+
time.Sleep(maxWaitForUnmountDuration * 5)
1016+
// Assert
1017+
waitForNewDetacherCallCount(t, 0 /* expectedCallCount */, fakePlugin)
1018+
verifyNewAttacherCallCount(t, false /* expectZeroNewAttacherCallCount */, fakePlugin)
1019+
waitForAttachCallCount(t, 1 /* expectedAttachCallCount */, fakePlugin)
1020+
verifyNewDetacherCallCount(t, true /* expectZeroNewDetacherCallCount */, fakePlugin)
1021+
waitForDetachCallCount(t, 0 /* expectedDetachCallCount */, fakePlugin)
1022+
1023+
// Act
1024+
// Mark the node unhealthy
1025+
node2 := node1.DeepCopy()
1026+
node2.Status.Conditions[0].Status = v1.ConditionFalse
1027+
informerFactory.Core().V1().Nodes().Informer().GetStore().Update(node2)
1028+
// Assert -- Detach was triggered after maxWaitForUnmountDuration
1029+
waitForNewDetacherCallCount(t, 1 /* expectedCallCount */, fakePlugin)
1030+
verifyNewAttacherCallCount(t, false /* expectZeroNewAttacherCallCount */, fakePlugin)
1031+
waitForAttachCallCount(t, 1 /* expectedAttachCallCount */, fakePlugin)
1032+
verifyNewDetacherCallCount(t, false /* expectZeroNewDetacherCallCount */, fakePlugin)
1033+
waitForDetachCallCount(t, 1 /* expectedDetachCallCount */, fakePlugin)
1034+
}
1035+
9391036
func Test_ReportMultiAttachError(t *testing.T) {
9401037
type nodeWithPods struct {
9411038
name k8stypes.NodeName

0 commit comments

Comments
 (0)