//go:build linux package quadlet import ( "fmt" "os" "os/exec" "os/user" "path" "path/filepath" "strconv" "strings" "syscall" "testing" "github.com/stretchr/testify/assert" ) func TestUnitDirs(t *testing.T) { u, err := user.Current() assert.NoError(t, err) uidInt, err := strconv.Atoi(u.Uid) assert.NoError(t, err) if os.Getenv("_UNSHARED") != "true" { unitDirs := GetUnitDirs(false) resolvedUnitDirAdminUser := ResolveUnitDirAdminUser() userLevelFilter := GetUserLevelFilter(resolvedUnitDirAdminUser) rootfulPaths := NewSearchPaths() AppendSubPaths(rootfulPaths, UnitDirTemp, false, userLevelFilter) AppendSubPaths(rootfulPaths, UnitDirAdmin, false, userLevelFilter) AppendSubPaths(rootfulPaths, UnitDirDistro, false, userLevelFilter) assert.Equal(t, rootfulPaths.GetSortedPaths(), unitDirs, "rootful unit dirs should match") configDir, err := os.UserConfigDir() assert.NoError(t, err) rootlessPaths := NewSearchPaths() systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator))) nonNumericFilter := GetNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel) runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR") if found { AppendSubPaths(rootlessPaths, path.Join(runtimeDir, "containers/systemd"), false, nil) } AppendSubPaths(rootlessPaths, path.Join(configDir, "containers/systemd"), false, nil) AppendSubPaths(rootlessPaths, filepath.Join(UnitDirAdmin, "users"), true, nonNumericFilter) AppendSubPaths(rootlessPaths, filepath.Join(UnitDirAdmin, "users", u.Uid), true, userLevelFilter) unitDirs = GetUnitDirs(true) assert.Equal(t, rootlessPaths.GetSortedPaths(), unitDirs, "rootless unit dirs should match") // Test that relative path returns an empty list t.Setenv("QUADLET_UNIT_DIRS", "./relative/path") unitDirs = GetUnitDirs(false) assert.Equal(t, []string{}, unitDirs) name := t.TempDir() t.Setenv("QUADLET_UNIT_DIRS", name) unitDirs = GetUnitDirs(false) assert.Equal(t, []string{name}, unitDirs, "rootful should use environment variable") unitDirs = GetUnitDirs(true) assert.Equal(t, []string{name}, unitDirs, "rootless should use environment variable") symLinkTestBaseDir := t.TempDir() actualDir := filepath.Join(symLinkTestBaseDir, "actual") err = os.Mkdir(actualDir, 0755) assert.NoError(t, err) innerDir := filepath.Join(actualDir, "inner") err = os.Mkdir(innerDir, 0755) assert.NoError(t, err) symlink := filepath.Join(symLinkTestBaseDir, "symlink") err = os.Symlink(actualDir, symlink) assert.NoError(t, err) t.Setenv("QUADLET_UNIT_DIRS", symlink) unitDirs = GetUnitDirs(true) assert.Equal(t, []string{actualDir, innerDir}, unitDirs, "directory resolution should follow symlink") // Make a more elborate test with the following structure: // /linkToDir - real directory to link to // /linkToDir/a - real directory // /linkToDir/b - link to /unitDir/b/a should be ignored // /linkToDir/c - link to /unitDir should be ignored // /unitDir - start from here // /unitDir/a - real directory // /unitDir/a/a - real directory // /unitDir/a/a/a - real directory // /unitDir/b/a - real directory // /unitDir/b/b - link to /unitDir/a/a should be ignored // /unitDir/c - link to /linkToDir createDir := func(path, name string, dirs []string) (string, []string) { dirName := filepath.Join(path, name) assert.NotContains(t, dirs, dirName) err = os.Mkdir(dirName, 0755) assert.NoError(t, err) dirs = append(dirs, dirName) return dirName, dirs } linkDir := func(path, name, target string) { linkName := filepath.Join(path, name) err = os.Symlink(target, linkName) assert.NoError(t, err) } symLinkRecursiveTestBaseDir := t.TempDir() expectedDirs := make([]string, 0) // Create /unitDir unitsDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "unitsDir", expectedDirs) // Create /unitDir/a aDirPath, expectedDirs := createDir(unitsDirPath, "a", expectedDirs) // Create /unitDir/a/a aaDirPath, expectedDirs := createDir(aDirPath, "a", expectedDirs) // Create /unitDir/a/a/a _, expectedDirs = createDir(aaDirPath, "a", expectedDirs) // Create /unitDir/a/b _, expectedDirs = createDir(aDirPath, "b", expectedDirs) // Create /unitDir/b bDirPath, expectedDirs := createDir(unitsDirPath, "b", expectedDirs) // Create /unitDir/b/a baDirPath, expectedDirs := createDir(bDirPath, "a", expectedDirs) // Create /linkToDir linkToDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "linkToDir", expectedDirs) // Create /linkToDir/a _, expectedDirs = createDir(linkToDirPath, "a", expectedDirs) // Link /unitDir/b/b to /unitDir/a/a linkDir(bDirPath, "b", aaDirPath) // Link /linkToDir/b to /unitDir/b/a linkDir(linkToDirPath, "b", baDirPath) // Link /linkToDir/c to /unitDir linkDir(linkToDirPath, "c", unitsDirPath) // Link /unitDir/c to /linkToDir linkDir(unitsDirPath, "c", linkToDirPath) t.Setenv("QUADLET_UNIT_DIRS", unitsDirPath) unitDirs = GetUnitDirs(true) assert.Equal(t, expectedDirs, unitDirs, "directory resolution should follow symlink") // remove the temporary directory at the end of the program defer os.RemoveAll(symLinkTestBaseDir) // because chroot is only available for root, // unshare the namespace and map user to root c := exec.Command("/proc/self/exe", os.Args[1:]...) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr c.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUSER, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 0, HostID: uidInt, Size: 1, }, }, } c.Env = append(os.Environ(), "_UNSHARED=true") err = c.Run() assert.NoError(t, err) } else { fmt.Println(os.Args) symLinkTestBaseDir := t.TempDir() rootF, err := os.Open("/") assert.NoError(t, err) defer rootF.Close() defer func() { err := rootF.Chdir() assert.NoError(t, err) err = syscall.Chroot(".") assert.NoError(t, err) }() err = syscall.Chroot(symLinkTestBaseDir) assert.NoError(t, err) err = os.MkdirAll(UnitDirAdmin, 0755) assert.NoError(t, err) err = os.RemoveAll(UnitDirAdmin) assert.NoError(t, err) createDir := func(path, name string) string { dirName := filepath.Join(path, name) err = os.Mkdir(dirName, 0755) assert.NoError(t, err) return dirName } linkDir := func(path, name, target string) { linkName := filepath.Join(path, name) err = os.Symlink(target, linkName) assert.NoError(t, err) } systemdDir := createDir("/", "systemd") userDir := createDir("/", "users") linkDir(systemdDir, "users", userDir) linkDir(UnitDirAdmin, "", systemdDir) uidDir := createDir(userDir, u.Uid) uidDir2 := createDir(userDir, strconv.Itoa(uidInt+1)) userInternalDir := createDir(userDir, "internal") // Make sure QUADLET_UNIT_DIRS is not set t.Setenv("QUADLET_UNIT_DIRS", "") // Test Rootful unitDirs := GetUnitDirs(false) assert.NotContains(t, unitDirs, userDir, "rootful should not contain rootless") assert.NotContains(t, unitDirs, userInternalDir, "rootful should not contain rootless") // Test Rootless unitDirs = GetUnitDirs(true) assert.NotContains(t, unitDirs, uidDir2, "rootless should not contain other users'") assert.Contains(t, unitDirs, userInternalDir, "rootless should contain sub-directories of users dir") assert.Contains(t, unitDirs, uidDir, "rootless should contain the directory for its UID") } }