mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Merge pull request #125070 from torredil/kublet-vm-race
Ensure volumes are unmounted during graceful node shutdown
This commit is contained in:
commit
e2c17c09a4
@ -931,6 +931,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
|||||||
shutdownManager, shutdownAdmitHandler := nodeshutdown.NewManager(&nodeshutdown.Config{
|
shutdownManager, shutdownAdmitHandler := nodeshutdown.NewManager(&nodeshutdown.Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ProbeManager: klet.probeManager,
|
ProbeManager: klet.probeManager,
|
||||||
|
VolumeManager: klet.volumeManager,
|
||||||
Recorder: kubeDeps.Recorder,
|
Recorder: kubeDeps.Recorder,
|
||||||
NodeRef: nodeRef,
|
NodeRef: nodeRef,
|
||||||
GetPodsFunc: klet.GetActivePods,
|
GetPodsFunc: klet.GetActivePods,
|
||||||
|
@ -1128,7 +1128,7 @@ func TestUpdateNodeStatusAndVolumesInUseWithNodeLease(t *testing.T) {
|
|||||||
kubelet.setCachedMachineInfo(&cadvisorapi.MachineInfo{})
|
kubelet.setCachedMachineInfo(&cadvisorapi.MachineInfo{})
|
||||||
|
|
||||||
// override test volumeManager
|
// override test volumeManager
|
||||||
fakeVolumeManager := kubeletvolume.NewFakeVolumeManager(tc.existingVolumes)
|
fakeVolumeManager := kubeletvolume.NewFakeVolumeManager(tc.existingVolumes, 0, nil)
|
||||||
kubelet.volumeManager = fakeVolumeManager
|
kubelet.volumeManager = fakeVolumeManager
|
||||||
|
|
||||||
// Only test VolumesInUse setter
|
// Only test VolumesInUse setter
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/kubelet/eviction"
|
"k8s.io/kubernetes/pkg/kubelet/eviction"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/prober"
|
"k8s.io/kubernetes/pkg/kubelet/prober"
|
||||||
|
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
|
||||||
"k8s.io/utils/clock"
|
"k8s.io/utils/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ type Manager interface {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Logger klog.Logger
|
Logger klog.Logger
|
||||||
ProbeManager prober.Manager
|
ProbeManager prober.Manager
|
||||||
|
VolumeManager volumemanager.VolumeManager
|
||||||
Recorder record.EventRecorder
|
Recorder record.EventRecorder
|
||||||
NodeRef *v1.ObjectReference
|
NodeRef *v1.ObjectReference
|
||||||
GetPodsFunc eviction.ActivePodsFunc
|
GetPodsFunc eviction.ActivePodsFunc
|
||||||
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||||||
package nodeshutdown
|
package nodeshutdown
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -41,6 +42,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/nodeshutdown/systemd"
|
"k8s.io/kubernetes/pkg/kubelet/nodeshutdown/systemd"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/prober"
|
"k8s.io/kubernetes/pkg/kubelet/prober"
|
||||||
|
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
|
||||||
"k8s.io/utils/clock"
|
"k8s.io/utils/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,6 +75,8 @@ type managerImpl struct {
|
|||||||
nodeRef *v1.ObjectReference
|
nodeRef *v1.ObjectReference
|
||||||
probeManager prober.Manager
|
probeManager prober.Manager
|
||||||
|
|
||||||
|
volumeManager volumemanager.VolumeManager
|
||||||
|
|
||||||
shutdownGracePeriodByPodPriority []kubeletconfig.ShutdownGracePeriodByPodPriority
|
shutdownGracePeriodByPodPriority []kubeletconfig.ShutdownGracePeriodByPodPriority
|
||||||
|
|
||||||
getPods eviction.ActivePodsFunc
|
getPods eviction.ActivePodsFunc
|
||||||
@ -123,6 +127,7 @@ func NewManager(conf *Config) (Manager, lifecycle.PodAdmitHandler) {
|
|||||||
logger: conf.Logger,
|
logger: conf.Logger,
|
||||||
probeManager: conf.ProbeManager,
|
probeManager: conf.ProbeManager,
|
||||||
recorder: conf.Recorder,
|
recorder: conf.Recorder,
|
||||||
|
volumeManager: conf.VolumeManager,
|
||||||
nodeRef: conf.NodeRef,
|
nodeRef: conf.NodeRef,
|
||||||
getPods: conf.GetPodsFunc,
|
getPods: conf.GetPodsFunc,
|
||||||
killPodFunc: conf.KillPodFunc,
|
killPodFunc: conf.KillPodFunc,
|
||||||
@ -395,19 +400,44 @@ func (m *managerImpl) processShutdownEvent() error {
|
|||||||
}(pod, group)
|
}(pod, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This duration determines how long the shutdown manager will wait for the pods in this group
|
||||||
|
// to terminate before proceeding to the next group.
|
||||||
|
var groupTerminationWaitDuration = time.Duration(group.ShutdownGracePeriodSeconds) * time.Second
|
||||||
var (
|
var (
|
||||||
doneCh = make(chan struct{})
|
doneCh = make(chan struct{})
|
||||||
timer = m.clock.NewTimer(time.Duration(group.ShutdownGracePeriodSeconds) * time.Second)
|
timer = m.clock.NewTimer(groupTerminationWaitDuration)
|
||||||
|
ctx, ctxCancel = context.WithTimeout(context.Background(), groupTerminationWaitDuration)
|
||||||
)
|
)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(doneCh)
|
defer close(doneCh)
|
||||||
|
defer ctxCancel()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
// The signal to kill a Pod was sent successfully to all the pods,
|
||||||
|
// let's wait until all the volumes are unmounted from all the pods before
|
||||||
|
// continuing to the next group. This is done so that the CSI Driver (assuming
|
||||||
|
// that it's part of the highest group) has a chance to perform unmounts.
|
||||||
|
if err := m.volumeManager.WaitForAllPodsUnmount(ctx, group.Pods); err != nil {
|
||||||
|
var podIdentifiers []string
|
||||||
|
for _, pod := range group.Pods {
|
||||||
|
podIdentifiers = append(podIdentifiers, fmt.Sprintf("%s/%s", pod.Namespace, pod.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting for volume teardown is done on a best basis effort,
|
||||||
|
// report an error and continue.
|
||||||
|
//
|
||||||
|
// Depending on the user provided kubelet configuration value
|
||||||
|
// either the `timer` will tick and we'll continue to shutdown the next group, or,
|
||||||
|
// WaitForAllPodsUnmount will timeout, therefore this goroutine
|
||||||
|
// will close doneCh and we'll continue to shutdown the next group.
|
||||||
|
m.logger.Error(err, "Failed while waiting for all the volumes belonging to Pods in this group to unmount", "pods", podIdentifiers)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-doneCh:
|
case <-doneCh:
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
case <-timer.C():
|
case <-timer.C():
|
||||||
|
ctxCancel()
|
||||||
m.logger.V(1).Info("Shutdown manager pod killing time out", "gracePeriod", group.ShutdownGracePeriodSeconds, "priority", group.Priority)
|
m.logger.V(1).Info("Shutdown manager pod killing time out", "gracePeriod", group.ShutdownGracePeriodSeconds, "priority", group.Priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@ -45,6 +46,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/kubelet/nodeshutdown/systemd"
|
"k8s.io/kubernetes/pkg/kubelet/nodeshutdown/systemd"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/prober"
|
"k8s.io/kubernetes/pkg/kubelet/prober"
|
||||||
probetest "k8s.io/kubernetes/pkg/kubelet/prober/testing"
|
probetest "k8s.io/kubernetes/pkg/kubelet/prober/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
|
||||||
"k8s.io/utils/clock"
|
"k8s.io/utils/clock"
|
||||||
testingclock "k8s.io/utils/clock/testing"
|
testingclock "k8s.io/utils/clock/testing"
|
||||||
)
|
)
|
||||||
@ -348,10 +350,12 @@ func TestManager(t *testing.T) {
|
|||||||
|
|
||||||
proberManager := probetest.FakeManager{}
|
proberManager := probetest.FakeManager{}
|
||||||
fakeRecorder := &record.FakeRecorder{}
|
fakeRecorder := &record.FakeRecorder{}
|
||||||
|
fakeVolumeManager := volumemanager.NewFakeVolumeManager([]v1.UniqueVolumeName{}, 0, nil)
|
||||||
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
||||||
manager, _ := NewManager(&Config{
|
manager, _ := NewManager(&Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ProbeManager: proberManager,
|
ProbeManager: proberManager,
|
||||||
|
VolumeManager: fakeVolumeManager,
|
||||||
Recorder: fakeRecorder,
|
Recorder: fakeRecorder,
|
||||||
NodeRef: nodeRef,
|
NodeRef: nodeRef,
|
||||||
GetPodsFunc: activePodsFunc,
|
GetPodsFunc: activePodsFunc,
|
||||||
@ -452,11 +456,13 @@ func TestFeatureEnabled(t *testing.T) {
|
|||||||
|
|
||||||
proberManager := probetest.FakeManager{}
|
proberManager := probetest.FakeManager{}
|
||||||
fakeRecorder := &record.FakeRecorder{}
|
fakeRecorder := &record.FakeRecorder{}
|
||||||
|
fakeVolumeManager := volumemanager.NewFakeVolumeManager([]v1.UniqueVolumeName{}, 0, nil)
|
||||||
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
||||||
|
|
||||||
manager, _ := NewManager(&Config{
|
manager, _ := NewManager(&Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ProbeManager: proberManager,
|
ProbeManager: proberManager,
|
||||||
|
VolumeManager: fakeVolumeManager,
|
||||||
Recorder: fakeRecorder,
|
Recorder: fakeRecorder,
|
||||||
NodeRef: nodeRef,
|
NodeRef: nodeRef,
|
||||||
GetPodsFunc: activePodsFunc,
|
GetPodsFunc: activePodsFunc,
|
||||||
@ -509,10 +515,12 @@ func TestRestart(t *testing.T) {
|
|||||||
|
|
||||||
proberManager := probetest.FakeManager{}
|
proberManager := probetest.FakeManager{}
|
||||||
fakeRecorder := &record.FakeRecorder{}
|
fakeRecorder := &record.FakeRecorder{}
|
||||||
|
fakeVolumeManager := volumemanager.NewFakeVolumeManager([]v1.UniqueVolumeName{}, 0, nil)
|
||||||
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
||||||
manager, _ := NewManager(&Config{
|
manager, _ := NewManager(&Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ProbeManager: proberManager,
|
ProbeManager: proberManager,
|
||||||
|
VolumeManager: fakeVolumeManager,
|
||||||
Recorder: fakeRecorder,
|
Recorder: fakeRecorder,
|
||||||
NodeRef: nodeRef,
|
NodeRef: nodeRef,
|
||||||
GetPodsFunc: activePodsFunc,
|
GetPodsFunc: activePodsFunc,
|
||||||
@ -740,6 +748,7 @@ func Test_managerImpl_processShutdownEvent(t *testing.T) {
|
|||||||
var (
|
var (
|
||||||
probeManager = probetest.FakeManager{}
|
probeManager = probetest.FakeManager{}
|
||||||
fakeRecorder = &record.FakeRecorder{}
|
fakeRecorder = &record.FakeRecorder{}
|
||||||
|
fakeVolumeManager = volumemanager.NewFakeVolumeManager([]v1.UniqueVolumeName{}, 0, nil)
|
||||||
syncNodeStatus = func() {}
|
syncNodeStatus = func() {}
|
||||||
nodeRef = &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
nodeRef = &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
||||||
fakeclock = testingclock.NewFakeClock(time.Now())
|
fakeclock = testingclock.NewFakeClock(time.Now())
|
||||||
@ -749,6 +758,7 @@ func Test_managerImpl_processShutdownEvent(t *testing.T) {
|
|||||||
recorder record.EventRecorder
|
recorder record.EventRecorder
|
||||||
nodeRef *v1.ObjectReference
|
nodeRef *v1.ObjectReference
|
||||||
probeManager prober.Manager
|
probeManager prober.Manager
|
||||||
|
volumeManager volumemanager.VolumeManager
|
||||||
shutdownGracePeriodByPodPriority []kubeletconfig.ShutdownGracePeriodByPodPriority
|
shutdownGracePeriodByPodPriority []kubeletconfig.ShutdownGracePeriodByPodPriority
|
||||||
getPods eviction.ActivePodsFunc
|
getPods eviction.ActivePodsFunc
|
||||||
killPodFunc eviction.KillPodFunc
|
killPodFunc eviction.KillPodFunc
|
||||||
@ -770,6 +780,7 @@ func Test_managerImpl_processShutdownEvent(t *testing.T) {
|
|||||||
recorder: fakeRecorder,
|
recorder: fakeRecorder,
|
||||||
nodeRef: nodeRef,
|
nodeRef: nodeRef,
|
||||||
probeManager: probeManager,
|
probeManager: probeManager,
|
||||||
|
volumeManager: fakeVolumeManager,
|
||||||
shutdownGracePeriodByPodPriority: []kubeletconfig.ShutdownGracePeriodByPodPriority{
|
shutdownGracePeriodByPodPriority: []kubeletconfig.ShutdownGracePeriodByPodPriority{
|
||||||
{
|
{
|
||||||
Priority: 1,
|
Priority: 1,
|
||||||
@ -808,6 +819,7 @@ func Test_managerImpl_processShutdownEvent(t *testing.T) {
|
|||||||
)
|
)
|
||||||
m := &managerImpl{
|
m := &managerImpl{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
volumeManager: tt.fields.volumeManager,
|
||||||
recorder: tt.fields.recorder,
|
recorder: tt.fields.recorder,
|
||||||
nodeRef: tt.fields.nodeRef,
|
nodeRef: tt.fields.nodeRef,
|
||||||
probeManager: tt.fields.probeManager,
|
probeManager: tt.fields.probeManager,
|
||||||
@ -839,3 +851,65 @@ func Test_managerImpl_processShutdownEvent(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_processShutdownEvent_VolumeUnmountTimeout(t *testing.T) {
|
||||||
|
var (
|
||||||
|
probeManager = probetest.FakeManager{}
|
||||||
|
fakeRecorder = &record.FakeRecorder{}
|
||||||
|
syncNodeStatus = func() {}
|
||||||
|
nodeRef = &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
|
||||||
|
fakeclock = testingclock.NewFakeClock(time.Now())
|
||||||
|
shutdownGracePeriodSeconds = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
fakeVolumeManager := volumemanager.NewFakeVolumeManager(
|
||||||
|
[]v1.UniqueVolumeName{},
|
||||||
|
3*time.Second, // This value is intentionally longer than the shutdownGracePeriodSeconds (2s) to test the behavior
|
||||||
|
// for volume unmount operations that take longer than the allowed grace period.
|
||||||
|
fmt.Errorf("unmount timeout"),
|
||||||
|
)
|
||||||
|
logger := ktesting.NewLogger(t, ktesting.NewConfig(ktesting.BufferLogs(true)))
|
||||||
|
m := &managerImpl{
|
||||||
|
logger: logger,
|
||||||
|
volumeManager: fakeVolumeManager,
|
||||||
|
recorder: fakeRecorder,
|
||||||
|
nodeRef: nodeRef,
|
||||||
|
probeManager: probeManager,
|
||||||
|
shutdownGracePeriodByPodPriority: []kubeletconfig.ShutdownGracePeriodByPodPriority{
|
||||||
|
{
|
||||||
|
Priority: 1,
|
||||||
|
ShutdownGracePeriodSeconds: int64(shutdownGracePeriodSeconds),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getPods: func() []*v1.Pod {
|
||||||
|
return []*v1.Pod{
|
||||||
|
makePod("test-pod", 1, nil),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
killPodFunc: func(pod *v1.Pod, isEvicted bool, gracePeriodOverride *int64, fn func(*v1.PodStatus)) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
syncNodeStatus: syncNodeStatus,
|
||||||
|
dbusCon: &fakeDbus{},
|
||||||
|
clock: fakeclock,
|
||||||
|
}
|
||||||
|
|
||||||
|
start := fakeclock.Now()
|
||||||
|
err := m.processShutdownEvent()
|
||||||
|
end := fakeclock.Now()
|
||||||
|
|
||||||
|
require.NoError(t, err, "managerImpl.processShutdownEvent() should not return an error")
|
||||||
|
|
||||||
|
// Check if processShutdownEvent completed within the expected time
|
||||||
|
actualDuration := int(end.Sub(start).Seconds())
|
||||||
|
assert.LessOrEqual(t, actualDuration, shutdownGracePeriodSeconds, "processShutdownEvent took too long")
|
||||||
|
|
||||||
|
underlier, ok := logger.GetSink().(ktesting.Underlier)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Should have had a ktesting LogSink, got %T", logger.GetSink())
|
||||||
|
}
|
||||||
|
|
||||||
|
log := underlier.GetBuffer().String()
|
||||||
|
expectedLogMessage := "Failed while waiting for all the volumes belonging to Pods in this group to unmount"
|
||||||
|
assert.Contains(t, log, expectedLogMessage, "Expected log message not found")
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
@ -31,6 +32,7 @@ import (
|
|||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
"k8s.io/apimachinery/pkg/util/runtime"
|
"k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
@ -106,6 +108,12 @@ type VolumeManager interface {
|
|||||||
// the duration defined in podAttachAndMountTimeout.
|
// the duration defined in podAttachAndMountTimeout.
|
||||||
WaitForUnmount(ctx context.Context, pod *v1.Pod) error
|
WaitForUnmount(ctx context.Context, pod *v1.Pod) error
|
||||||
|
|
||||||
|
// WaitForAllPodsUnmount is a version of WaitForUnmount that blocks and
|
||||||
|
// waits until all the volumes belonging to all the pods are unmounted.
|
||||||
|
// An error is returned if there's at least one Pod with volumes not unmounted
|
||||||
|
// within the duration defined in podAttachAndMountTimeout.
|
||||||
|
WaitForAllPodsUnmount(ctx context.Context, pods []*v1.Pod) error
|
||||||
|
|
||||||
// GetMountedVolumesForPod returns a VolumeMap containing the volumes
|
// GetMountedVolumesForPod returns a VolumeMap containing the volumes
|
||||||
// referenced by the specified pod that are successfully attached and
|
// referenced by the specified pod that are successfully attached and
|
||||||
// mounted. The key in the map is the OuterVolumeSpecName (i.e.
|
// mounted. The key in the map is the OuterVolumeSpecName (i.e.
|
||||||
@ -479,6 +487,24 @@ func (vm *volumeManager) WaitForUnmount(ctx context.Context, pod *v1.Pod) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vm *volumeManager) WaitForAllPodsUnmount(ctx context.Context, pods []*v1.Pod) error {
|
||||||
|
var (
|
||||||
|
errors []error
|
||||||
|
wg sync.WaitGroup
|
||||||
|
)
|
||||||
|
wg.Add(len(pods))
|
||||||
|
for _, pod := range pods {
|
||||||
|
go func(pod *v1.Pod) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := vm.WaitForUnmount(ctx, pod); err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}(pod)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return utilerrors.NewAggregate(errors)
|
||||||
|
}
|
||||||
|
|
||||||
func (vm *volumeManager) getVolumesNotInDSW(uniquePodName types.UniquePodName, expectedVolumes []string) []string {
|
func (vm *volumeManager) getVolumesNotInDSW(uniquePodName types.UniquePodName, expectedVolumes []string) []string {
|
||||||
volumesNotInDSW := sets.New[string](expectedVolumes...)
|
volumesNotInDSW := sets.New[string](expectedVolumes...)
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package volumemanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/config"
|
"k8s.io/kubernetes/pkg/kubelet/config"
|
||||||
@ -29,10 +30,14 @@ import (
|
|||||||
type FakeVolumeManager struct {
|
type FakeVolumeManager struct {
|
||||||
volumes map[v1.UniqueVolumeName]bool
|
volumes map[v1.UniqueVolumeName]bool
|
||||||
reportedInUse map[v1.UniqueVolumeName]bool
|
reportedInUse map[v1.UniqueVolumeName]bool
|
||||||
|
unmountDelay time.Duration
|
||||||
|
unmountError error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ VolumeManager = &FakeVolumeManager{}
|
||||||
|
|
||||||
// NewFakeVolumeManager creates a new VolumeManager test instance
|
// NewFakeVolumeManager creates a new VolumeManager test instance
|
||||||
func NewFakeVolumeManager(initialVolumes []v1.UniqueVolumeName) *FakeVolumeManager {
|
func NewFakeVolumeManager(initialVolumes []v1.UniqueVolumeName, unmountDelay time.Duration, unmountError error) *FakeVolumeManager {
|
||||||
volumes := map[v1.UniqueVolumeName]bool{}
|
volumes := map[v1.UniqueVolumeName]bool{}
|
||||||
for _, v := range initialVolumes {
|
for _, v := range initialVolumes {
|
||||||
volumes[v] = true
|
volumes[v] = true
|
||||||
@ -40,6 +45,8 @@ func NewFakeVolumeManager(initialVolumes []v1.UniqueVolumeName) *FakeVolumeManag
|
|||||||
return &FakeVolumeManager{
|
return &FakeVolumeManager{
|
||||||
volumes: volumes,
|
volumes: volumes,
|
||||||
reportedInUse: map[v1.UniqueVolumeName]bool{},
|
reportedInUse: map[v1.UniqueVolumeName]bool{},
|
||||||
|
unmountDelay: unmountDelay,
|
||||||
|
unmountError: unmountError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +64,15 @@ func (f *FakeVolumeManager) WaitForUnmount(ctx context.Context, pod *v1.Pod) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeVolumeManager) WaitForAllPodsUnmount(ctx context.Context, pods []*v1.Pod) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(f.unmountDelay):
|
||||||
|
return f.unmountError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetMountedVolumesForPod is not implemented
|
// GetMountedVolumesForPod is not implemented
|
||||||
func (f *FakeVolumeManager) GetMountedVolumesForPod(podName types.UniquePodName) container.VolumeMap {
|
func (f *FakeVolumeManager) GetMountedVolumesForPod(podName types.UniquePodName) container.VolumeMap {
|
||||||
return nil
|
return nil
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
kubetypes "k8s.io/apimachinery/pkg/types"
|
kubetypes "k8s.io/apimachinery/pkg/types"
|
||||||
@ -86,7 +87,11 @@ func TestGetMountedVolumesForPodAndGetVolumesInUse(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("can't make a temp dir: %v", err)
|
t.Fatalf("can't make a temp dir: %v", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer func() {
|
||||||
|
if err := os.RemoveAll(tmpDir); err != nil {
|
||||||
|
t.Fatalf("failed to remove temp dir: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
podManager := kubepod.NewBasicPodManager()
|
podManager := kubepod.NewBasicPodManager()
|
||||||
|
|
||||||
node, pod, pv, claim := createObjects(test.pvMode, test.podMode)
|
node, pod, pv, claim := createObjects(test.pvMode, test.podMode)
|
||||||
@ -545,3 +550,65 @@ func delayClaimBecomesBound(
|
|||||||
}
|
}
|
||||||
kubeClient.CoreV1().PersistentVolumeClaims(namespace).Update(context.TODO(), volumeClaim, metav1.UpdateOptions{})
|
kubeClient.CoreV1().PersistentVolumeClaims(namespace).Update(context.TODO(), volumeClaim, metav1.UpdateOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWaitForAllPodsUnmount(t *testing.T) {
|
||||||
|
tmpDir, err := utiltesting.MkTmpdir("volumeManagerTest")
|
||||||
|
require.NoError(t, err, "Failed to create temp directory")
|
||||||
|
defer func() {
|
||||||
|
if err := os.RemoveAll(tmpDir); err != nil {
|
||||||
|
t.Errorf("Failed to remove temp directory: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
podMode v1.PersistentVolumeMode
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful unmount",
|
||||||
|
podMode: "",
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "timeout waiting for unmount",
|
||||||
|
podMode: v1.PersistentVolumeFilesystem,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
podManager := kubepod.NewBasicPodManager()
|
||||||
|
|
||||||
|
node, pod, pv, claim := createObjects(test.podMode, test.podMode)
|
||||||
|
kubeClient := fake.NewSimpleClientset(node, pod, pv, claim)
|
||||||
|
|
||||||
|
manager := newTestVolumeManager(t, tmpDir, podManager, kubeClient, node)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
sourcesReady := config.NewSourcesReady(func(_ sets.Set[string]) bool { return true })
|
||||||
|
go manager.Run(ctx, sourcesReady)
|
||||||
|
|
||||||
|
podManager.SetPods([]*v1.Pod{pod})
|
||||||
|
|
||||||
|
go simulateVolumeInUseUpdate(
|
||||||
|
v1.UniqueVolumeName(node.Status.VolumesAttached[0].Name),
|
||||||
|
ctx.Done(),
|
||||||
|
manager)
|
||||||
|
|
||||||
|
err := manager.WaitForAttachAndMount(context.Background(), pod)
|
||||||
|
require.NoError(t, err, "Failed to wait for attach and mount")
|
||||||
|
|
||||||
|
err = manager.WaitForAllPodsUnmount(ctx, []*v1.Pod{pod})
|
||||||
|
|
||||||
|
if test.expectedError {
|
||||||
|
require.Error(t, err, "Expected error due to timeout")
|
||||||
|
require.Contains(t, err.Error(), "context deadline exceeded", "Expected deadline exceeded error")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err, "Expected no error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user