Run pod bind mounts in the host mount namespace via nsenter.
Kubelet checks volume mounts from the host namespace, so NodePublishVolume must mkdir and bind-mount the pod target path on the host, not only inside the CSI plugin container. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -106,16 +106,38 @@ func cleanupMountPoint(target string, mounter *mount.SafeFormatAndMount) error {
|
||||
return mount.CleanupMountPoint(target, mounter, true)
|
||||
}
|
||||
|
||||
func hostMount(args ...string) ([]byte, error) {
|
||||
return exec.Command("nsenter", append([]string{"--target", "1", "--mount", "--"}, args...)...).CombinedOutput()
|
||||
}
|
||||
|
||||
func isHostMounted(path string) bool {
|
||||
out, err := hostMount("findmnt", "-n", path)
|
||||
return err == nil && strings.TrimSpace(string(out)) != ""
|
||||
}
|
||||
|
||||
func bindMount(source, target string) error {
|
||||
if !isMounted(source) {
|
||||
if !isHostMounted(source) && !isMounted(source) {
|
||||
return fmt.Errorf("staging path %s is not mounted", source)
|
||||
}
|
||||
if isMounted(target) {
|
||||
if isHostMounted(target) {
|
||||
return nil
|
||||
}
|
||||
mounter := mount.New("")
|
||||
if err := mounter.Mount(source, target, "", []string{"bind"}); err != nil {
|
||||
return fmt.Errorf("bind mount %s -> %s: %w", source, target, err)
|
||||
if out, err := hostMount("mkdir", "-p", target); err != nil {
|
||||
return fmt.Errorf("create bind mount target %s: %w: %s", target, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
if out, err := hostMount("mount", "--bind", source, target); err != nil {
|
||||
return fmt.Errorf("bind mount %s -> %s: %w: %s", source, target, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmountHost(path string) error {
|
||||
if !isHostMounted(path) {
|
||||
return nil
|
||||
}
|
||||
out, err := hostMount("umount", path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("umount %s: %w: %s", path, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,45 +4,23 @@ package driver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBindMountCreatesTargetAndIsIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
source := filepath.Join(root, "globalmount")
|
||||
target := filepath.Join(root, "pods", "test", "mount")
|
||||
if err := os.MkdirAll(source, 0o755); err != nil {
|
||||
t.Fatalf("mkdir source: %v", err)
|
||||
func TestHostMountRequiresNsenter(t *testing.T) {
|
||||
if _, err := exec.LookPath("nsenter"); err != nil {
|
||||
t.Skip("nsenter not available")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(source, "probe"), []byte("ok"), 0o644); err != nil {
|
||||
t.Fatalf("write probe file: %v", err)
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("host mount namespace tests require root")
|
||||
}
|
||||
|
||||
mounter := newMounter()
|
||||
if err := mounter.Mount("tmpfs", source, "tmpfs", []string{}); err != nil {
|
||||
t.Fatalf("mount tmpfs at source: %v", err)
|
||||
tmp := t.TempDir()
|
||||
if out, err := hostMount("mkdir", "-p", tmp); err != nil {
|
||||
t.Fatalf("hostMount mkdir failed: %v: %s", err, string(out))
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cleanupMountPoint(source, mounter)
|
||||
})
|
||||
|
||||
if err := bindMount(source, target); err != nil {
|
||||
t.Fatalf("first bindMount failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cleanupMountPoint(target, mounter)
|
||||
})
|
||||
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("target mount path missing: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(target, "probe")); err != nil {
|
||||
t.Fatalf("bind mount did not expose source contents: %v", err)
|
||||
}
|
||||
if err := bindMount(source, target); err != nil {
|
||||
t.Fatalf("second bindMount failed: %v", err)
|
||||
if _, err := os.Stat(tmp); err != nil {
|
||||
t.Fatalf("expected host-visible directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ func (s *NodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstage
|
||||
}
|
||||
|
||||
mounter := newMounter()
|
||||
if err := unmountHost(req.GetStagingTargetPath()); err != nil {
|
||||
return nil, internalError(err)
|
||||
}
|
||||
if err := cleanupMountPoint(req.GetStagingTargetPath(), mounter); err != nil {
|
||||
return nil, internalError(err)
|
||||
}
|
||||
@@ -90,7 +93,7 @@ func (s *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublish
|
||||
return nil, invalidArgument("staging target path is required")
|
||||
}
|
||||
|
||||
if !isMounted(req.GetStagingTargetPath()) {
|
||||
if !isMounted(req.GetStagingTargetPath()) && !isHostMounted(req.GetStagingTargetPath()) {
|
||||
return nil, internalError(fmt.Errorf("staging path %s is not mounted", req.GetStagingTargetPath()))
|
||||
}
|
||||
|
||||
@@ -111,6 +114,9 @@ func (s *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpub
|
||||
}
|
||||
|
||||
mounter := newMounter()
|
||||
if err := unmountHost(req.GetTargetPath()); err != nil {
|
||||
return nil, internalError(err)
|
||||
}
|
||||
if err := cleanupMountPoint(req.GetTargetPath(), mounter); err != nil {
|
||||
return nil, internalError(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user