From 413552e10e35802c79c91162ea5ad69bdd8fea47 Mon Sep 17 00:00:00 2001 From: Hari Kannan Date: Thu, 20 Jul 2023 23:10:28 +0100 Subject: [PATCH] quadlet recursively scan for unit files Signed-off-by: Hari Kannan --- cmd/quadlet/main.go | 73 +++++++++++++-- cmd/quadlet/main_test.go | 18 ++-- docs/source/markdown/podman-systemd.unit.5.md | 4 +- test/e2e/common_test.go | 88 +++++++++++++++++++ .../e2e/quadlet/test_subdir/mysleep.container | 11 +++ .../test_subdir/sub_one/mysleep_1.container | 11 +++ .../test_subdir/sub_two/mysleep_2.container | 11 +++ test/e2e/quadlet_test.go | 22 +++++ 8 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 test/e2e/quadlet/test_subdir/mysleep.container create mode 100644 test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container create mode 100644 test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index 3936df9a93..ae75344ce5 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -8,6 +8,7 @@ import ( "os/user" "path" "path/filepath" + "regexp" "sort" "strings" "unicode" @@ -33,6 +34,10 @@ var ( versionFlag bool // True if -version is used ) +const ( + SystemUserDirLevel = 5 +) + var ( // data saved between logToKmsg calls noKmsg = false @@ -103,28 +108,84 @@ func Debugf(format string, a ...interface{}) { func getUnitDirs(rootless bool) []string { // Allow overriding source dir, this is mainly for the CI tests unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS") + dirs := make([]string, 0) + if len(unitDirsEnv) > 0 { - return strings.Split(unitDirsEnv, ":") + for _, eachUnitDir := range strings.Split(unitDirsEnv, ":") { + if !filepath.IsAbs(eachUnitDir) { + Logf("%s not a valid file path", eachUnitDir) + return nil + } + dirs = appendSubPaths(dirs, eachUnitDir, false, nil) + } + return dirs } - dirs := make([]string, 0) if rootless { configDir, err := os.UserConfigDir() if err != nil { fmt.Fprintf(os.Stderr, "Warning: %v", err) return nil } - dirs = append(dirs, path.Join(configDir, "containers/systemd")) + dirs = appendSubPaths(dirs, path.Join(configDir, "containers/systemd"), false, nil) u, err := user.Current() if err == nil { - dirs = append(dirs, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid)) + dirs = appendSubPaths(dirs, filepath.Join(quadlet.UnitDirAdmin, "users"), true, nonNumericFilter) + dirs = appendSubPaths(dirs, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), true, userLevelFilter) } else { fmt.Fprintf(os.Stderr, "Warning: %v", err) } return append(dirs, filepath.Join(quadlet.UnitDirAdmin, "users")) } - dirs = append(dirs, quadlet.UnitDirAdmin) - return append(dirs, quadlet.UnitDirDistro) + + dirs = appendSubPaths(dirs, quadlet.UnitDirAdmin, false, userLevelFilter) + return appendSubPaths(dirs, quadlet.UnitDirDistro, false, nil) +} + +func appendSubPaths(dirs []string, path string, isUserFlag bool, filterPtr func(string, bool) bool) []string { + err := filepath.WalkDir(path, func(_path string, info os.DirEntry, err error) error { + if info == nil || info.IsDir() { + if filterPtr == nil || filterPtr(_path, isUserFlag) { + dirs = append(dirs, _path) + } + } + return err + }) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + Debugf("Error occurred walking sub directories %q: %s", path, err) + } + } + return dirs +} + +func nonNumericFilter(_path string, isUserFlag bool) bool { + // when running in rootless, only recrusive walk directories that are non numeric + // ignore sub dirs under the user directory that may correspond to a user id + if strings.Contains(_path, filepath.Join(quadlet.UnitDirAdmin, "users")) { + listDirUserPathLevels := strings.Split(_path, string(os.PathSeparator)) + if len(listDirUserPathLevels) > SystemUserDirLevel { + if !(regexp.MustCompile(`^[0-9]*$`).MatchString(listDirUserPathLevels[SystemUserDirLevel])) { + return true + } + } + } else { + return true + } + return false +} + +func userLevelFilter(_path string, isUserFlag bool) bool { + // if quadlet generator is run rootless, do not recurse other user sub dirs + // if quadlet generator is run as root, ignore users sub dirs + if strings.Contains(_path, filepath.Join(quadlet.UnitDirAdmin, "users")) { + if isUserFlag { + return true + } + } else { + return true + } + return false } func isExtSupported(filename string) bool { diff --git a/cmd/quadlet/main_test.go b/cmd/quadlet/main_test.go index 709b252d17..9ee2ea00cd 100644 --- a/cmd/quadlet/main_test.go +++ b/cmd/quadlet/main_test.go @@ -47,10 +47,9 @@ func TestIsUnambiguousName(t *testing.T) { } func TestUnitDirs(t *testing.T) { - rootDirs := []string{ - quadlet.UnitDirAdmin, - quadlet.UnitDirDistro, - } + rootDirs := []string{} + rootDirs = appendSubPaths(rootDirs, quadlet.UnitDirAdmin, false, userLevelFilter) + rootDirs = appendSubPaths(rootDirs, quadlet.UnitDirDistro, false, userLevelFilter) unitDirs := getUnitDirs(false) assert.Equal(t, unitDirs, rootDirs, "rootful unit dirs should match") @@ -59,11 +58,12 @@ func TestUnitDirs(t *testing.T) { u, err := user.Current() assert.Nil(t, err) - rootlessDirs := []string{ - path.Join(configDir, "containers/systemd"), - filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), - filepath.Join(quadlet.UnitDirAdmin, "users"), - } + rootlessDirs := []string{} + + rootlessDirs = appendSubPaths(rootlessDirs, path.Join(configDir, "containers/systemd"), false, nil) + rootlessDirs = appendSubPaths(rootlessDirs, filepath.Join(quadlet.UnitDirAdmin, "users"), true, nonNumericFilter) + rootlessDirs = appendSubPaths(rootlessDirs, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), true, userLevelFilter) + rootlessDirs = append(rootlessDirs, filepath.Join(quadlet.UnitDirAdmin, "users")) unitDirs = getUnitDirs(true) assert.Equal(t, unitDirs, rootlessDirs, "rootless unit dirs should match") diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index ed89f6e53c..76de937349 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -44,7 +44,9 @@ For rootless containers, when administrators place Quadlet files in the Quadlet when the login session begins. If the administrator places a Quadlet file in the /etc/containers/systemd/users/${UID}/ directory, then only the user with the matching UID execute the Quadlet when the login -session gets started. +session gets started. For unit files placed in subdirectories within +/etc/containers/systemd/user/${UID}/ and the other user unit search paths, +Quadlet will recursively search and run the unit files present in these subdirectories. ### Enabling unit files diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 50ef089efe..7901c69372 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -5,6 +5,7 @@ import ( "bytes" "errors" "fmt" + "io" "math/rand" "net" "net/url" @@ -15,6 +16,7 @@ import ( "strconv" "strings" "sync" + "syscall" "testing" "time" @@ -1321,3 +1323,89 @@ func useCustomNetworkDir(podmanTest *PodmanTestIntegration, tempdir string) { podmanTest.RestartRemoteService() } } + +// copy directories recursively from source path to destination path +func CopyDirectory(srcDir, dest string) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(srcDir, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + fileInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("failed to get raw syscall.Stat_t data for %q", sourcePath) + } + + switch fileInfo.Mode() & os.ModeType { + case os.ModeDir: + if err := os.MkdirAll(destPath, 0755); err != nil { + return fmt.Errorf("failed to create directory: %q, error: %q", destPath, err.Error()) + } + if err := CopyDirectory(sourcePath, destPath); err != nil { + return err + } + case os.ModeSymlink: + if err := CopySymLink(sourcePath, destPath); err != nil { + return err + } + default: + if err := Copy(sourcePath, destPath); err != nil { + return err + } + } + + if err := os.Lchown(destPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err + } + + fInfo, err := entry.Info() + if err != nil { + return err + } + + isSymlink := fInfo.Mode()&os.ModeSymlink != 0 + if !isSymlink { + if err := os.Chmod(destPath, fInfo.Mode()); err != nil { + return err + } + } + } + return nil +} + +func Copy(srcFile, dstFile string) error { + out, err := os.Create(dstFile) + if err != nil { + return err + } + + defer out.Close() + + in, err := os.Open(srcFile) + if err != nil { + return err + } + + _, err = io.Copy(out, in) + if err != nil { + return err + } + defer in.Close() + return nil +} + +func CopySymLink(source, dest string) error { + link, err := os.Readlink(source) + if err != nil { + return err + } + return os.Symlink(link, dest) +} diff --git a/test/e2e/quadlet/test_subdir/mysleep.container b/test/e2e/quadlet/test_subdir/mysleep.container new file mode 100644 index 0000000000..975f6c681d --- /dev/null +++ b/test/e2e/quadlet/test_subdir/mysleep.container @@ -0,0 +1,11 @@ +[Unit] +Description=The sleep container +After=local-fs.target + +[Container] +Image=registry.access.redhat.com/ubi9-minimal:latest +Exec=sleep 1000 + +[Install] +# Start by default on boot +WantedBy=multi-user.target default.target diff --git a/test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container b/test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container new file mode 100644 index 0000000000..975f6c681d --- /dev/null +++ b/test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container @@ -0,0 +1,11 @@ +[Unit] +Description=The sleep container +After=local-fs.target + +[Container] +Image=registry.access.redhat.com/ubi9-minimal:latest +Exec=sleep 1000 + +[Install] +# Start by default on boot +WantedBy=multi-user.target default.target diff --git a/test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container b/test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container new file mode 100644 index 0000000000..975f6c681d --- /dev/null +++ b/test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container @@ -0,0 +1,11 @@ +[Unit] +Description=The sleep container +After=local-fs.target + +[Container] +Image=registry.access.redhat.com/ubi9-minimal:latest +Exec=sleep 1000 + +[Install] +# Start by default on boot +WantedBy=multi-user.target default.target diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index 1d2e499d78..7a541cf984 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -471,6 +471,28 @@ BOGUS=foo Expect(session).Should(Exit(1)) }) + It("Should scan and return output for files in subdirectories", func() { + dirName := "test_subdir" + + err = CopyDirectory(filepath.Join("quadlet", dirName), quadletDir) + + if err != nil { + GinkgoWriter.Println("error:", err) + } + + session := podmanTest.Quadlet([]string{"-dryrun", "-user"}, quadletDir) + session.WaitWithDefaultTimeout() + + current := session.OutputToStringArray() + expected := []string{ + "---mysleep.service---", + "---mysleep_1.service---", + "---mysleep_2.service---", + } + + Expect(current).To(ContainElements(expected)) + }) + It("Should parse a kube file and print it to stdout", func() { fileName := "basic.kube" testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))