mirror of
https://github.com/containers/podman.git
synced 2025-05-20 16:47:39 +08:00
quadlet recursively scan for unit files
Signed-off-by: Hari Kannan <harikannan512@gmail.com>
This commit is contained in:
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
11
test/e2e/quadlet/test_subdir/mysleep.container
Normal file
11
test/e2e/quadlet/test_subdir/mysleep.container
Normal file
@ -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
|
11
test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container
Normal file
11
test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container
Normal file
@ -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
|
11
test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container
Normal file
11
test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container
Normal file
@ -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
|
@ -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))
|
||||
|
Reference in New Issue
Block a user