Unverified Commit d6625f85 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #58177 from jingxu97/Jan/reconstruct

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Redesign and implement volume reconstruction work This PR is the first part of redesign of volume reconstruction work. The detailed design information is https://github.com/kubernetes/community/pull/1601 The changes include 1. Remove dependency on volume spec stored in actual state for volume cleanup process (UnmountVolume and UnmountDevice) Modify AttachedVolume struct to add DeviceMountPath so that volume unmount operation can use this information instead of constructing from volume spec 2. Modify reconciler's volume reconstruction process (syncState). Currently workflow is when kubelet restarts, syncState() is only called once before reconciler starts its loop. a. If volume plugin supports reconstruction, it will use the reconstructed volume spec information to update actual state as before. b. If volume plugin cannot support reconstruction, it will use the scanned mount path information to clean up the mounts. In this PR, all the plugins still support reconstruction (except glusterfs), so reconstruction of some plugins will still have issues. The next PR will modify those plugins that cannot support reconstruction well. This PR addresses issue #52683
parents 98abac70 9588d209
...@@ -96,7 +96,7 @@ func (c *PodConfig) SeenAllSources(seenSources sets.String) bool { ...@@ -96,7 +96,7 @@ func (c *PodConfig) SeenAllSources(seenSources sets.String) bool {
if c.pods == nil { if c.pods == nil {
return false return false
} }
glog.V(6).Infof("Looking for %v, have seen %v", c.sources.List(), seenSources) glog.V(5).Infof("Looking for %v, have seen %v", c.sources.List(), seenSources)
return seenSources.HasAll(c.sources.List()...) && c.pods.seenSources(c.sources.List()...) return seenSources.HasAll(c.sources.List()...) && c.pods.seenSources(c.sources.List()...)
} }
......
...@@ -73,7 +73,7 @@ type ActualStateOfWorld interface { ...@@ -73,7 +73,7 @@ type ActualStateOfWorld interface {
// must unmounted prior to detach. // must unmounted prior to detach.
// If a volume with the name volumeName does not exist in the list of // If a volume with the name volumeName does not exist in the list of
// attached volumes, an error is returned. // attached volumes, an error is returned.
SetVolumeGloballyMounted(volumeName v1.UniqueVolumeName, globallyMounted bool) error SetVolumeGloballyMounted(volumeName v1.UniqueVolumeName, globallyMounted bool, devicePath, deviceMountPath string) error
// DeletePodFromVolume removes the given pod from the given volume in the // DeletePodFromVolume removes the given pod from the given volume in the
// cache indicating the volume has been successfully unmounted from the pod. // cache indicating the volume has been successfully unmounted from the pod.
...@@ -109,6 +109,13 @@ type ActualStateOfWorld interface { ...@@ -109,6 +109,13 @@ type ActualStateOfWorld interface {
// volumes that do not need to update contents should not fail. // volumes that do not need to update contents should not fail.
PodExistsInVolume(podName volumetypes.UniquePodName, volumeName v1.UniqueVolumeName) (bool, string, error) PodExistsInVolume(podName volumetypes.UniquePodName, volumeName v1.UniqueVolumeName) (bool, string, error)
// VolumeExistsWithSpecName returns true if the given volume specified with the
// volume spec name (a.k.a., InnerVolumeSpecName) exists in the list of
// volumes that should be attached to this node.
// If a pod with the same name does not exist under the specified
// volume, false is returned.
VolumeExistsWithSpecName(podName volumetypes.UniquePodName, volumeSpecName string) bool
// VolumeExists returns true if the given volume exists in the list of // VolumeExists returns true if the given volume exists in the list of
// attached volumes in the cache, indicating the volume is attached to this // attached volumes in the cache, indicating the volume is attached to this
// node. // node.
...@@ -240,6 +247,10 @@ type attachedVolume struct { ...@@ -240,6 +247,10 @@ type attachedVolume struct {
// devicePath contains the path on the node where the volume is attached for // devicePath contains the path on the node where the volume is attached for
// attachable volumes // attachable volumes
devicePath string devicePath string
// deviceMountPath contains the path on the node where the device should
// be mounted after it is attached.
deviceMountPath string
} }
// The mountedPod object represents a pod for which the kubelet volume manager // The mountedPod object represents a pod for which the kubelet volume manager
...@@ -318,13 +329,13 @@ func (asw *actualStateOfWorld) MarkVolumeAsUnmounted( ...@@ -318,13 +329,13 @@ func (asw *actualStateOfWorld) MarkVolumeAsUnmounted(
} }
func (asw *actualStateOfWorld) MarkDeviceAsMounted( func (asw *actualStateOfWorld) MarkDeviceAsMounted(
volumeName v1.UniqueVolumeName) error { volumeName v1.UniqueVolumeName, devicePath, deviceMountPath string) error {
return asw.SetVolumeGloballyMounted(volumeName, true /* globallyMounted */) return asw.SetVolumeGloballyMounted(volumeName, true /* globallyMounted */, devicePath, deviceMountPath)
} }
func (asw *actualStateOfWorld) MarkDeviceAsUnmounted( func (asw *actualStateOfWorld) MarkDeviceAsUnmounted(
volumeName v1.UniqueVolumeName) error { volumeName v1.UniqueVolumeName) error {
return asw.SetVolumeGloballyMounted(volumeName, false /* globallyMounted */) return asw.SetVolumeGloballyMounted(volumeName, false /* globallyMounted */, "", "")
} }
// addVolume adds the given volume to the cache indicating the specified // addVolume adds the given volume to the cache indicating the specified
...@@ -454,7 +465,7 @@ func (asw *actualStateOfWorld) MarkRemountRequired( ...@@ -454,7 +465,7 @@ func (asw *actualStateOfWorld) MarkRemountRequired(
} }
func (asw *actualStateOfWorld) SetVolumeGloballyMounted( func (asw *actualStateOfWorld) SetVolumeGloballyMounted(
volumeName v1.UniqueVolumeName, globallyMounted bool) error { volumeName v1.UniqueVolumeName, globallyMounted bool, devicePath, deviceMountPath string) error {
asw.Lock() asw.Lock()
defer asw.Unlock() defer asw.Unlock()
...@@ -466,6 +477,8 @@ func (asw *actualStateOfWorld) SetVolumeGloballyMounted( ...@@ -466,6 +477,8 @@ func (asw *actualStateOfWorld) SetVolumeGloballyMounted(
} }
volumeObj.globallyMounted = globallyMounted volumeObj.globallyMounted = globallyMounted
volumeObj.deviceMountPath = deviceMountPath
volumeObj.devicePath = devicePath
asw.attachedVolumes[volumeName] = volumeObj asw.attachedVolumes[volumeName] = volumeObj
return nil return nil
} }
...@@ -529,6 +542,19 @@ func (asw *actualStateOfWorld) PodExistsInVolume( ...@@ -529,6 +542,19 @@ func (asw *actualStateOfWorld) PodExistsInVolume(
return podExists, volumeObj.devicePath, nil return podExists, volumeObj.devicePath, nil
} }
func (asw *actualStateOfWorld) VolumeExistsWithSpecName(podName volumetypes.UniquePodName, volumeSpecName string) bool {
asw.RLock()
defer asw.RUnlock()
for _, volumeObj := range asw.attachedVolumes {
for name := range volumeObj.mountedPods {
if podName == name && volumeObj.spec.Name() == volumeSpecName {
return true
}
}
}
return false
}
func (asw *actualStateOfWorld) VolumeExists( func (asw *actualStateOfWorld) VolumeExists(
volumeName v1.UniqueVolumeName) bool { volumeName v1.UniqueVolumeName) bool {
asw.RLock() asw.RLock()
...@@ -625,8 +651,11 @@ func (asw *actualStateOfWorld) newAttachedVolume( ...@@ -625,8 +651,11 @@ func (asw *actualStateOfWorld) newAttachedVolume(
VolumeSpec: attachedVolume.spec, VolumeSpec: attachedVolume.spec,
NodeName: asw.nodeName, NodeName: asw.nodeName,
PluginIsAttachable: attachedVolume.pluginIsAttachable, PluginIsAttachable: attachedVolume.pluginIsAttachable,
DevicePath: attachedVolume.devicePath}, DevicePath: attachedVolume.devicePath,
GloballyMounted: attachedVolume.globallyMounted} DeviceMountPath: attachedVolume.deviceMountPath,
PluginName: attachedVolume.pluginName},
GloballyMounted: attachedVolume.globallyMounted,
}
} }
// Compile-time check to ensure volumeNotAttachedError implements the error interface // Compile-time check to ensure volumeNotAttachedError implements the error interface
...@@ -691,5 +720,6 @@ func getMountedVolume( ...@@ -691,5 +720,6 @@ func getMountedVolume(
Mounter: mountedPod.mounter, Mounter: mountedPod.mounter,
BlockVolumeMapper: mountedPod.blockVolumeMapper, BlockVolumeMapper: mountedPod.blockVolumeMapper,
VolumeGidValue: mountedPod.volumeGidValue, VolumeGidValue: mountedPod.volumeGidValue,
VolumeSpec: attachedVolume.spec}} VolumeSpec: attachedVolume.spec,
DeviceMountPath: attachedVolume.deviceMountPath}}
} }
...@@ -222,6 +222,7 @@ func Test_AddPodToVolume_Positive_ExistingVolumeNewNode(t *testing.T) { ...@@ -222,6 +222,7 @@ func Test_AddPodToVolume_Positive_ExistingVolumeNewNode(t *testing.T) {
verifyVolumeDoesntExistInUnmountedVolumes(t, generatedVolumeName, asw) verifyVolumeDoesntExistInUnmountedVolumes(t, generatedVolumeName, asw)
verifyVolumeDoesntExistInGloballyMountedVolumes(t, generatedVolumeName, asw) verifyVolumeDoesntExistInGloballyMountedVolumes(t, generatedVolumeName, asw)
verifyPodExistsInVolumeAsw(t, podName, generatedVolumeName, "fake/device/path" /* expectedDevicePath */, asw) verifyPodExistsInVolumeAsw(t, podName, generatedVolumeName, "fake/device/path" /* expectedDevicePath */, asw)
verifyVolumeExistsWithSpecNameInVolumeAsw(t, podName, volumeSpec.Name(), asw)
} }
// Populates data struct with a volume // Populates data struct with a volume
...@@ -292,6 +293,7 @@ func Test_AddPodToVolume_Positive_ExistingVolumeExistingNode(t *testing.T) { ...@@ -292,6 +293,7 @@ func Test_AddPodToVolume_Positive_ExistingVolumeExistingNode(t *testing.T) {
verifyVolumeDoesntExistInUnmountedVolumes(t, generatedVolumeName, asw) verifyVolumeDoesntExistInUnmountedVolumes(t, generatedVolumeName, asw)
verifyVolumeDoesntExistInGloballyMountedVolumes(t, generatedVolumeName, asw) verifyVolumeDoesntExistInGloballyMountedVolumes(t, generatedVolumeName, asw)
verifyPodExistsInVolumeAsw(t, podName, generatedVolumeName, "fake/device/path" /* expectedDevicePath */, asw) verifyPodExistsInVolumeAsw(t, podName, generatedVolumeName, "fake/device/path" /* expectedDevicePath */, asw)
verifyVolumeExistsWithSpecNameInVolumeAsw(t, podName, volumeSpec.Name(), asw)
} }
// Calls AddPodToVolume() to add pod to empty data stuct // Calls AddPodToVolume() to add pod to empty data stuct
...@@ -370,6 +372,7 @@ func Test_AddPodToVolume_Negative_VolumeDoesntExist(t *testing.T) { ...@@ -370,6 +372,7 @@ func Test_AddPodToVolume_Negative_VolumeDoesntExist(t *testing.T) {
volumeName, volumeName,
false, /* expectVolumeToExist */ false, /* expectVolumeToExist */
asw) asw)
verifyVolumeDoesntExistWithSpecNameInVolumeAsw(t, podName, volumeSpec.Name(), asw)
} }
// Calls MarkVolumeAsAttached() once to add volume // Calls MarkVolumeAsAttached() once to add volume
...@@ -400,6 +403,7 @@ func Test_MarkDeviceAsMounted_Positive_NewVolume(t *testing.T) { ...@@ -400,6 +403,7 @@ func Test_MarkDeviceAsMounted_Positive_NewVolume(t *testing.T) {
} }
volumeSpec := &volume.Spec{Volume: &pod.Spec.Volumes[0]} volumeSpec := &volume.Spec{Volume: &pod.Spec.Volumes[0]}
devicePath := "fake/device/path" devicePath := "fake/device/path"
deviceMountPath := "fake/device/mount/path"
generatedVolumeName, err := volumehelper.GetUniqueVolumeNameFromSpec(plugin, volumeSpec) generatedVolumeName, err := volumehelper.GetUniqueVolumeNameFromSpec(plugin, volumeSpec)
err = asw.MarkVolumeAsAttached(emptyVolumeName, volumeSpec, "" /* nodeName */, devicePath) err = asw.MarkVolumeAsAttached(emptyVolumeName, volumeSpec, "" /* nodeName */, devicePath)
...@@ -408,7 +412,7 @@ func Test_MarkDeviceAsMounted_Positive_NewVolume(t *testing.T) { ...@@ -408,7 +412,7 @@ func Test_MarkDeviceAsMounted_Positive_NewVolume(t *testing.T) {
} }
// Act // Act
err = asw.MarkDeviceAsMounted(generatedVolumeName) err = asw.MarkDeviceAsMounted(generatedVolumeName, devicePath, deviceMountPath)
// Assert // Assert
if err != nil { if err != nil {
...@@ -546,3 +550,33 @@ func verifyPodDoesntExistInVolumeAsw( ...@@ -546,3 +550,33 @@ func verifyPodDoesntExistInVolumeAsw(
devicePath) devicePath)
} }
} }
func verifyVolumeExistsWithSpecNameInVolumeAsw(
t *testing.T,
expectedPodName volumetypes.UniquePodName,
expectedVolumeName string,
asw ActualStateOfWorld) {
podExistsInVolume :=
asw.VolumeExistsWithSpecName(expectedPodName, expectedVolumeName)
if !podExistsInVolume {
t.Fatalf(
"ASW VolumeExistsWithSpecName result invalid. Expected: <true> Actual: <%v>",
podExistsInVolume)
}
}
func verifyVolumeDoesntExistWithSpecNameInVolumeAsw(
t *testing.T,
podToCheck volumetypes.UniquePodName,
volumeToCheck string,
asw ActualStateOfWorld) {
podExistsInVolume :=
asw.VolumeExistsWithSpecName(podToCheck, volumeToCheck)
if podExistsInVolume {
t.Fatalf(
"ASW VolumeExistsWithSpecName result invalid. Expected: <false> Actual: <%v>",
podExistsInVolume)
}
}
...@@ -98,6 +98,13 @@ type DesiredStateOfWorld interface { ...@@ -98,6 +98,13 @@ type DesiredStateOfWorld interface {
// with pod's unique name. This map can be used to determine which pod is currently // with pod's unique name. This map can be used to determine which pod is currently
// in desired state of world. // in desired state of world.
GetPods() map[types.UniquePodName]bool GetPods() map[types.UniquePodName]bool
// VolumeExistsWithSpecName returns true if the given volume specified with the
// volume spec name (a.k.a., InnerVolumeSpecName) exists in the list of
// volumes that should be attached to this node.
// If a pod with the same name does not exist under the specified
// volume, false is returned.
VolumeExistsWithSpecName(podName types.UniquePodName, volumeSpecName string) bool
} }
// VolumeToMount represents a volume that is attached to this node and needs to // VolumeToMount represents a volume that is attached to this node and needs to
...@@ -234,7 +241,6 @@ func (dsw *desiredStateOfWorld) AddPodToVolume( ...@@ -234,7 +241,6 @@ func (dsw *desiredStateOfWorld) AddPodToVolume(
spec: volumeSpec, spec: volumeSpec,
outerVolumeSpecName: outerVolumeSpecName, outerVolumeSpecName: outerVolumeSpecName,
} }
return volumeName, nil return volumeName, nil
} }
...@@ -303,6 +309,19 @@ func (dsw *desiredStateOfWorld) PodExistsInVolume( ...@@ -303,6 +309,19 @@ func (dsw *desiredStateOfWorld) PodExistsInVolume(
return podExists return podExists
} }
func (dsw *desiredStateOfWorld) VolumeExistsWithSpecName(podName types.UniquePodName, volumeSpecName string) bool {
dsw.RLock()
defer dsw.RUnlock()
for _, volumeObj := range dsw.volumesToMount {
for name, podObj := range volumeObj.podsToMount {
if podName == name && podObj.spec.Name() == volumeSpecName {
return true
}
}
}
return false
}
func (dsw *desiredStateOfWorld) GetPods() map[types.UniquePodName]bool { func (dsw *desiredStateOfWorld) GetPods() map[types.UniquePodName]bool {
dsw.RLock() dsw.RLock()
defer dsw.RUnlock() defer dsw.RUnlock()
......
...@@ -69,6 +69,7 @@ func Test_AddPodToVolume_Positive_NewPodNewVolume(t *testing.T) { ...@@ -69,6 +69,7 @@ func Test_AddPodToVolume_Positive_NewPodNewVolume(t *testing.T) {
verifyVolumeExistsInVolumesToMount( verifyVolumeExistsInVolumesToMount(
t, generatedVolumeName, false /* expectReportedInUse */, dsw) t, generatedVolumeName, false /* expectReportedInUse */, dsw)
verifyPodExistsInVolumeDsw(t, podName, generatedVolumeName, dsw) verifyPodExistsInVolumeDsw(t, podName, generatedVolumeName, dsw)
verifyVolumeExistsWithSpecNameInVolumeDsw(t, podName, volumeSpec.Name(), dsw)
} }
// Calls AddPodToVolume() twice to add the same pod to the same volume // Calls AddPodToVolume() twice to add the same pod to the same volume
...@@ -113,6 +114,7 @@ func Test_AddPodToVolume_Positive_ExistingPodExistingVolume(t *testing.T) { ...@@ -113,6 +114,7 @@ func Test_AddPodToVolume_Positive_ExistingPodExistingVolume(t *testing.T) {
verifyVolumeExistsInVolumesToMount( verifyVolumeExistsInVolumesToMount(
t, generatedVolumeName, false /* expectReportedInUse */, dsw) t, generatedVolumeName, false /* expectReportedInUse */, dsw)
verifyPodExistsInVolumeDsw(t, podName, generatedVolumeName, dsw) verifyPodExistsInVolumeDsw(t, podName, generatedVolumeName, dsw)
verifyVolumeExistsWithSpecNameInVolumeDsw(t, podName, volumeSpec.Name(), dsw)
} }
// Populates data struct with a new volume/pod // Populates data struct with a new volume/pod
...@@ -160,6 +162,7 @@ func Test_DeletePodFromVolume_Positive_PodExistsVolumeExists(t *testing.T) { ...@@ -160,6 +162,7 @@ func Test_DeletePodFromVolume_Positive_PodExistsVolumeExists(t *testing.T) {
verifyVolumeDoesntExist(t, generatedVolumeName, dsw) verifyVolumeDoesntExist(t, generatedVolumeName, dsw)
verifyVolumeDoesntExistInVolumesToMount(t, generatedVolumeName, dsw) verifyVolumeDoesntExistInVolumesToMount(t, generatedVolumeName, dsw)
verifyPodDoesntExistInVolumeDsw(t, podName, generatedVolumeName, dsw) verifyPodDoesntExistInVolumeDsw(t, podName, generatedVolumeName, dsw)
verifyVolumeDoesntExistWithSpecNameInVolumeDsw(t, podName, volumeSpec.Name(), dsw)
} }
// Calls AddPodToVolume() to add three new volumes to data struct // Calls AddPodToVolume() to add three new volumes to data struct
...@@ -380,3 +383,29 @@ func verifyPodDoesntExistInVolumeDsw( ...@@ -380,3 +383,29 @@ func verifyPodDoesntExistInVolumeDsw(
podExistsInVolume) podExistsInVolume)
} }
} }
func verifyVolumeExistsWithSpecNameInVolumeDsw(
t *testing.T,
expectedPodName volumetypes.UniquePodName,
expectedVolumeSpecName string,
dsw DesiredStateOfWorld) {
if podExistsInVolume := dsw.VolumeExistsWithSpecName(
expectedPodName, expectedVolumeSpecName); !podExistsInVolume {
t.Fatalf(
"DSW VolumeExistsWithSpecNam returned incorrect value. Expected: <true> Actual: <%v>",
podExistsInVolume)
}
}
func verifyVolumeDoesntExistWithSpecNameInVolumeDsw(
t *testing.T,
expectedPodName volumetypes.UniquePodName,
expectedVolumeSpecName string,
dsw DesiredStateOfWorld) {
if podExistsInVolume := dsw.VolumeExistsWithSpecName(
expectedPodName, expectedVolumeSpecName); podExistsInVolume {
t.Fatalf(
"DSW VolumeExistsWithSpecNam returned incorrect value. Expected: <true> Actual: <%v>",
podExistsInVolume)
}
}
...@@ -123,6 +123,7 @@ type processedPods struct { ...@@ -123,6 +123,7 @@ type processedPods struct {
func (dswp *desiredStateOfWorldPopulator) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) { func (dswp *desiredStateOfWorldPopulator) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) {
// Wait for the completion of a loop that started after sources are all ready, then set hasAddedPods accordingly // Wait for the completion of a loop that started after sources are all ready, then set hasAddedPods accordingly
glog.Infof("Desired state populator starts to run")
wait.PollUntil(dswp.loopSleepDuration, func() (bool, error) { wait.PollUntil(dswp.loopSleepDuration, func() (bool, error) {
done := sourcesReady.AllReady() done := sourcesReady.AllReady()
dswp.populatorLoopFunc()() dswp.populatorLoopFunc()()
......
...@@ -933,7 +933,8 @@ func Test_GenerateUnmapDeviceFunc_Plugin_Not_Found(t *testing.T) { ...@@ -933,7 +933,8 @@ func Test_GenerateUnmapDeviceFunc_Plugin_Not_Found(t *testing.T) {
false, /* checkNodeCapabilitiesBeforeMount */ false, /* checkNodeCapabilitiesBeforeMount */
nil)) nil))
var mounter mount.Interface var mounter mount.Interface
deviceToDetach := operationexecutor.AttachedVolume{VolumeSpec: &volume.Spec{}} plugins := volumetesting.NewFakeFileVolumePlugin()
deviceToDetach := operationexecutor.AttachedVolume{VolumeSpec: &volume.Spec{}, PluginName: plugins[0].GetPluginName()}
err := oex.UnmapDevice(deviceToDetach, asw, mounter) err := oex.UnmapDevice(deviceToDetach, asw, mounter)
// Assert // Assert
if assert.Error(t, err) { if assert.Error(t, err) {
...@@ -949,7 +950,7 @@ func waitForMount( ...@@ -949,7 +950,7 @@ func waitForMount(
volumeName v1.UniqueVolumeName, volumeName v1.UniqueVolumeName,
asw cache.ActualStateOfWorld) { asw cache.ActualStateOfWorld) {
err := retryWithExponentialBackOff( err := retryWithExponentialBackOff(
time.Duration(5*time.Millisecond), time.Duration(500*time.Millisecond),
func() (bool, error) { func() (bool, error) {
mountedVolumes := asw.GetMountedVolumes() mountedVolumes := asw.GetMountedVolumes()
for _, mountedVolume := range mountedVolumes { for _, mountedVolume := range mountedVolumes {
...@@ -973,7 +974,7 @@ func waitForDetach( ...@@ -973,7 +974,7 @@ func waitForDetach(
volumeName v1.UniqueVolumeName, volumeName v1.UniqueVolumeName,
asw cache.ActualStateOfWorld) { asw cache.ActualStateOfWorld) {
err := retryWithExponentialBackOff( err := retryWithExponentialBackOff(
time.Duration(5*time.Millisecond), time.Duration(500*time.Millisecond),
func() (bool, error) { func() (bool, error) {
if asw.VolumeExists(volumeName) { if asw.VolumeExists(volumeName) {
return false, nil return false, nil
......
...@@ -21,6 +21,7 @@ package mount ...@@ -21,6 +21,7 @@ package mount
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type FileType string type FileType string
...@@ -261,3 +262,21 @@ func isBind(options []string) (bool, []string) { ...@@ -261,3 +262,21 @@ func isBind(options []string) (bool, []string) {
return bind, bindRemountOpts return bind, bindRemountOpts
} }
// TODO: this is a workaround for the unmount device issue caused by gci mounter.
// In GCI cluster, if gci mounter is used for mounting, the container started by mounter
// script will cause additional mounts created in the container. Since these mounts are
// irrelavant to the original mounts, they should be not considered when checking the
// mount references. Current solution is to filter out those mount paths that contain
// the string of original mount path.
// Plan to work on better approach to solve this issue.
func HasMountRefs(mountPath string, mountRefs []string) bool {
count := 0
for _, ref := range mountRefs {
if !strings.Contains(ref, mountPath) {
count = count + 1
}
}
return count > 0
}
...@@ -22,7 +22,6 @@ package operationexecutor ...@@ -22,7 +22,6 @@ package operationexecutor
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
...@@ -169,7 +168,7 @@ type ActualStateOfWorldMounterUpdater interface { ...@@ -169,7 +168,7 @@ type ActualStateOfWorldMounterUpdater interface {
MarkVolumeAsUnmounted(podName volumetypes.UniquePodName, volumeName v1.UniqueVolumeName) error MarkVolumeAsUnmounted(podName volumetypes.UniquePodName, volumeName v1.UniqueVolumeName) error
// Marks the specified volume as having been globally mounted. // Marks the specified volume as having been globally mounted.
MarkDeviceAsMounted(volumeName v1.UniqueVolumeName) error MarkDeviceAsMounted(volumeName v1.UniqueVolumeName, devicePath, deviceMountPath string) error
// Marks the specified volume as having its global mount unmounted. // Marks the specified volume as having its global mount unmounted.
MarkDeviceAsUnmounted(volumeName v1.UniqueVolumeName) error MarkDeviceAsUnmounted(volumeName v1.UniqueVolumeName) error
...@@ -386,6 +385,14 @@ type AttachedVolume struct { ...@@ -386,6 +385,14 @@ type AttachedVolume struct {
// DevicePath contains the path on the node where the volume is attached. // DevicePath contains the path on the node where the volume is attached.
// For non-attachable volumes this is empty. // For non-attachable volumes this is empty.
DevicePath string DevicePath string
// DeviceMountPath contains the path on the node where the device should
// be mounted after it is attached.
DeviceMountPath string
// PluginName is the Unescaped Qualified name of the volume plugin used to
// attach and mount this volume.
PluginName string
} }
// GenerateMsgDetailed returns detailed msgs for attached volumes // GenerateMsgDetailed returns detailed msgs for attached volumes
...@@ -529,6 +536,10 @@ type MountedVolume struct { ...@@ -529,6 +536,10 @@ type MountedVolume struct {
// VolumeSpec is a volume spec containing the specification for the volume // VolumeSpec is a volume spec containing the specification for the volume
// that should be mounted. // that should be mounted.
VolumeSpec *volume.Spec VolumeSpec *volume.Spec
// DeviceMountPath contains the path on the node where the device should
// be mounted after it is attached.
DeviceMountPath string
} }
// GenerateMsgDetailed returns detailed msgs for mounted volumes // GenerateMsgDetailed returns detailed msgs for mounted volumes
...@@ -866,6 +877,21 @@ func NewVolumeHandler(volumeSpec *volume.Spec, oe OperationExecutor) (VolumeStat ...@@ -866,6 +877,21 @@ func NewVolumeHandler(volumeSpec *volume.Spec, oe OperationExecutor) (VolumeStat
return volumeHandler, nil return volumeHandler, nil
} }
// NewVolumeHandlerWithMode return a new instance of volumeHandler depens on a volumeMode
func NewVolumeHandlerWithMode(volumeMode v1.PersistentVolumeMode, oe OperationExecutor) (VolumeStateHandler, error) {
var volumeHandler VolumeStateHandler
if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) {
if volumeMode == v1.PersistentVolumeFilesystem {
volumeHandler = NewFilesystemVolumeHandler(oe)
} else {
volumeHandler = NewBlockVolumeHandler(oe)
}
} else {
volumeHandler = NewFilesystemVolumeHandler(oe)
}
return volumeHandler, nil
}
// NewFilesystemVolumeHandler returns a new instance of FilesystemVolumeHandler. // NewFilesystemVolumeHandler returns a new instance of FilesystemVolumeHandler.
func NewFilesystemVolumeHandler(operationExecutor OperationExecutor) FilesystemVolumeHandler { func NewFilesystemVolumeHandler(operationExecutor OperationExecutor) FilesystemVolumeHandler {
return FilesystemVolumeHandler{ return FilesystemVolumeHandler{
...@@ -924,7 +950,7 @@ func (f FilesystemVolumeHandler) UnmountDeviceHandler(attachedVolume AttachedVol ...@@ -924,7 +950,7 @@ func (f FilesystemVolumeHandler) UnmountDeviceHandler(attachedVolume AttachedVol
// ReconstructVolumeHandler create volumeSpec from mount path // ReconstructVolumeHandler create volumeSpec from mount path
// This method is handler for filesystem volume // This method is handler for filesystem volume
func (f FilesystemVolumeHandler) ReconstructVolumeHandler(plugin volume.VolumePlugin, _ volume.BlockVolumePlugin, _ types.UID, _ volumetypes.UniquePodName, volumeSpecName string, mountPath string, _ string) (*volume.Spec, error) { func (f FilesystemVolumeHandler) ReconstructVolumeHandler(plugin volume.VolumePlugin, _ volume.BlockVolumePlugin, _ types.UID, _ volumetypes.UniquePodName, volumeSpecName string, mountPath string, _ string) (*volume.Spec, error) {
glog.V(12).Infof("Starting operationExecutor.ReconstructVolumepodName") glog.V(4).Infof("Starting operationExecutor.ReconstructVolumepodName volume spec name %s, mount path %s", volumeSpecName, mountPath)
volumeSpec, err := plugin.ConstructVolumeSpec(volumeSpecName, mountPath) volumeSpec, err := plugin.ConstructVolumeSpec(volumeSpecName, mountPath)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -1024,21 +1050,3 @@ func (b BlockVolumeHandler) CheckVolumeExistence(mountPath, volumeName string, m ...@@ -1024,21 +1050,3 @@ func (b BlockVolumeHandler) CheckVolumeExistence(mountPath, volumeName string, m
} }
return islinkExist, nil return islinkExist, nil
} }
// TODO: this is a workaround for the unmount device issue caused by gci mounter.
// In GCI cluster, if gci mounter is used for mounting, the container started by mounter
// script will cause additional mounts created in the container. Since these mounts are
// irrelavant to the original mounts, they should be not considered when checking the
// mount references. Current solution is to filter out those mount paths that contain
// the string of original mount path.
// Plan to work on better approach to solve this issue.
func hasMountRefs(mountPath string, mountRefs []string) bool {
count := 0
for _, ref := range mountRefs {
if !strings.Contains(ref, mountPath) {
count = count + 1
}
}
return count > 0
}
...@@ -514,7 +514,7 @@ func (og *operationGenerator) GenerateMountVolumeFunc( ...@@ -514,7 +514,7 @@ func (og *operationGenerator) GenerateMountVolumeFunc(
// Update actual state of world to reflect volume is globally mounted // Update actual state of world to reflect volume is globally mounted
markDeviceMountedErr := actualStateOfWorld.MarkDeviceAsMounted( markDeviceMountedErr := actualStateOfWorld.MarkDeviceAsMounted(
volumeToMount.VolumeName) volumeToMount.VolumeName, devicePath, deviceMountPath)
if markDeviceMountedErr != nil { if markDeviceMountedErr != nil {
// On failure, return error. Caller will log and retry. // On failure, return error. Caller will log and retry.
return volumeToMount.GenerateError("MountVolume.MarkDeviceAsMounted failed", markDeviceMountedErr) return volumeToMount.GenerateError("MountVolume.MarkDeviceAsMounted failed", markDeviceMountedErr)
...@@ -707,7 +707,7 @@ func (og *operationGenerator) GenerateUnmountDeviceFunc( ...@@ -707,7 +707,7 @@ func (og *operationGenerator) GenerateUnmountDeviceFunc(
mounter mount.Interface) (volumetypes.GeneratedOperations, error) { mounter mount.Interface) (volumetypes.GeneratedOperations, error) {
// Get attacher plugin // Get attacher plugin
attachableVolumePlugin, err := attachableVolumePlugin, err :=
og.volumePluginMgr.FindAttachablePluginBySpec(deviceToDetach.VolumeSpec) og.volumePluginMgr.FindAttachablePluginByName(deviceToDetach.PluginName)
if err != nil || attachableVolumePlugin == nil { if err != nil || attachableVolumePlugin == nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmountDevice.FindAttachablePluginBySpec failed", err) return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmountDevice.FindAttachablePluginBySpec failed", err)
} }
...@@ -716,22 +716,11 @@ func (og *operationGenerator) GenerateUnmountDeviceFunc( ...@@ -716,22 +716,11 @@ func (og *operationGenerator) GenerateUnmountDeviceFunc(
if err != nil { if err != nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmountDevice.NewDetacher failed", err) return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmountDevice.NewDetacher failed", err)
} }
volumeAttacher, err := attachableVolumePlugin.NewAttacher()
if err != nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmountDevice.NewAttacher failed", err)
}
unmountDeviceFunc := func() (error, error) { unmountDeviceFunc := func() (error, error) {
deviceMountPath, err := deviceMountPath := deviceToDetach.DeviceMountPath
volumeAttacher.GetDeviceMountPath(deviceToDetach.VolumeSpec)
if err != nil {
// On failure, return error. Caller will log and retry.
return deviceToDetach.GenerateError("GetDeviceMountPath failed", err)
}
refs, err := attachableVolumePlugin.GetDeviceMountRefs(deviceMountPath) refs, err := attachableVolumePlugin.GetDeviceMountRefs(deviceMountPath)
if err != nil || hasMountRefs(deviceMountPath, refs) { if err != nil || mount.HasMountRefs(deviceMountPath, refs) {
if err == nil { if err == nil {
err = fmt.Errorf("The device mount path %q is still mounted by other references %v", deviceMountPath, refs) err = fmt.Errorf("The device mount path %q is still mounted by other references %v", deviceMountPath, refs)
} }
...@@ -828,6 +817,13 @@ func (og *operationGenerator) GenerateMapVolumeFunc( ...@@ -828,6 +817,13 @@ func (og *operationGenerator) GenerateMapVolumeFunc(
mapVolumeFunc := func() (error, error) { mapVolumeFunc := func() (error, error) {
var devicePath string var devicePath string
// Set up global map path under the given plugin directory using symbolic link
globalMapPath, err :=
blockVolumeMapper.GetGlobalMapPath(volumeToMount.VolumeSpec)
if err != nil {
// On failure, return error. Caller will log and retry.
return volumeToMount.GenerateError("MapVolume.GetDeviceMountPath failed", err)
}
if volumeAttacher != nil { if volumeAttacher != nil {
// Wait for attachable volumes to finish attaching // Wait for attachable volumes to finish attaching
glog.Infof(volumeToMount.GenerateMsgDetailed("MapVolume.WaitForAttach entering", fmt.Sprintf("DevicePath %q", volumeToMount.DevicePath))) glog.Infof(volumeToMount.GenerateMsgDetailed("MapVolume.WaitForAttach entering", fmt.Sprintf("DevicePath %q", volumeToMount.DevicePath)))
...@@ -843,7 +839,7 @@ func (og *operationGenerator) GenerateMapVolumeFunc( ...@@ -843,7 +839,7 @@ func (og *operationGenerator) GenerateMapVolumeFunc(
// Update actual state of world to reflect volume is globally mounted // Update actual state of world to reflect volume is globally mounted
markDeviceMappedErr := actualStateOfWorld.MarkDeviceAsMounted( markDeviceMappedErr := actualStateOfWorld.MarkDeviceAsMounted(
volumeToMount.VolumeName) volumeToMount.VolumeName, devicePath, globalMapPath)
if markDeviceMappedErr != nil { if markDeviceMappedErr != nil {
// On failure, return error. Caller will log and retry. // On failure, return error. Caller will log and retry.
return volumeToMount.GenerateError("MapVolume.MarkDeviceAsMounted failed", markDeviceMappedErr) return volumeToMount.GenerateError("MapVolume.MarkDeviceAsMounted failed", markDeviceMappedErr)
...@@ -863,18 +859,13 @@ func (og *operationGenerator) GenerateMapVolumeFunc( ...@@ -863,18 +859,13 @@ func (og *operationGenerator) GenerateMapVolumeFunc(
return volumeToMount.GenerateError("MapVolume failed", fmt.Errorf("Device path of the volume is empty")) return volumeToMount.GenerateError("MapVolume failed", fmt.Errorf("Device path of the volume is empty"))
} }
} }
// Set up global map path under the given plugin directory using symbolic link
globalMapPath, err :=
blockVolumeMapper.GetGlobalMapPath(volumeToMount.VolumeSpec)
if err != nil {
// On failure, return error. Caller will log and retry.
return volumeToMount.GenerateError("MapVolume.GetDeviceMountPath failed", err)
}
mapErr = og.blkUtil.MapDevice(devicePath, globalMapPath, string(volumeToMount.Pod.UID)) mapErr = og.blkUtil.MapDevice(devicePath, globalMapPath, string(volumeToMount.Pod.UID))
if mapErr != nil { if mapErr != nil {
// On failure, return error. Caller will log and retry. // On failure, return error. Caller will log and retry.
return volumeToMount.GenerateError("MapVolume.MapDevice failed", mapErr) return volumeToMount.GenerateError("MapVolume.MapDevice failed", mapErr)
} }
// Device mapping for global map path succeeded // Device mapping for global map path succeeded
simpleMsg, detailedMsg := volumeToMount.GenerateMsg("MapVolume.MapDevice succeeded", fmt.Sprintf("globalMapPath %q", globalMapPath)) simpleMsg, detailedMsg := volumeToMount.GenerateMsg("MapVolume.MapDevice succeeded", fmt.Sprintf("globalMapPath %q", globalMapPath))
verbosity := glog.Level(4) verbosity := glog.Level(4)
...@@ -969,8 +960,7 @@ func (og *operationGenerator) GenerateUnmapVolumeFunc( ...@@ -969,8 +960,7 @@ func (og *operationGenerator) GenerateUnmapVolumeFunc(
} }
// Try to unmap podUID symlink under global map path dir // Try to unmap podUID symlink under global map path dir
// plugins/kubernetes.io/{PluginName}/volumeDevices/{volumePluginDependentPath}/{podUID} // plugins/kubernetes.io/{PluginName}/volumeDevices/{volumePluginDependentPath}/{podUID}
globalUnmapPath, err := globalUnmapPath := volumeToUnmount.DeviceMountPath
blockVolumeUnmapper.GetGlobalMapPath(volumeToUnmount.VolumeSpec)
if err != nil { if err != nil {
// On failure, return error. Caller will log and retry. // On failure, return error. Caller will log and retry.
return volumeToUnmount.GenerateError("UnmapVolume.GetGlobalUnmapPath failed", err) return volumeToUnmount.GenerateError("UnmapVolume.GetGlobalUnmapPath failed", err)
...@@ -1024,23 +1014,14 @@ func (og *operationGenerator) GenerateUnmapDeviceFunc( ...@@ -1024,23 +1014,14 @@ func (og *operationGenerator) GenerateUnmapDeviceFunc(
actualStateOfWorld ActualStateOfWorldMounterUpdater, actualStateOfWorld ActualStateOfWorldMounterUpdater,
mounter mount.Interface) (volumetypes.GeneratedOperations, error) { mounter mount.Interface) (volumetypes.GeneratedOperations, error) {
// Get block volume mapper plugin
var blockVolumeMapper volume.BlockVolumeMapper
blockVolumePlugin, err := blockVolumePlugin, err :=
og.volumePluginMgr.FindMapperPluginBySpec(deviceToDetach.VolumeSpec) og.volumePluginMgr.FindMapperPluginByName(deviceToDetach.PluginName)
if err != nil { if err != nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmapDevice.FindMapperPluginBySpec failed", err) return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmapDevice.FindMapperPluginBySpec failed", err)
} }
if blockVolumePlugin == nil { if blockVolumePlugin == nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmapDevice.FindMapperPluginBySpec failed to find BlockVolumeMapper plugin. Volume plugin is nil.", nil) return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmapDevice.FindMapperPluginBySpec failed to find BlockVolumeMapper plugin. Volume plugin is nil.", nil)
} }
blockVolumeMapper, newMapperErr := blockVolumePlugin.NewBlockVolumeMapper(
deviceToDetach.VolumeSpec,
nil, /* Pod */
volume.VolumeOptions{})
if newMapperErr != nil {
return volumetypes.GeneratedOperations{}, deviceToDetach.GenerateErrorDetailed("UnmapDevice.NewBlockVolumeMapper initialization failed", newMapperErr)
}
blockVolumeUnmapper, newUnmapperErr := blockVolumePlugin.NewBlockVolumeUnmapper( blockVolumeUnmapper, newUnmapperErr := blockVolumePlugin.NewBlockVolumeUnmapper(
string(deviceToDetach.VolumeName), string(deviceToDetach.VolumeName),
...@@ -1052,8 +1033,7 @@ func (og *operationGenerator) GenerateUnmapDeviceFunc( ...@@ -1052,8 +1033,7 @@ func (og *operationGenerator) GenerateUnmapDeviceFunc(
unmapDeviceFunc := func() (error, error) { unmapDeviceFunc := func() (error, error) {
// Search under globalMapPath dir if all symbolic links from pods have been removed already. // Search under globalMapPath dir if all symbolic links from pods have been removed already.
// If symbolick links are there, pods may still refer the volume. // If symbolick links are there, pods may still refer the volume.
globalMapPath, err := globalMapPath := deviceToDetach.DeviceMountPath
blockVolumeMapper.GetGlobalMapPath(deviceToDetach.VolumeSpec)
if err != nil { if err != nil {
// On failure, return error. Caller will log and retry. // On failure, return error. Caller will log and retry.
return deviceToDetach.GenerateError("UnmapDevice.GetGlobalMapPath failed", err) return deviceToDetach.GenerateError("UnmapDevice.GetGlobalMapPath failed", err)
......
...@@ -223,6 +223,10 @@ var _ = utils.SIGDescribe("PersistentVolumes[Disruptive][Flaky]", func() { ...@@ -223,6 +223,10 @@ var _ = utils.SIGDescribe("PersistentVolumes[Disruptive][Flaky]", func() {
testItStmt: "Should test that a volume mounted to a pod that is deleted while the kubelet is down unmounts when the kubelet returns.", testItStmt: "Should test that a volume mounted to a pod that is deleted while the kubelet is down unmounts when the kubelet returns.",
runTest: utils.TestVolumeUnmountsFromDeletedPod, runTest: utils.TestVolumeUnmountsFromDeletedPod,
}, },
{
testItStmt: "Should test that a volume mounted to a pod that is force deleted while the kubelet is down unmounts when the kubelet returns.",
runTest: utils.TestVolumeUnmountsFromForceDeletedPod,
},
} }
// Test loop executes each disruptiveTest iteratively. // Test loop executes each disruptiveTest iteratively.
......
...@@ -156,7 +156,8 @@ func TestKubeletRestartsAndRestoresMount(c clientset.Interface, f *framework.Fra ...@@ -156,7 +156,8 @@ func TestKubeletRestartsAndRestoresMount(c clientset.Interface, f *framework.Fra
} }
// TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down. // TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down.
func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume) { // forceDelete is true indicating whether the pod is forcelly deleted.
func TestVolumeUnmountsFromDeletedPodWithForceOption(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume, forceDelete bool) {
nodeIP, err := framework.GetHostExternalAddress(c, clientPod) nodeIP, err := framework.GetHostExternalAddress(c, clientPod)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
nodeIP = nodeIP + ":22" nodeIP = nodeIP + ":22"
...@@ -175,7 +176,11 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew ...@@ -175,7 +176,11 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew
} }
}() }()
By(fmt.Sprintf("Deleting Pod %q", clientPod.Name)) By(fmt.Sprintf("Deleting Pod %q", clientPod.Name))
err = c.CoreV1().Pods(clientPod.Namespace).Delete(clientPod.Name, &metav1.DeleteOptions{}) if forceDelete {
err = c.CoreV1().Pods(clientPod.Namespace).Delete(clientPod.Name, metav1.NewDeleteOptions(0))
} else {
err = c.CoreV1().Pods(clientPod.Namespace).Delete(clientPod.Name, &metav1.DeleteOptions{})
}
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Starting the kubelet and waiting for pod to delete.") By("Starting the kubelet and waiting for pod to delete.")
KubeletCommand(KStart, c, clientPod) KubeletCommand(KStart, c, clientPod)
...@@ -184,6 +189,11 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew ...@@ -184,6 +189,11 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew
Expect(err).NotTo(HaveOccurred(), "Expected pod to terminate.") Expect(err).NotTo(HaveOccurred(), "Expected pod to terminate.")
} }
if forceDelete {
// With forceDelete, since pods are immediately deleted from API server, there is no way to be sure when volumes are torn down
// so wait some time to finish
time.Sleep(30 * time.Second)
}
By("Expecting the volume mount not to be found.") By("Expecting the volume mount not to be found.")
result, err = framework.SSH(fmt.Sprintf("mount | grep %s", clientPod.UID), nodeIP, framework.TestContext.Provider) result, err = framework.SSH(fmt.Sprintf("mount | grep %s", clientPod.UID), nodeIP, framework.TestContext.Provider)
framework.LogSSHResult(result) framework.LogSSHResult(result)
...@@ -192,6 +202,16 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew ...@@ -192,6 +202,16 @@ func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framew
framework.Logf("Volume unmounted on node %s", clientPod.Spec.NodeName) framework.Logf("Volume unmounted on node %s", clientPod.Spec.NodeName)
} }
// TestVolumeUnmountsFromDeletedPod tests that a volume unmounts if the client pod was deleted while the kubelet was down.
func TestVolumeUnmountsFromDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume) {
TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, pvc, pv, false)
}
// TestVolumeUnmountsFromFoceDeletedPod tests that a volume unmounts if the client pod was forcelly deleted while the kubelet was down.
func TestVolumeUnmountsFromForceDeletedPod(c clientset.Interface, f *framework.Framework, clientPod *v1.Pod, pvc *v1.PersistentVolumeClaim, pv *v1.PersistentVolume) {
TestVolumeUnmountsFromDeletedPodWithForceOption(c, f, clientPod, pvc, pv, true)
}
// RunInPodWithVolume runs a command in a pod with given claim mounted to /mnt directory. // RunInPodWithVolume runs a command in a pod with given claim mounted to /mnt directory.
func RunInPodWithVolume(c clientset.Interface, ns, claimName, command string) { func RunInPodWithVolume(c clientset.Interface, ns, claimName, command string) {
pod := &v1.Pod{ pod := &v1.Pod{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment