Skip to content

Commit

Permalink
quadlet recursively scan for unit files
Browse files Browse the repository at this point in the history
Signed-off-by: Hari Kannan <harikannan512@gmail.com>
  • Loading branch information
harikannan512 committed Jul 20, 2023
1 parent 4315611 commit 413552e
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 16 deletions.
73 changes: 67 additions & 6 deletions cmd/quadlet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/user"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"unicode"
Expand All @@ -33,6 +34,10 @@ var (
versionFlag bool // True if -version is used
)

const (
SystemUserDirLevel = 5
)

var (
// data saved between logToKmsg calls
noKmsg = false
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions cmd/quadlet/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion docs/source/markdown/podman-systemd.unit.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions test/e2e/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bytes"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/url"
Expand All @@ -15,6 +16,7 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"

Expand Down Expand Up @@ -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 changes: 11 additions & 0 deletions test/e2e/quadlet/test_subdir/mysleep.container
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions test/e2e/quadlet/test_subdir/sub_one/mysleep_1.container
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions test/e2e/quadlet/test_subdir/sub_two/mysleep_2.container
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions test/e2e/quadlet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 413552e

Please sign in to comment.