mirror of
https://github.com/containers/podman.git
synced 2025-12-13 18:37:36 +08:00
Add support for .pod unit files with only PodmanArgs, GlobalArgs, ContainersConfModule and PodName Add support for linking .container units with .pod ones Add e2e and system tests Add to man page Signed-off-by: Ygal Blum <ygal.blum@gmail.com>
565 lines
15 KiB
Go
565 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/containers/podman/v4/pkg/systemd/parser"
|
|
"github.com/containers/podman/v4/pkg/systemd/quadlet"
|
|
"github.com/containers/podman/v4/version/rawversion"
|
|
)
|
|
|
|
// This commandline app is the systemd generator (system and user,
|
|
// decided by the name of the binary).
|
|
|
|
// Generators run at very early startup, so must work in a very
|
|
// limited environment (e.g. no /var, /home, or syslog). See:
|
|
// https://www.freedesktop.org/software/systemd/man/systemd.generator.html#Notes%20about%20writing%20generators
|
|
// for more details.
|
|
|
|
var (
|
|
verboseFlag bool // True if -v passed
|
|
noKmsgFlag bool
|
|
isUserFlag bool // True if run as quadlet-user-generator executable
|
|
dryRunFlag bool // True if -dryrun is used
|
|
versionFlag bool // True if -version is used
|
|
)
|
|
|
|
const (
|
|
SystemUserDirLevel = 5
|
|
)
|
|
|
|
var (
|
|
// data saved between logToKmsg calls
|
|
noKmsg = false
|
|
kmsgFile *os.File
|
|
)
|
|
|
|
var (
|
|
void struct{}
|
|
// Key: Extension
|
|
// Value: Processing order for resource naming dependencies
|
|
supportedExtensions = map[string]int{
|
|
".container": 3,
|
|
".volume": 2,
|
|
".kube": 3,
|
|
".network": 2,
|
|
".image": 1,
|
|
".pod": 4,
|
|
}
|
|
)
|
|
|
|
// We log directly to /dev/kmsg, because that is the only way to get information out
|
|
// of the generator into the system logs.
|
|
func logToKmsg(s string) bool {
|
|
if noKmsg {
|
|
return false
|
|
}
|
|
|
|
if kmsgFile == nil {
|
|
f, err := os.OpenFile("/dev/kmsg", os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
noKmsg = true
|
|
return false
|
|
}
|
|
kmsgFile = f
|
|
}
|
|
|
|
if _, err := kmsgFile.WriteString(s); err != nil {
|
|
kmsgFile.Close()
|
|
kmsgFile = nil
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func Logf(format string, a ...interface{}) {
|
|
s := fmt.Sprintf(format, a...)
|
|
line := fmt.Sprintf("quadlet-generator[%d]: %s", os.Getpid(), s)
|
|
|
|
if !logToKmsg(line) || dryRunFlag {
|
|
fmt.Fprintf(os.Stderr, "%s\n", line)
|
|
os.Stderr.Sync()
|
|
}
|
|
}
|
|
|
|
var debugEnabled = false
|
|
|
|
func enableDebug() {
|
|
debugEnabled = true
|
|
}
|
|
|
|
func Debugf(format string, a ...interface{}) {
|
|
if debugEnabled {
|
|
Logf(format, a...)
|
|
}
|
|
}
|
|
|
|
// 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 /etc/containers/systemd/users, /etc/containers/systemd/users/$UID, and $XDG_CONFIG_HOME/containers/systemd
|
|
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 {
|
|
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
|
|
}
|
|
|
|
if rootless {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
|
return nil
|
|
}
|
|
dirs = appendSubPaths(dirs, path.Join(configDir, "containers/systemd"), false, nil)
|
|
u, err := user.Current()
|
|
if err == nil {
|
|
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 = 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 {
|
|
resolvedPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
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
|
|
dirs = append(dirs, path)
|
|
return dirs
|
|
}
|
|
|
|
err = filepath.WalkDir(resolvedPath, 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, recursive walk directories that are non numeric
|
|
// ignore sub dirs under the `users` directory which 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 {
|
|
ext := filepath.Ext(filename)
|
|
_, ok := supportedExtensions[ext]
|
|
return ok
|
|
}
|
|
|
|
var seen = make(map[string]struct{})
|
|
|
|
func loadUnitsFromDir(sourcePath string) ([]*parser.UnitFile, error) {
|
|
var prevError error
|
|
files, err := os.ReadDir(sourcePath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
return []*parser.UnitFile{}, nil
|
|
}
|
|
|
|
var units []*parser.UnitFile
|
|
|
|
for _, file := range files {
|
|
name := file.Name()
|
|
if _, ok := seen[name]; !ok && isExtSupported(name) {
|
|
path := path.Join(sourcePath, name)
|
|
|
|
Debugf("Loading source unit file %s", path)
|
|
|
|
if f, err := parser.ParseUnitFile(path); err != nil {
|
|
err = fmt.Errorf("error loading %q, %w", path, err)
|
|
if prevError != nil {
|
|
prevError = fmt.Errorf("%s\n%s", prevError, err)
|
|
}
|
|
} else {
|
|
seen[name] = void
|
|
units = append(units, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return units, prevError
|
|
}
|
|
|
|
func generateServiceFile(service *parser.UnitFile) error {
|
|
Debugf("writing %q", service.Path)
|
|
|
|
service.PrependComment("",
|
|
fmt.Sprintf("Automatically generated by %s", os.Args[0]),
|
|
"")
|
|
|
|
f, err := os.Create(service.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
err = service.Write(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = f.Sync()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// This parses the `Install` group of the unit file and creates the required
|
|
// symlinks to get systemd to start the newly generated file as needed.
|
|
// In a traditional setup this is done by "systemctl enable", but that doesn't
|
|
// work for auto-generated files like these.
|
|
func enableServiceFile(outputPath string, service *parser.UnitFile) {
|
|
symlinks := make([]string, 0)
|
|
|
|
aliases := service.LookupAllStrv(quadlet.InstallGroup, "Alias")
|
|
for _, alias := range aliases {
|
|
symlinks = append(symlinks, filepath.Clean(alias))
|
|
}
|
|
|
|
wantedBy := service.LookupAllStrv(quadlet.InstallGroup, "WantedBy")
|
|
for _, wantedByUnit := range wantedBy {
|
|
// Only allow filenames, not paths
|
|
if !strings.Contains(wantedByUnit, "/") {
|
|
symlinks = append(symlinks, fmt.Sprintf("%s.wants/%s", wantedByUnit, service.Filename))
|
|
}
|
|
}
|
|
|
|
requiredBy := service.LookupAllStrv(quadlet.InstallGroup, "RequiredBy")
|
|
for _, requiredByUnit := range requiredBy {
|
|
// Only allow filenames, not paths
|
|
if !strings.Contains(requiredByUnit, "/") {
|
|
symlinks = append(symlinks, fmt.Sprintf("%s.requires/%s", requiredByUnit, service.Filename))
|
|
}
|
|
}
|
|
|
|
for _, symlinkRel := range symlinks {
|
|
target, err := filepath.Rel(path.Dir(symlinkRel), service.Filename)
|
|
if err != nil {
|
|
Logf("Can't create symlink %s: %s", symlinkRel, err)
|
|
continue
|
|
}
|
|
symlinkPath := path.Join(outputPath, symlinkRel)
|
|
|
|
symlinkDir := path.Dir(symlinkPath)
|
|
err = os.MkdirAll(symlinkDir, os.ModePerm)
|
|
if err != nil {
|
|
Logf("Can't create dir %s: %s", symlinkDir, err)
|
|
continue
|
|
}
|
|
|
|
Debugf("Creating symlink %s -> %s", symlinkPath, target)
|
|
_ = os.Remove(symlinkPath) // overwrite existing symlinks
|
|
err = os.Symlink(target, symlinkPath)
|
|
if err != nil {
|
|
Logf("Failed creating symlink %s: %s", symlinkPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func isImageID(imageName string) bool {
|
|
// All sha25:... names are assumed by podman to be fully specified
|
|
if strings.HasPrefix(imageName, "sha256:") {
|
|
return true
|
|
}
|
|
|
|
// However, podman also accepts image ids as pure hex strings,
|
|
// but only those of length 64 are unambiguous image ids
|
|
if len(imageName) != 64 {
|
|
return false
|
|
}
|
|
|
|
for _, c := range imageName {
|
|
if !unicode.Is(unicode.Hex_Digit, c) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isUnambiguousName(imageName string) bool {
|
|
// Fully specified image ids are unambiguous
|
|
if isImageID(imageName) {
|
|
return true
|
|
}
|
|
|
|
// Otherwise we require a fully qualified name
|
|
firstSlash := strings.Index(imageName, "/")
|
|
if firstSlash == -1 {
|
|
// No domain or path, not fully qualified
|
|
return false
|
|
}
|
|
|
|
// What is before the first slash can be a domain or a path
|
|
domain := imageName[:firstSlash]
|
|
|
|
// If its a domain (has dot or port or is "localhost") it is considered fq
|
|
if strings.ContainsAny(domain, ".:") || domain == "localhost" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// warns if input is an ambiguous name, i.e. a partial image id or a short
|
|
// name (i.e. is missing a registry)
|
|
//
|
|
// Examples:
|
|
// - short names: "image:tag", "library/fedora"
|
|
// - fully qualified names: "quay.io/image", "localhost/image:tag",
|
|
// "server.org:5000/lib/image", "sha256:..."
|
|
//
|
|
// We implement a simple version of this from scratch here to avoid
|
|
// a huge dependency in the generator just for a warning.
|
|
func warnIfAmbiguousName(unit *parser.UnitFile, group string) {
|
|
imageName, ok := unit.Lookup(group, quadlet.KeyImage)
|
|
if !ok {
|
|
return
|
|
}
|
|
if strings.HasSuffix(imageName, ".image") {
|
|
return
|
|
}
|
|
if !isUnambiguousName(imageName) {
|
|
Logf("Warning: %s specifies the image \"%s\" which not a fully qualified image name. This is not ideal for performance and security reasons. See the podman-pull manpage discussion of short-name-aliases.conf for details.", unit.Filename, imageName)
|
|
}
|
|
}
|
|
|
|
func generatePodsInfoMap(units []*parser.UnitFile) map[string]*quadlet.PodInfo {
|
|
podsInfoMap := make(map[string]*quadlet.PodInfo)
|
|
for _, unit := range units {
|
|
if !strings.HasSuffix(unit.Filename, ".pod") {
|
|
continue
|
|
}
|
|
|
|
serviceName := quadlet.GetPodServiceName(unit)
|
|
podsInfoMap[unit.Filename] = &quadlet.PodInfo{
|
|
ServiceName: serviceName,
|
|
Containers: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
return podsInfoMap
|
|
}
|
|
|
|
func main() {
|
|
if err := process(); err != nil {
|
|
Logf("%s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func process() error {
|
|
var prevError error
|
|
|
|
prgname := path.Base(os.Args[0])
|
|
isUserFlag = strings.Contains(prgname, "user")
|
|
|
|
flag.Parse()
|
|
|
|
if versionFlag {
|
|
fmt.Printf("%s\n", rawversion.RawVersion)
|
|
return prevError
|
|
}
|
|
|
|
if verboseFlag || dryRunFlag {
|
|
enableDebug()
|
|
}
|
|
|
|
if noKmsgFlag || dryRunFlag {
|
|
noKmsg = true
|
|
}
|
|
|
|
reportError := func(err error) {
|
|
if prevError != nil {
|
|
err = fmt.Errorf("%s\n%s", prevError, err)
|
|
}
|
|
prevError = err
|
|
}
|
|
|
|
if !dryRunFlag && flag.NArg() < 1 {
|
|
reportError(errors.New("missing output directory argument"))
|
|
return prevError
|
|
}
|
|
|
|
var outputPath string
|
|
|
|
if !dryRunFlag {
|
|
outputPath = flag.Arg(0)
|
|
|
|
Debugf("Starting quadlet-generator, output to: %s", outputPath)
|
|
}
|
|
|
|
sourcePaths := getUnitDirs(isUserFlag)
|
|
|
|
var units []*parser.UnitFile
|
|
for _, d := range sourcePaths {
|
|
if result, err := loadUnitsFromDir(d); err != nil {
|
|
reportError(err)
|
|
} else {
|
|
units = append(units, result...)
|
|
}
|
|
}
|
|
|
|
if len(units) == 0 {
|
|
// containers/podman/issues/17374: exit cleanly but log that we
|
|
// had nothing to do
|
|
Debugf("No files parsed from %s", sourcePaths)
|
|
return prevError
|
|
}
|
|
|
|
if !dryRunFlag {
|
|
err := os.MkdirAll(outputPath, os.ModePerm)
|
|
if err != nil {
|
|
reportError(err)
|
|
return prevError
|
|
}
|
|
}
|
|
|
|
// Sort unit files according to potential inter-dependencies, with Volume and Network units
|
|
// taking precedence over all others.
|
|
sort.Slice(units, func(i, j int) bool {
|
|
getOrder := func(i int) int {
|
|
ext := filepath.Ext(units[i].Filename)
|
|
order, ok := supportedExtensions[ext]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return order
|
|
}
|
|
return getOrder(i) < getOrder(j)
|
|
})
|
|
|
|
// Generate the PodsInfoMap to allow containers to link to their pods and add themselves to the pod's containers list
|
|
podsInfoMap := generatePodsInfoMap(units)
|
|
|
|
// A map of network/volume unit file-names, against their calculated names, as needed by Podman.
|
|
var resourceNames = make(map[string]string)
|
|
|
|
for _, unit := range units {
|
|
var service *parser.UnitFile
|
|
var name string
|
|
var err error
|
|
|
|
switch {
|
|
case strings.HasSuffix(unit.Filename, ".container"):
|
|
warnIfAmbiguousName(unit, quadlet.ContainerGroup)
|
|
service, err = quadlet.ConvertContainer(unit, resourceNames, isUserFlag, podsInfoMap)
|
|
case strings.HasSuffix(unit.Filename, ".volume"):
|
|
warnIfAmbiguousName(unit, quadlet.VolumeGroup)
|
|
service, name, err = quadlet.ConvertVolume(unit, unit.Filename, resourceNames)
|
|
case strings.HasSuffix(unit.Filename, ".kube"):
|
|
service, err = quadlet.ConvertKube(unit, resourceNames, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".network"):
|
|
service, name, err = quadlet.ConvertNetwork(unit, unit.Filename)
|
|
case strings.HasSuffix(unit.Filename, ".image"):
|
|
warnIfAmbiguousName(unit, quadlet.ImageGroup)
|
|
service, name, err = quadlet.ConvertImage(unit)
|
|
case strings.HasSuffix(unit.Filename, ".pod"):
|
|
service, err = quadlet.ConvertPod(unit, unit.Filename, podsInfoMap)
|
|
default:
|
|
Logf("Unsupported file type %q", unit.Filename)
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
reportError(fmt.Errorf("converting %q: %w", unit.Filename, err))
|
|
continue
|
|
}
|
|
|
|
if name != "" {
|
|
resourceNames[unit.Filename] = name
|
|
}
|
|
service.Path = path.Join(outputPath, service.Filename)
|
|
|
|
if dryRunFlag {
|
|
data, err := service.ToString()
|
|
if err != nil {
|
|
reportError(fmt.Errorf("parsing %s: %w", service.Path, err))
|
|
continue
|
|
}
|
|
fmt.Printf("---%s---\n%s\n", service.Path, data)
|
|
continue
|
|
}
|
|
if err := generateServiceFile(service); err != nil {
|
|
reportError(fmt.Errorf("generating service file %s: %w", service.Path, err))
|
|
}
|
|
enableServiceFile(outputPath, service)
|
|
}
|
|
return prevError
|
|
}
|
|
|
|
func init() {
|
|
flag.BoolVar(&verboseFlag, "v", false, "Print debug information")
|
|
flag.BoolVar(&noKmsgFlag, "no-kmsg-log", false, "Don't log to kmsg")
|
|
flag.BoolVar(&isUserFlag, "user", false, "Run as systemd user")
|
|
flag.BoolVar(&dryRunFlag, "dryrun", false, "Run in dryrun mode printing debug information")
|
|
flag.BoolVar(&versionFlag, "version", false, "Print version information and exit")
|
|
}
|