mirror of
https://github.com/containers/podman.git
synced 2025-08-24 10:04:57 +08:00

This adds `podman quadlet list`, `podman quadlet install`, `podman quadlet rm` and `podman quadlet print`. Signed-off-by: Matt Heon <mheon@redhat.com> Co-authored-by: flouthoc <flouthoc.git@gmail.com> Signed-off-by: flouthoc <flouthoc.git@gmail.com>
248 lines
7.2 KiB
Go
248 lines
7.2 KiB
Go
package quadlet
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/containers/podman/v5/pkg/logiface"
|
|
)
|
|
|
|
// This returns whether a file has an extension recognized as a valid Quadlet unit type.
|
|
func IsExtSupported(filename string) bool {
|
|
ext := filepath.Ext(filename)
|
|
_, ok := SupportedExtensions[ext]
|
|
return ok
|
|
}
|
|
|
|
// This returns default install paths for .Quadlet files for `rootless` and `root` user.
|
|
// Defaults to `/etc/containers/systemd` for root user and `$XDG_CONFIG_HOME/containers/systemd`
|
|
// for rootless users.
|
|
func GetInstallUnitDirPath(rootless bool) string {
|
|
if rootless {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
logiface.Errorf("Warning: %v", err)
|
|
return ""
|
|
}
|
|
return path.Join(configDir, "containers/systemd")
|
|
}
|
|
return UnitDirAdmin
|
|
}
|
|
|
|
// 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 can live in $XDG_RUNTIME_DIR/containers/systemd, /etc/containers/systemd/users, /etc/containers/systemd/users/$UID, and $XDG_CONFIG_HOME/containers/systemd
|
|
func GetUnitDirs(rootless bool) []string {
|
|
paths := NewSearchPaths()
|
|
|
|
// Allow overriding source dir, this is mainly for the CI tests
|
|
if getDirsFromEnv(paths) {
|
|
return paths.sorted
|
|
}
|
|
|
|
resolvedUnitDirAdminUser := ResolveUnitDirAdminUser()
|
|
userLevelFilter := GetUserLevelFilter(resolvedUnitDirAdminUser)
|
|
|
|
if rootless {
|
|
systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator)))
|
|
nonNumericFilter := GetNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel)
|
|
getRootlessDirs(paths, nonNumericFilter, userLevelFilter)
|
|
} else {
|
|
getRootDirs(paths, userLevelFilter)
|
|
}
|
|
return paths.sorted
|
|
}
|
|
|
|
type searchPaths struct {
|
|
sorted []string
|
|
// map to store paths so we can quickly check if we saw them already and not loop in case of symlinks
|
|
visitedDirs map[string]struct{}
|
|
}
|
|
|
|
func NewSearchPaths() *searchPaths {
|
|
return &searchPaths{
|
|
sorted: make([]string, 0),
|
|
visitedDirs: make(map[string]struct{}, 0),
|
|
}
|
|
}
|
|
|
|
func (s *searchPaths) Add(path string) {
|
|
s.sorted = append(s.sorted, path)
|
|
s.visitedDirs[path] = struct{}{}
|
|
}
|
|
|
|
func (s *searchPaths) GetSortedPaths() []string {
|
|
return s.sorted
|
|
}
|
|
|
|
func (s *searchPaths) Visited(path string) bool {
|
|
_, visited := s.visitedDirs[path]
|
|
return visited
|
|
}
|
|
|
|
func getDirsFromEnv(paths *searchPaths) bool {
|
|
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
|
if len(unitDirsEnv) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, eachUnitDir := range strings.Split(unitDirsEnv, ":") {
|
|
if !filepath.IsAbs(eachUnitDir) {
|
|
logiface.Errorf("%s not a valid file path", eachUnitDir)
|
|
break
|
|
}
|
|
AppendSubPaths(paths, eachUnitDir, false, nil)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func AppendSubPaths(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) {
|
|
resolvedPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
logiface.Debugf("Error occurred resolving path %q: %s", path, err)
|
|
}
|
|
// Despite the failure add the path to the list for logging purposes
|
|
// This is the equivalent of adding the path when info==nil below
|
|
paths.Add(path)
|
|
return
|
|
}
|
|
|
|
if skipPath(paths, resolvedPath, isUserFlag, filterPtr) {
|
|
return
|
|
}
|
|
|
|
// Add the current directory
|
|
paths.Add(resolvedPath)
|
|
|
|
// Read the contents of the directory
|
|
entries, err := os.ReadDir(resolvedPath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
logiface.Debugf("Error occurred walking sub directories %q: %s", path, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Recursively run through the contents of the directory
|
|
for _, entry := range entries {
|
|
fullPath := filepath.Join(resolvedPath, entry.Name())
|
|
AppendSubPaths(paths, fullPath, isUserFlag, filterPtr)
|
|
}
|
|
}
|
|
|
|
func skipPath(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) bool {
|
|
// If the path is already in the map no need to read it again
|
|
if paths.Visited(path) {
|
|
return true
|
|
}
|
|
|
|
// Don't traverse drop-in directories
|
|
if strings.HasSuffix(path, ".d") {
|
|
return true
|
|
}
|
|
|
|
// Check if the directory should be filtered out
|
|
if filterPtr != nil && !filterPtr(path, isUserFlag) {
|
|
return true
|
|
}
|
|
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
logiface.Debugf("Error occurred resolving path %q: %s", path, err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Not a directory nothing to add
|
|
return !stat.IsDir()
|
|
}
|
|
|
|
func ResolveUnitDirAdminUser() string {
|
|
unitDirAdminUser := filepath.Join(UnitDirAdmin, "users")
|
|
var err error
|
|
var resolvedUnitDirAdminUser string
|
|
if resolvedUnitDirAdminUser, err = filepath.EvalSymlinks(unitDirAdminUser); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
logiface.Debugf("Error occurred resolving path %q: %s", unitDirAdminUser, err)
|
|
}
|
|
resolvedUnitDirAdminUser = unitDirAdminUser
|
|
}
|
|
return resolvedUnitDirAdminUser
|
|
}
|
|
|
|
func GetUserLevelFilter(resolvedUnitDirAdminUser string) func(string, bool) bool {
|
|
return func(_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.HasPrefix(_path, resolvedUnitDirAdminUser) {
|
|
if isUserFlag {
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func GetNonNumericFilter(resolvedUnitDirAdminUser string, systemUserDirLevel int) func(string, bool) bool {
|
|
return func(path string, isUserFlag bool) bool {
|
|
// when running in rootless, recursive walk directories that are non numeric
|
|
// ignore sub dirs under the `users` directory which correspond to a user id
|
|
if strings.HasPrefix(path, resolvedUnitDirAdminUser) {
|
|
listDirUserPathLevels := strings.Split(path, string(os.PathSeparator))
|
|
// Make sure to add the base directory
|
|
if len(listDirUserPathLevels) == systemUserDirLevel {
|
|
return true
|
|
}
|
|
if len(listDirUserPathLevels) > systemUserDirLevel {
|
|
if !(regexp.MustCompile(`^[0-9]*$`).MatchString(listDirUserPathLevels[systemUserDirLevel])) {
|
|
return true
|
|
}
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getRootlessDirs(paths *searchPaths, nonNumericFilter, userLevelFilter func(string, bool) bool) {
|
|
runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR")
|
|
if found {
|
|
AppendSubPaths(paths, path.Join(runtimeDir, "containers/systemd"), false, nil)
|
|
}
|
|
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
logiface.Errorf("Warning: %v", err)
|
|
return
|
|
}
|
|
AppendSubPaths(paths, path.Join(configDir, "containers/systemd"), false, nil)
|
|
|
|
u, err := user.Current()
|
|
if err == nil {
|
|
AppendSubPaths(paths, filepath.Join(UnitDirAdmin, "users"), true, nonNumericFilter)
|
|
AppendSubPaths(paths, filepath.Join(UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
|
} else {
|
|
logiface.Errorf("Warning: %v", err)
|
|
// Add the base directory even if the UID was not found
|
|
paths.Add(filepath.Join(UnitDirAdmin, "users"))
|
|
}
|
|
}
|
|
|
|
func getRootDirs(paths *searchPaths, userLevelFilter func(string, bool) bool) {
|
|
AppendSubPaths(paths, UnitDirTemp, false, userLevelFilter)
|
|
AppendSubPaths(paths, UnitDirAdmin, false, userLevelFilter)
|
|
AppendSubPaths(paths, UnitDirDistro, false, nil)
|
|
}
|