diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index 9a956ad625..bdee9eebab 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "os" + "os/user" "path" "path/filepath" "strings" @@ -98,8 +99,8 @@ func Debugf(format string, a ...interface{}) { // This returns the directories where we read quadlet .container and .volumes from // For system generators these are in /usr/share/containers/systemd (for distro files) // and /etc/containers/systemd (for sysadmin files). -// For user generators these live in $XDG_CONFIG_HOME/containers/systemd -func getUnitDirs(user bool) []string { +// For user generators these can live in /etc/containers/systemd/users, /etc/containers/systemd/users/$UID, and $XDG_CONFIG_HOME/containers/systemd +func getUnitDirs(rootless bool) []string { // Allow overdiding source dir, this is mainly for the CI tests unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS") if len(unitDirsEnv) > 0 { @@ -107,15 +108,23 @@ func getUnitDirs(user bool) []string { } dirs := make([]string, 0) - if user { - if configDir, err := os.UserConfigDir(); err == nil { - dirs = append(dirs, path.Join(configDir, "containers/systemd")) + 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 = append(dirs, filepath.Join(quadlet.UnitDirAdmin, "users")) + u, err := user.Current() + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: %v", err) + return dirs } - } else { - dirs = append(dirs, quadlet.UnitDirAdmin) - dirs = append(dirs, quadlet.UnitDirDistro) + return append(dirs, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid)) } - return dirs + dirs = append(dirs, quadlet.UnitDirAdmin) + return append(dirs, quadlet.UnitDirDistro) } func isExtSupported(filename string) bool { diff --git a/cmd/quadlet/main_test.go b/cmd/quadlet/main_test.go index df12f2277c..3ff1e960f1 100644 --- a/cmd/quadlet/main_test.go +++ b/cmd/quadlet/main_test.go @@ -1,8 +1,13 @@ package main import ( + "os" + "os/user" + "path" + "path/filepath" "testing" + "github.com/containers/podman/v4/pkg/systemd/quadlet" "github.com/stretchr/testify/assert" ) @@ -40,3 +45,38 @@ func TestIsUnambiguousName(t *testing.T) { assert.Equal(t, res, test.res, "%q", test.input) } } + +func TestUnitDirs(t *testing.T) { + rootDirs := []string{ + quadlet.UnitDirAdmin, + quadlet.UnitDirDistro, + } + unitDirs := getUnitDirs(false) + assert.Equal(t, unitDirs, rootDirs, "rootful unit dirs should match") + + configDir, err := os.UserConfigDir() + assert.Nil(t, err) + u, err := user.Current() + assert.Nil(t, err) + + rootlessDirs := []string{ + path.Join(configDir, "containers/systemd"), + filepath.Join(quadlet.UnitDirAdmin, "users"), + filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), + } + + unitDirs = getUnitDirs(true) + assert.Equal(t, unitDirs, rootlessDirs, "rootless unit dirs should match") + + name, err := os.MkdirTemp("", "dir") + assert.Nil(t, err) + // remove the temporary directory at the end of the program + defer os.RemoveAll(name) + + t.Setenv("QUADLET_UNIT_DIRS", name) + unitDirs = getUnitDirs(false) + assert.Equal(t, unitDirs, []string{name}, "rootful should use environment variable") + + unitDirs = getUnitDirs(true) + assert.Equal(t, unitDirs, []string{name}, "rootless should use environment variable") +} diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index e6e70e7409..22e773835f 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -2,7 +2,7 @@ ## NAME -podman\-systemd.unit - systemd units using Podman quadlet +podman\-systemd.unit - systemd units using Podman Quadlet ## SYNOPSIS @@ -15,6 +15,8 @@ podman\-systemd.unit - systemd units using Podman quadlet ### Podman user unit search path + * /etc/containers/systemd/users/ + * /etc/containers/systemd/users/$(UID) * $XDG_CONFIG_HOME/containers/systemd/ * ~/.config/containers/systemd/ @@ -38,6 +40,14 @@ Each file type has a custom section (for example, `[Container]`) that is handled other sections will be passed on untouched, allowing the use of any normal systemd configuration options like dependencies or cgroup limits. +For rootless containers, when administrators place Quadlet files in the +/etc/containers/systemd/users directory, all users' sessions will execute the +Quadlet when the login session begins. If the administrator places a Quadlet +file in the /etc/containers/systemd/user/${UID}/ directory, then only the +user with the matching UID will execute the Quadlet when the login +session gets started. + + ### Enabling unit files The services created by Podman are considered transient by systemd, which means they don't have the same @@ -145,7 +155,7 @@ Adds a device node from the host into the container. The format of this is `HOST-DEVICE[:CONTAINER-DEVICE][:PERMISSIONS]`, where `HOST-DEVICE` is the path of the device node on the host, `CONTAINER-DEVICE` is the path of the device node in the container, and `PERMISSIONS` is a list of permissions combining 'r' for read, -'w' for write, and 'm' for mknod(2). The `-` prefix tells quadlet to add the device +'w' for write, and 'm' for mknod(2). The `-` prefix tells Quadlet to add the device only if it exists on the host. This key can be listed multiple times. @@ -307,7 +317,7 @@ generally has the form `type=TYPE,TYPE-SPECIFIC-OPTION[,...]`. As a special case, for `type=volume` if `source` ends with `.volume`, a Podman named volume called `systemd-$name` will be used as the source, and the generated systemd service will contain a dependency on the `$name-volume.service`. Such a volume can be automatically be lazily -created by using a `$name.volume` quadlet file. +created by using a `$name.volume` Quadlet file. This key can be listed multiple times. @@ -320,7 +330,7 @@ not set up networking in the container. As a special case, if the `name` of the network ends with `.network`, a Podman network called `systemd-$name` will be used, and the generated systemd service will contain a dependency on the `$name-network.service`. Such a network can be automatically -created by using a `$name.network` quadlet file. +created by using a `$name.network` Quadlet file. This key can be listed multiple times. @@ -449,7 +459,7 @@ If `SOURCE-VOLUME` starts with `.`, Quadlet will resolve the path relative to th As a special case, if `SOURCE-VOLUME` ends with `.volume`, a Podman named volume called `systemd-$name` will be used as the source, and the generated systemd service will contain a dependency on the `$name-volume.service`. Such a volume can be automatically be lazily -created by using a `$name.volume` quadlet file. +created by using a `$name.volume` Quadlet file. This key can be listed multiple times. @@ -498,7 +508,7 @@ not set up networking in the container. As a special case, if the `name` of the network ends with `.network`, a Podman network called `systemd-$name` will be used, and the generated systemd service will contain a dependency on the `$name-network.service`. Such a network can be automatically -created by using a `$name.network` quadlet file. +created by using a `$name.network` Quadlet file. This key can be listed multiple times.