mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +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"
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@ -33,6 +34,10 @@ var (
|
|||||||
versionFlag bool // True if -version is used
|
versionFlag bool // True if -version is used
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SystemUserDirLevel = 5
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// data saved between logToKmsg calls
|
// data saved between logToKmsg calls
|
||||||
noKmsg = false
|
noKmsg = false
|
||||||
@ -103,28 +108,84 @@ func Debugf(format string, a ...interface{}) {
|
|||||||
func getUnitDirs(rootless bool) []string {
|
func getUnitDirs(rootless bool) []string {
|
||||||
// Allow overriding source dir, this is mainly for the CI tests
|
// Allow overriding source dir, this is mainly for the CI tests
|
||||||
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
||||||
|
dirs := make([]string, 0)
|
||||||
|
|
||||||
if len(unitDirsEnv) > 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 {
|
if rootless {
|
||||||
configDir, err := os.UserConfigDir()
|
configDir, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dirs = append(dirs, path.Join(configDir, "containers/systemd"))
|
dirs = appendSubPaths(dirs, path.Join(configDir, "containers/systemd"), false, nil)
|
||||||
u, err := user.Current()
|
u, err := user.Current()
|
||||||
if err == nil {
|
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 {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
||||||
}
|
}
|
||||||
return append(dirs, filepath.Join(quadlet.UnitDirAdmin, "users"))
|
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 {
|
func isExtSupported(filename string) bool {
|
||||||
|
@ -47,10 +47,9 @@ func TestIsUnambiguousName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnitDirs(t *testing.T) {
|
func TestUnitDirs(t *testing.T) {
|
||||||
rootDirs := []string{
|
rootDirs := []string{}
|
||||||
quadlet.UnitDirAdmin,
|
rootDirs = appendSubPaths(rootDirs, quadlet.UnitDirAdmin, false, userLevelFilter)
|
||||||
quadlet.UnitDirDistro,
|
rootDirs = appendSubPaths(rootDirs, quadlet.UnitDirDistro, false, userLevelFilter)
|
||||||
}
|
|
||||||
unitDirs := getUnitDirs(false)
|
unitDirs := getUnitDirs(false)
|
||||||
assert.Equal(t, unitDirs, rootDirs, "rootful unit dirs should match")
|
assert.Equal(t, unitDirs, rootDirs, "rootful unit dirs should match")
|
||||||
|
|
||||||
@ -59,11 +58,12 @@ func TestUnitDirs(t *testing.T) {
|
|||||||
u, err := user.Current()
|
u, err := user.Current()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
rootlessDirs := []string{
|
rootlessDirs := []string{}
|
||||||
path.Join(configDir, "containers/systemd"),
|
|
||||||
filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid),
|
rootlessDirs = appendSubPaths(rootlessDirs, path.Join(configDir, "containers/systemd"), false, nil)
|
||||||
filepath.Join(quadlet.UnitDirAdmin, "users"),
|
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)
|
unitDirs = getUnitDirs(true)
|
||||||
assert.Equal(t, unitDirs, rootlessDirs, "rootless unit dirs should match")
|
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
|
Quadlet when the login session begins. If the administrator places a Quadlet
|
||||||
file in the /etc/containers/systemd/users/${UID}/ directory, then only the
|
file in the /etc/containers/systemd/users/${UID}/ directory, then only the
|
||||||
user with the matching UID execute the Quadlet when the login
|
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
|
### Enabling unit files
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -15,6 +16,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -1321,3 +1323,89 @@ func useCustomNetworkDir(podmanTest *PodmanTestIntegration, tempdir string) {
|
|||||||
podmanTest.RestartRemoteService()
|
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))
|
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() {
|
It("Should parse a kube file and print it to stdout", func() {
|
||||||
fileName := "basic.kube"
|
fileName := "basic.kube"
|
||||||
testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))
|
testcase := loadQuadletTestcase(filepath.Join("quadlet", fileName))
|
||||||
|
Reference in New Issue
Block a user