kubernetes/pkg/volume/util/subpath/subpath_linux_test.go
kubernetes-prow[bot] 0d4e495e91
Merge pull request #139275 from yuehaii/glusterfs-failure
Fix(kubelet):  pods with subPath mounts stuck in error loop after FUSE/GlusterFS network filesystem disruption
2026-06-23 13:24:36 +00:00

1662 lines
44 KiB
Go

//go:build linux
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package subpath
import (
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"syscall"
"testing"
"k8s.io/klog/v2"
"k8s.io/mount-utils"
)
func removeAll(t *testing.T, dir string) {
t.Helper()
if err := os.RemoveAll(dir); err != nil {
t.Errorf("failed to remove %q: %v", dir, err)
}
}
func TestSafeMakeDir(t *testing.T) {
defaultPerm := os.FileMode(0750) + os.ModeDir
maxPerm := os.FileMode(0777) + os.ModeDir
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) error
path string
checkPath string
perm os.FileMode
expectError bool
}{
{
"directory-does-not-exist",
func(base string) error {
return nil
},
"test/directory",
"test/directory",
defaultPerm,
false,
},
{
"all-created-subpath-directory-with-permissions",
func(base string) error {
return nil
},
"test/directory",
"test",
maxPerm,
false,
},
{
"directory-with-sgid",
func(base string) error {
return nil
},
"test/directory",
"test/directory",
os.FileMode(0777) + os.ModeDir + os.ModeSetgid,
false,
},
{
"directory-with-suid",
func(base string) error {
return nil
},
"test/directory",
"test/directory",
os.FileMode(0777) + os.ModeDir + os.ModeSetuid,
false,
},
{
"directory-with-sticky-bit",
func(base string) error {
return nil
},
"test/directory",
"test/directory",
os.FileMode(0777) + os.ModeDir + os.ModeSticky,
false,
},
{
"directory-exists",
func(base string) error {
return os.MkdirAll(filepath.Join(base, "test/directory"), 0750)
},
"test/directory",
"test/directory",
defaultPerm,
false,
},
{
"create-base",
func(base string) error {
return nil
},
"",
"",
defaultPerm,
false,
},
{
"escape-base-using-dots",
func(base string) error {
return nil
},
"..",
"",
defaultPerm,
true,
},
{
"escape-base-using-dots-2",
func(base string) error {
return nil
},
"test/../../..",
"",
defaultPerm,
true,
},
{
"follow-symlinks",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "destination"), defaultPerm); err != nil {
return err
}
return os.Symlink("destination", filepath.Join(base, "test"))
},
"test/directory",
"destination/directory",
defaultPerm,
false,
},
{
"follow-symlink-loop",
func(base string) error {
return os.Symlink("test", filepath.Join(base, "test"))
},
"test/directory",
"",
defaultPerm,
true,
},
{
"follow-symlink-multiple follow",
func(base string) error {
/* test1/dir points to test2 and test2/dir points to test1 */
if err := os.MkdirAll(filepath.Join(base, "test1"), defaultPerm); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(base, "test2"), defaultPerm); err != nil {
return err
}
if err := os.Symlink(filepath.Join(base, "test2"), filepath.Join(base, "test1/dir")); err != nil {
return err
}
if err := os.Symlink(filepath.Join(base, "test1"), filepath.Join(base, "test2/dir")); err != nil {
return err
}
return nil
},
"test1/dir/dir/dir/dir/dir/dir/dir/foo",
"test2/foo",
defaultPerm,
false,
},
{
"danglink-symlink",
func(base string) error {
return os.Symlink("non-existing", filepath.Join(base, "test"))
},
"test/directory",
"",
defaultPerm,
true,
},
{
"non-directory",
func(base string) error {
return ioutil.WriteFile(filepath.Join(base, "test"), []byte{}, defaultPerm)
},
"test/directory",
"",
defaultPerm,
true,
},
{
"non-directory-final",
func(base string) error {
return ioutil.WriteFile(filepath.Join(base, "test"), []byte{}, defaultPerm)
},
"test",
"",
defaultPerm,
true,
},
{
"escape-with-relative-symlink",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "dir"), defaultPerm); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(base, "exists"), defaultPerm); err != nil {
return err
}
return os.Symlink("../exists", filepath.Join(base, "dir/test"))
},
"dir/test",
"",
defaultPerm,
false,
},
{
"escape-with-relative-symlink-not-exists",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "dir"), defaultPerm); err != nil {
return err
}
return os.Symlink("../not-exists", filepath.Join(base, "dir/test"))
},
"dir/test",
"",
defaultPerm,
true,
},
{
"escape-with-symlink",
func(base string) error {
return os.Symlink("/", filepath.Join(base, "test"))
},
"test/directory",
"",
defaultPerm,
true,
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
base, err := ioutil.TempDir("", "safe-make-dir-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
defer removeAll(t, base)
test.prepare(base)
pathToCreate := filepath.Join(base, test.path)
err = doSafeMakeDir(pathToCreate, base, test.perm)
if err != nil && !test.expectError {
t.Fatal(err)
}
if err != nil {
t.Logf("got error: %s", err)
}
if err == nil && test.expectError {
t.Fatalf("expected error, got none")
}
if test.checkPath != "" {
st, err := os.Stat(filepath.Join(base, test.checkPath))
if err != nil {
t.Fatalf("cannot read path %s", test.checkPath)
}
actualMode := st.Mode()
if actualMode != test.perm {
if actualMode^test.perm == os.ModeSetgid && test.perm&os.ModeSetgid == 0 {
// when TMPDIR is a kubernetes emptydir, the sticky gid bit is set due to fsgroup
t.Logf("masking bit from %o", actualMode)
} else {
t.Errorf("expected permissions %o, got %o (%b)", test.perm, actualMode, test.perm^actualMode)
}
}
}
})
}
}
func TestRemoveEmptyDirs(t *testing.T) {
defaultPerm := os.FileMode(0750)
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) error
// Function that validates directory structure after the test
validate func(base string) error
baseDir string
endDir string
expectError bool
}{
{
name: "all-empty",
prepare: func(base string) error {
return os.MkdirAll(filepath.Join(base, "a/b/c"), defaultPerm)
},
validate: func(base string) error {
return validateDirEmpty(filepath.Join(base, "a"))
},
baseDir: "a",
endDir: "a/b/c",
expectError: false,
},
{
name: "dir-not-empty",
prepare: func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "a/b/c"), defaultPerm); err != nil {
return err
}
return os.Mkdir(filepath.Join(base, "a/b/d"), defaultPerm)
},
validate: func(base string) error {
if err := validateDirNotExists(filepath.Join(base, "a/b/c")); err != nil {
return err
}
return validateDirExists(filepath.Join(base, "a/b"))
},
baseDir: "a",
endDir: "a/b/c",
expectError: false,
},
{
name: "path-not-within-base",
prepare: func(base string) error {
return os.MkdirAll(filepath.Join(base, "a/b/c"), defaultPerm)
},
validate: func(base string) error {
return validateDirExists(filepath.Join(base, "a"))
},
baseDir: "a",
endDir: "b/c",
expectError: true,
},
{
name: "path-already-deleted",
prepare: func(base string) error {
return nil
},
validate: func(base string) error {
return nil
},
baseDir: "a",
endDir: "a/b/c",
expectError: false,
},
{
name: "path-not-dir",
prepare: func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "a/b"), defaultPerm); err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(base, "a/b", "c"), []byte{}, defaultPerm)
},
validate: func(base string) error {
if err := validateDirExists(filepath.Join(base, "a/b")); err != nil {
return err
}
return validateFileExists(filepath.Join(base, "a/b/c"))
},
baseDir: "a",
endDir: "a/b/c",
expectError: true,
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "remove-empty-dirs-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
if err = test.prepare(base); err != nil {
os.RemoveAll(base)
t.Fatalf("failed to prepare test %q: %v", test.name, err.Error())
}
err = removeEmptyDirs(filepath.Join(base, test.baseDir), filepath.Join(base, test.endDir))
if err != nil && !test.expectError {
t.Errorf("test %q failed: %v", test.name, err)
}
if err == nil && test.expectError {
t.Errorf("test %q failed: expected error, got success", test.name)
}
if err = test.validate(base); err != nil {
t.Errorf("test %q failed validation: %v", test.name, err)
}
os.RemoveAll(base)
}
}
func TestCleanSubPaths(t *testing.T) {
defaultPerm := os.FileMode(0750)
testVol := "vol1"
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) ([]mount.MountPoint, error)
// Function that validates directory structure after the test
validate func(base string) error
expectError bool
unmount func(path string) error
}{
{
name: "not-exists",
prepare: func(base string) ([]mount.MountPoint, error) {
return nil, nil
},
validate: func(base string) error {
return nil
},
expectError: false,
},
{
name: "subpath-not-mount",
prepare: func(base string) ([]mount.MountPoint, error) {
return nil, os.MkdirAll(filepath.Join(base, containerSubPathDirectoryName, testVol, "container1", "0"), defaultPerm)
},
validate: func(base string) error {
return validateDirNotExists(filepath.Join(base, containerSubPathDirectoryName))
},
expectError: false,
},
{
name: "subpath-file",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1")
if err := os.MkdirAll(path, defaultPerm); err != nil {
return nil, err
}
return nil, ioutil.WriteFile(filepath.Join(path, "0"), []byte{}, defaultPerm)
},
validate: func(base string) error {
return validateDirNotExists(filepath.Join(base, containerSubPathDirectoryName))
},
expectError: false,
},
{
name: "subpath-container-not-dir",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol)
if err := os.MkdirAll(path, defaultPerm); err != nil {
return nil, err
}
return nil, ioutil.WriteFile(filepath.Join(path, "container1"), []byte{}, defaultPerm)
},
validate: func(base string) error {
return validateDirExists(filepath.Join(base, containerSubPathDirectoryName, testVol))
},
expectError: true,
},
{
name: "subpath-multiple-container-not-dir",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol)
if err := os.MkdirAll(filepath.Join(path, "container1"), defaultPerm); err != nil {
return nil, err
}
return nil, ioutil.WriteFile(filepath.Join(path, "container2"), []byte{}, defaultPerm)
},
validate: func(base string) error {
path := filepath.Join(base, containerSubPathDirectoryName, testVol)
if err := validateDirNotExists(filepath.Join(path, "container1")); err != nil {
return err
}
return validateFileExists(filepath.Join(path, "container2"))
},
expectError: true,
},
{
name: "subpath-mount",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1", "0")
if err := os.MkdirAll(path, defaultPerm); err != nil {
return nil, err
}
mounts := []mount.MountPoint{{Device: "/dev/sdb", Path: path}}
return mounts, nil
},
validate: func(base string) error {
return validateDirNotExists(filepath.Join(base, containerSubPathDirectoryName))
},
},
{
name: "subpath-mount-multiple",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1", "0")
path2 := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1", "1")
path3 := filepath.Join(base, containerSubPathDirectoryName, testVol, "container2", "1")
if err := os.MkdirAll(path, defaultPerm); err != nil {
return nil, err
}
if err := os.MkdirAll(path2, defaultPerm); err != nil {
return nil, err
}
if err := os.MkdirAll(path3, defaultPerm); err != nil {
return nil, err
}
mounts := []mount.MountPoint{
{Device: "/dev/sdb", Path: path},
{Device: "/dev/sdb", Path: path3},
}
return mounts, nil
},
validate: func(base string) error {
return validateDirNotExists(filepath.Join(base, containerSubPathDirectoryName))
},
},
{
name: "subpath-mount-multiple-vols",
prepare: func(base string) ([]mount.MountPoint, error) {
path := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1", "0")
path2 := filepath.Join(base, containerSubPathDirectoryName, "vol2", "container1", "1")
if err := os.MkdirAll(path, defaultPerm); err != nil {
return nil, err
}
if err := os.MkdirAll(path2, defaultPerm); err != nil {
return nil, err
}
mounts := []mount.MountPoint{
{Device: "/dev/sdb", Path: path},
}
return mounts, nil
},
validate: func(base string) error {
baseSubdir := filepath.Join(base, containerSubPathDirectoryName)
if err := validateDirNotExists(filepath.Join(baseSubdir, testVol)); err != nil {
return err
}
return validateDirExists(baseSubdir)
},
},
{
name: "subpath-with-files",
prepare: func(base string) ([]mount.MountPoint, error) {
containerPath := filepath.Join(base, containerSubPathDirectoryName, testVol, "container1")
if err := os.MkdirAll(containerPath, defaultPerm); err != nil {
return nil, err
}
file0 := filepath.Join(containerPath, "0")
if err := ioutil.WriteFile(file0, []byte{}, defaultPerm); err != nil {
return nil, err
}
dir1 := filepath.Join(containerPath, "1")
if err := os.MkdirAll(filepath.Join(dir1, "my-dir-1"), defaultPerm); err != nil {
return nil, err
}
dir2 := filepath.Join(containerPath, "2")
if err := os.MkdirAll(filepath.Join(dir2, "my-dir-2"), defaultPerm); err != nil {
return nil, err
}
file3 := filepath.Join(containerPath, "3")
if err := ioutil.WriteFile(file3, []byte{}, defaultPerm); err != nil {
return nil, err
}
mounts := []mount.MountPoint{
{Device: "/dev/sdb", Path: file0},
{Device: "/dev/sdc", Path: dir1},
{Device: "/dev/sdd", Path: dir2},
{Device: "/dev/sde", Path: file3},
}
return mounts, nil
},
unmount: func(mountpath string) error {
err := filepath.Walk(mountpath, func(path string, info os.FileInfo, _ error) error {
if path == mountpath {
// Skip top level directory
return nil
}
if err := os.Remove(path); err != nil {
return err
}
return filepath.SkipDir
})
if err != nil {
return fmt.Errorf("error processing %s: %s", mountpath, err)
}
return nil
},
validate: func(base string) error {
return validateDirNotExists(filepath.Join(base, containerSubPathDirectoryName))
},
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "clean-subpaths-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
mounts, err := test.prepare(base)
if err != nil {
os.RemoveAll(base)
t.Fatalf("failed to prepare test %q: %v", test.name, err.Error())
}
fm := mount.NewFakeMounter(mounts)
fm.UnmountFunc = test.unmount
err = doCleanSubPaths(fm, base, testVol)
if err != nil && !test.expectError {
t.Errorf("test %q failed: %v", test.name, err)
}
if err == nil && test.expectError {
t.Errorf("test %q failed: expected error, got success", test.name)
}
if err = test.validate(base); err != nil {
t.Errorf("test %q failed validation: %v", test.name, err)
}
os.RemoveAll(base)
}
}
var (
testVol = "vol1"
testPod = "pod0"
testContainer = "container0"
testSubpath = 1
)
func setupFakeMounter(testMounts []string) *mount.FakeMounter {
mounts := []mount.MountPoint{}
for _, mountPoint := range testMounts {
mounts = append(mounts, mount.MountPoint{Device: "/foo", Path: mountPoint})
}
return mount.NewFakeMounter(mounts)
}
func getTestPaths(base string) (string, string) {
return filepath.Join(base, testVol),
filepath.Join(base, testPod, containerSubPathDirectoryName, testVol, testContainer, strconv.Itoa(testSubpath))
}
func TestBindSubPath(t *testing.T) {
defaultPerm := os.FileMode(0750)
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) ([]string, string, string, error)
expectError bool
}{
{
name: "subpath-dir",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "dir0")
return nil, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
expectError: false,
},
{
name: "subpath-dir-symlink",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "dir0")
if err := os.MkdirAll(subpath, defaultPerm); err != nil {
return nil, "", "", err
}
subpathLink := filepath.Join(volpath, "dirLink")
return nil, volpath, subpath, os.Symlink(subpath, subpathLink)
},
expectError: false,
},
{
name: "subpath-file",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "file0")
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
return nil, volpath, subpath, ioutil.WriteFile(subpath, []byte{}, defaultPerm)
},
expectError: false,
},
{
name: "subpath-not-exists",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "file0")
return nil, volpath, subpath, nil
},
expectError: true,
},
{
name: "subpath-outside",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "dir0")
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
return nil, volpath, subpath, os.Symlink(base, subpath)
},
expectError: true,
},
{
name: "subpath-symlink-child-outside",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpathDir := filepath.Join(volpath, "dir0")
subpath := filepath.Join(subpathDir, "child0")
if err := os.MkdirAll(subpathDir, defaultPerm); err != nil {
return nil, "", "", err
}
return nil, volpath, subpath, os.Symlink(base, subpath)
},
expectError: true,
},
{
name: "subpath-child-outside-exists",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpathDir := filepath.Join(volpath, "dir0")
child := filepath.Join(base, "child0")
subpath := filepath.Join(subpathDir, "child0")
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
// touch file outside
if err := ioutil.WriteFile(child, []byte{}, defaultPerm); err != nil {
return nil, "", "", err
}
// create symlink for subpath dir
return nil, volpath, subpath, os.Symlink(base, subpathDir)
},
expectError: true,
},
{
name: "subpath-child-outside-not-exists",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpathDir := filepath.Join(volpath, "dir0")
subpath := filepath.Join(subpathDir, "child0")
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
// create symlink for subpath dir
return nil, volpath, subpath, os.Symlink(base, subpathDir)
},
expectError: true,
},
{
name: "subpath-child-outside-exists-middle-dir-symlink",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpathDir := filepath.Join(volpath, "dir0")
symlinkDir := filepath.Join(subpathDir, "linkDir0")
child := filepath.Join(base, "child0")
subpath := filepath.Join(symlinkDir, "child0")
if err := os.MkdirAll(subpathDir, defaultPerm); err != nil {
return nil, "", "", err
}
// touch file outside
if err := ioutil.WriteFile(child, []byte{}, defaultPerm); err != nil {
return nil, "", "", err
}
// create symlink for middle dir
return nil, volpath, subpath, os.Symlink(base, symlinkDir)
},
expectError: true,
},
{
name: "subpath-backstepping",
prepare: func(base string) ([]string, string, string, error) {
volpath, _ := getTestPaths(base)
subpath := filepath.Join(volpath, "dir0")
symlinkBase := filepath.Join(volpath, "..")
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
// create symlink for subpath
return nil, volpath, subpath, os.Symlink(symlinkBase, subpath)
},
expectError: true,
},
{
name: "subpath-mountdir-already-exists",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
if err := os.MkdirAll(subpathMount, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "dir0")
return nil, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
expectError: false,
},
{
name: "subpath-mount-already-exists",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
// Do NOT list subpathMount as a mount point: the intent of this
// test is that the bind target directory already exists on disk,
// not that a mismatching mount is present. Listing it as mounted
// would trigger the lazyUnmountFn path which fails on a plain dir.
mounts := []string{}
if err := os.MkdirAll(subpathMount, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "dir0")
return mounts, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
expectError: false,
},
{
name: "mount-unix-socket",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
socketFile, socketCreateError := createSocketFile(volpath)
return mounts, volpath, socketFile, socketCreateError
},
expectError: false,
},
{
name: "subpath-mounting-fifo",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
testFifo := filepath.Join(volpath, "mount_test.fifo")
err := syscall.Mkfifo(testFifo, 0)
return mounts, volpath, testFifo, err
},
expectError: false,
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "bind-subpath-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
mounts, volPath, subPath, err := test.prepare(base)
if err != nil {
os.RemoveAll(base)
t.Fatalf("failed to prepare test %q: %v", test.name, err.Error())
}
fm := setupFakeMounter(mounts)
subpath := Subpath{
VolumeMountIndex: testSubpath,
Path: subPath,
VolumeName: testVol,
VolumePath: volPath,
PodDir: filepath.Join(base, "pod0"),
ContainerName: testContainer,
}
_, subpathMount := getTestPaths(base)
bindPathTarget, err := doBindSubPath(fm, subpath)
if test.expectError {
if err == nil {
t.Errorf("test %q failed: expected error, got success", test.name)
}
if bindPathTarget != "" {
t.Errorf("test %q failed: expected empty bindPathTarget, got %v", test.name, bindPathTarget)
}
if err = validateDirNotExists(subpathMount); err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
}
if !test.expectError {
if err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
if bindPathTarget != subpathMount {
t.Errorf("test %q failed: expected bindPathTarget %v, got %v", test.name, subpathMount, bindPathTarget)
}
if err = validateFileExists(subpathMount); err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
}
os.RemoveAll(base)
}
}
func TestSubpath_PrepareSafeSubpath(t *testing.T) {
//complete code
defaultPerm := os.FileMode(0750)
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) ([]string, string, string, error)
modifyMounter func(fm *mount.FakeMounter, bindPathTarget string)
expectError bool
expectAction []mount.FakeAction
mountExists bool
expectDirExist bool
}{
{
name: "subpath-mount-already-exists-with-mismatching-mount",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
if err := os.MkdirAll(subpathMount, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "dir0")
return mounts, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
// lazyUnmountFn is injected via modifyMounter below; mounter.Unmount
// is no longer called for the mismatching-mount path.
modifyMounter: func(fm *mount.FakeMounter, bindPathTarget string) {
origLazyUnmountFn := lazyUnmountFn
lazyUnmountFn = func(path string) error {
lazyUnmountFn = origLazyUnmountFn
return nil
}
},
expectError: false,
expectAction: []mount.FakeAction{},
mountExists: false,
},
{
name: "subpath-mount-already-exists-with-samefile",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
subpathMountRoot := filepath.Dir(subpathMount)
if err := os.MkdirAll(subpathMountRoot, defaultPerm); err != nil {
return nil, "", "", err
}
targetFile, err := os.Create(subpathMount)
if err != nil {
return nil, "", "", err
}
defer targetFile.Close()
if err := os.MkdirAll(volpath, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "file0")
// using hard link to simulate bind mounts
err = os.Link(subpathMount, subpath)
if err != nil {
return nil, "", "", err
}
return mounts, volpath, subpath, nil
},
expectError: false,
expectAction: []mount.FakeAction{},
mountExists: true,
},
{
name: "subpath-mount-corrupted-recovered",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
if err := os.MkdirAll(subpathMount, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "dir0")
return mounts, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
modifyMounter: func(fm *mount.FakeMounter, bindPathTarget string) {
fm.MountCheckErrors = map[string]error{
bindPathTarget: os.NewSyscallError("fake", syscall.ESTALE),
}
},
expectError: false,
expectAction: []mount.FakeAction{{Action: "unmount"}},
mountExists: false,
},
{
name: "subpath-mount-corrupted-unmount-fails",
prepare: func(base string) ([]string, string, string, error) {
volpath, subpathMount := getTestPaths(base)
mounts := []string{subpathMount}
if err := os.MkdirAll(subpathMount, defaultPerm); err != nil {
return nil, "", "", err
}
subpath := filepath.Join(volpath, "dir0")
return mounts, volpath, subpath, os.MkdirAll(subpath, defaultPerm)
},
modifyMounter: func(fm *mount.FakeMounter, bindPathTarget string) {
fm.MountCheckErrors = map[string]error{
bindPathTarget: os.NewSyscallError("fake", syscall.ESTALE),
}
fm.UnmountFunc = func(path string) error {
return fmt.Errorf("unmount failed")
}
},
expectError: true,
expectDirExist: true,
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "bind-subpath-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
defer removeAll(t, base)
mounts, volPath, subPath, err := test.prepare(base)
if err != nil {
os.RemoveAll(base)
t.Fatalf("failed to prepare test %q: %v", test.name, err.Error())
}
fm := setupFakeMounter(mounts)
subpath := Subpath{
VolumeMountIndex: testSubpath,
Path: subPath,
VolumeName: testVol,
VolumePath: volPath,
PodDir: filepath.Join(base, "pod0"),
ContainerName: testContainer,
}
_, subpathMount := getTestPaths(base)
if test.modifyMounter != nil {
test.modifyMounter(fm, subpathMount)
}
bindMountExists, bindPathTarget, err := prepareSubpathTarget(fm, subpath)
if bindMountExists != test.mountExists {
t.Errorf("test %q failed: expected bindMountExists %v, got %v", test.name, test.mountExists, bindMountExists)
}
logActions := fm.GetLog()
if len(test.expectAction) == 0 && len(logActions) > 0 {
t.Errorf("test %q failed: expected no actions, got %v", test.name, logActions)
}
if len(test.expectAction) > 0 {
foundMatchingAction := false
testAction := test.expectAction[0]
for _, action := range logActions {
if action.Action == testAction.Action {
foundMatchingAction = true
break
}
}
if !foundMatchingAction {
t.Errorf("test %q failed: expected action %q, got %v", test.name, testAction.Action, logActions)
}
}
if test.expectError {
if err == nil {
t.Errorf("test %q failed: expected error, got success", test.name)
}
if bindPathTarget != "" {
t.Errorf("test %q failed: expected empty bindPathTarget, got %v", test.name, bindPathTarget)
}
if !test.expectDirExist {
if err = validateDirNotExists(subpathMount); err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
}
}
if !test.expectError {
if err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
if bindPathTarget != subpathMount {
t.Errorf("test %q failed: expected bindPathTarget %v, got %v", test.name, subpathMount, bindPathTarget)
}
if err = validateFileExists(subpathMount); err != nil {
t.Errorf("test %q failed: %v", test.name, err)
}
}
}
}
func TestPrepareSubpathTargetDifferentDevice(t *testing.T) {
defaultPerm := os.FileMode(0750)
base, err := os.MkdirTemp("", "different-device-")
if err != nil {
t.Fatal(err)
}
defer removeAll(t, base)
volPath, subpathMount := getTestPaths(base)
if err := os.MkdirAll(filepath.Dir(subpathMount), defaultPerm); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(volPath, defaultPerm); err != nil {
t.Fatal(err)
}
// Bind-mount target exists as a regular file (simulates an existing bind mount).
if err := os.WriteFile(subpathMount, []byte{}, 0640); err != nil {
t.Fatal(err)
}
// Source is a distinct file, different inode, not a hard-link to the target.
sourceFile := filepath.Join(volPath, "file-different")
if err := os.WriteFile(sourceFile, []byte{}, 0640); err != nil {
t.Fatal(err)
}
// FakeMounter lists subpathMount as a mount point so IsMountPoint returns true.
fm := setupFakeMounter([]string{subpathMount})
subpath := Subpath{
VolumeMountIndex: testSubpath,
Path: sourceFile,
VolumeName: testVol,
VolumePath: volPath,
PodDir: filepath.Join(base, "pod0"),
ContainerName: testContainer,
}
// Inject a spy for lazyUnmountFn; mounter.Unmount must NOT be called.
lazyUnmountCalled := false
origLazyUnmountFn := lazyUnmountFn
defer func() { lazyUnmountFn = origLazyUnmountFn }()
lazyUnmountFn = func(path string) error {
lazyUnmountCalled = true
return nil
}
alreadyMounted, gotTarget, err := prepareSubpathTarget(fm, subpath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if alreadyMounted {
t.Errorf("expected alreadyMounted=false after stale-device remount, got true")
}
if gotTarget != subpathMount {
t.Errorf("expected target %q, got %q", subpathMount, gotTarget)
}
if !lazyUnmountCalled {
t.Errorf("expected lazyUnmountFn to be called when source and bind-target have different inodes/devices")
}
// mounter.Unmount must NOT have been called, only lazyUnmountFn.
if actions := fm.GetLog(); len(actions) != 0 {
t.Errorf("expected no mounter.Unmount actions (must use lazyUnmountFn), got %v", actions)
}
}
func TestSubpathStaleBindMountRemount(t *testing.T) {
defaultPerm := os.FileMode(0750)
setup := func(t *testing.T) (base, volPath, subpathMount, sourceFile string) {
t.Helper()
var err error
base, err = os.MkdirTemp("", "stale-remount-")
if err != nil {
t.Fatal(err)
}
volPath, subpathMount = getTestPaths(base)
if err = os.MkdirAll(filepath.Dir(subpathMount), defaultPerm); err != nil {
t.Fatal(err)
}
if err = os.MkdirAll(volPath, defaultPerm); err != nil {
t.Fatal(err)
}
if err = os.WriteFile(subpathMount, []byte{}, 0640); err != nil {
t.Fatal(err)
}
sourceFile = filepath.Join(volPath, "file-stale")
if err = os.WriteFile(sourceFile, []byte{}, 0640); err != nil {
t.Fatal(err)
}
return
}
t.Run("lazyUnmount-error-propagated", func(t *testing.T) {
base, volPath, subpathMount, sourceFile := setup(t)
defer removeAll(t, base)
fm := setupFakeMounter([]string{subpathMount})
subpath := Subpath{
VolumeMountIndex: testSubpath,
Path: sourceFile,
VolumeName: testVol,
VolumePath: volPath,
PodDir: filepath.Join(base, "pod0"),
ContainerName: testContainer,
}
origLazyUnmountFn := lazyUnmountFn
defer func() { lazyUnmountFn = origLazyUnmountFn }()
lazyUnmountFn = func(string) error {
return fmt.Errorf("MNT_DETACH failed: device busy")
}
_, _, err := prepareSubpathTarget(fm, subpath)
if err == nil {
t.Fatal("expected error when lazyUnmountFn fails, got nil")
}
if !strings.Contains(err.Error(), "MNT_DETACH failed") {
t.Errorf("expected error to contain 'MNT_DETACH failed', got: %v", err)
}
})
t.Run("lazyUnmount-success-bind-mount-recreated", func(t *testing.T) {
base, volPath, subpathMount, sourceFile := setup(t)
defer removeAll(t, base)
fm := setupFakeMounter([]string{subpathMount})
subpath := Subpath{
VolumeMountIndex: testSubpath,
Path: sourceFile,
VolumeName: testVol,
VolumePath: volPath,
PodDir: filepath.Join(base, "pod0"),
ContainerName: testContainer,
}
origLazyUnmountFn := lazyUnmountFn
defer func() { lazyUnmountFn = origLazyUnmountFn }()
lazyUnmountFn = func(string) error { return nil }
alreadyMounted, gotTarget, err := prepareSubpathTarget(fm, subpath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if alreadyMounted {
t.Errorf("expected alreadyMounted=false so bind mount is recreated, got true")
}
if gotTarget != subpathMount {
t.Errorf("expected target %q, got %q", subpathMount, gotTarget)
}
// mounter.Unmount must NOT have been called.
if actions := fm.GetLog(); len(actions) != 0 {
t.Errorf("expected no mounter.Unmount actions, got %v", actions)
}
})
}
func TestSafeOpen(t *testing.T) {
defaultPerm := os.FileMode(0750)
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) error
path string
expectError bool
}{
{
"directory-does-not-exist",
func(base string) error {
return nil
},
"test/directory",
true,
},
{
"directory-exists",
func(base string) error {
return os.MkdirAll(filepath.Join(base, "test/directory"), 0750)
},
"test/directory",
false,
},
{
"escape-base-using-dots",
func(base string) error {
return nil
},
"..",
true,
},
{
"escape-base-using-dots-2",
func(base string) error {
return os.MkdirAll(filepath.Join(base, "test"), 0750)
},
"test/../../..",
true,
},
{
"symlink",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "destination"), defaultPerm); err != nil {
return err
}
return os.Symlink("destination", filepath.Join(base, "test"))
},
"test",
true,
},
{
"symlink-nested",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "dir1/dir2"), defaultPerm); err != nil {
return err
}
return os.Symlink("dir1", filepath.Join(base, "dir1/dir2/test"))
},
"test",
true,
},
{
"symlink-loop",
func(base string) error {
return os.Symlink("test", filepath.Join(base, "test"))
},
"test",
true,
},
{
"symlink-not-exists",
func(base string) error {
return os.Symlink("non-existing", filepath.Join(base, "test"))
},
"test",
true,
},
{
"non-directory",
func(base string) error {
return ioutil.WriteFile(filepath.Join(base, "test"), []byte{}, defaultPerm)
},
"test/directory",
true,
},
{
"non-directory-final",
func(base string) error {
return ioutil.WriteFile(filepath.Join(base, "test"), []byte{}, defaultPerm)
},
"test",
false,
},
{
"escape-with-relative-symlink",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "dir"), defaultPerm); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(base, "exists"), defaultPerm); err != nil {
return err
}
return os.Symlink("../exists", filepath.Join(base, "dir/test"))
},
"dir/test",
true,
},
{
"escape-with-relative-symlink-not-exists",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "dir"), defaultPerm); err != nil {
return err
}
return os.Symlink("../not-exists", filepath.Join(base, "dir/test"))
},
"dir/test",
true,
},
{
"escape-with-symlink",
func(base string) error {
return os.Symlink("/", filepath.Join(base, "test"))
},
"test",
true,
},
{
"mount-unix-socket",
func(base string) error {
socketFile, socketError := createSocketFile(base)
if socketError != nil {
return fmt.Errorf("error preparing socket file %s with %w", socketFile, socketError)
}
return nil
},
"mt.sock",
false,
},
{
"mounting-unix-socket-in-middle",
func(base string) error {
testSocketFile, socketError := createSocketFile(base)
if socketError != nil {
return fmt.Errorf("error preparing socket file %s with %w", testSocketFile, socketError)
}
return nil
},
"mt.sock/bar",
true,
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "safe-open-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
test.prepare(base)
pathToCreate := filepath.Join(base, test.path)
fd, err := doSafeOpen(pathToCreate, base)
if err != nil && !test.expectError {
t.Errorf("test %q: %s", test.name, err)
}
if err != nil {
klog.Infof("got error: %s", err)
}
if err == nil && test.expectError {
t.Errorf("test %q: expected error, got none", test.name)
}
syscall.Close(fd)
os.RemoveAll(base)
}
}
func createSocketFile(socketDir string) (string, error) {
testSocketFile := filepath.Join(socketDir, "mt.sock")
// Switch to volume path and create the socket file
// socket file can not have length of more than 108 character
// and hence we must use relative path
oldDir, _ := os.Getwd()
err := os.Chdir(socketDir)
if err != nil {
return "", err
}
defer func() {
os.Chdir(oldDir)
}()
_, socketCreateError := net.Listen("unix", "mt.sock")
return testSocketFile, socketCreateError
}
func TestFindExistingPrefix(t *testing.T) {
defaultPerm := os.FileMode(0750)
tests := []struct {
name string
// Function that prepares directory structure for the test under given
// base.
prepare func(base string) error
path string
expectedPath string
expectedDirs []string
expectError bool
}{
{
"directory-does-not-exist",
func(base string) error {
return nil
},
"directory",
"",
[]string{"directory"},
false,
},
{
"directory-exists",
func(base string) error {
return os.MkdirAll(filepath.Join(base, "test/directory"), 0750)
},
"test/directory",
"test/directory",
[]string{},
false,
},
{
"follow-symlinks",
func(base string) error {
if err := os.MkdirAll(filepath.Join(base, "destination/directory"), defaultPerm); err != nil {
return err
}
return os.Symlink("destination", filepath.Join(base, "test"))
},
"test/directory",
"test/directory",
[]string{},
false,
},
{
"follow-symlink-loop",
func(base string) error {
return os.Symlink("test", filepath.Join(base, "test"))
},
"test/directory",
"",
nil,
true,
},
{
"follow-symlink-multiple follow",
func(base string) error {
/* test1/dir points to test2 and test2/dir points to test1 */
if err := os.MkdirAll(filepath.Join(base, "test1"), defaultPerm); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(base, "test2"), defaultPerm); err != nil {
return err
}
if err := os.Symlink(filepath.Join(base, "test2"), filepath.Join(base, "test1/dir")); err != nil {
return err
}
if err := os.Symlink(filepath.Join(base, "test1"), filepath.Join(base, "test2/dir")); err != nil {
return err
}
return nil
},
"test1/dir/dir/foo/bar",
"test1/dir/dir",
[]string{"foo", "bar"},
false,
},
{
"danglink-symlink",
func(base string) error {
return os.Symlink("non-existing", filepath.Join(base, "test"))
},
// OS returns IsNotExist error both for dangling symlink and for
// non-existing directory.
"test/directory",
"",
[]string{"test", "directory"},
false,
},
{
"with-fifo-in-middle",
func(base string) error {
testFifo := filepath.Join(base, "mount_test.fifo")
return syscall.Mkfifo(testFifo, 0)
},
"mount_test.fifo/directory",
"",
nil,
true,
},
}
for _, test := range tests {
klog.V(4).Infof("test %q", test.name)
base, err := ioutil.TempDir("", "find-prefix-"+test.name+"-")
if err != nil {
t.Fatal(err.Error())
}
test.prepare(base)
path := filepath.Join(base, test.path)
existingPath, dirs, err := findExistingPrefix(base, path)
if err != nil && !test.expectError {
t.Errorf("test %q: %s", test.name, err)
}
if err != nil {
klog.Infof("got error: %s", err)
}
if err == nil && test.expectError {
t.Errorf("test %q: expected error, got none", test.name)
}
fullExpectedPath := filepath.Join(base, test.expectedPath)
if existingPath != fullExpectedPath {
t.Errorf("test %q: expected path %q, got %q", test.name, fullExpectedPath, existingPath)
}
if !reflect.DeepEqual(dirs, test.expectedDirs) {
t.Errorf("test %q: expected dirs %v, got %v", test.name, test.expectedDirs, dirs)
}
os.RemoveAll(base)
}
}
func validateDirEmpty(dir string) error {
files, err := ioutil.ReadDir(dir)
if err != nil {
return err
}
if len(files) != 0 {
return fmt.Errorf("directory %q is not empty", dir)
}
return nil
}
func validateDirExists(dir string) error {
_, err := ioutil.ReadDir(dir)
if err != nil {
return err
}
return nil
}
func validateDirNotExists(dir string) error {
_, err := ioutil.ReadDir(dir)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
return fmt.Errorf("dir %q still exists", dir)
}
func validateFileExists(file string) error {
if _, err := os.Stat(file); err != nil {
return err
}
return nil
}