mirror of
https://github.com/containers/podman.git
synced 2025-05-17 23:26:08 +08:00

This adds support for the UpheldBy option in quadlet files. The UpheldBy option is the counterpart to the Upholds option added in systemd v249 and is similar to the existing WantedBy and RequiredBy options. See https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#Upholds=. Signed-off-by: John Schug <john.ips.schug@gmail.com>
791 lines
22 KiB
Go
791 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/containers/podman/v5/pkg/systemd/parser"
|
|
"github.com/containers/podman/v5/pkg/systemd/quadlet"
|
|
"github.com/containers/podman/v5/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
|
|
)
|
|
|
|
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": 4,
|
|
".volume": 2,
|
|
".kube": 4,
|
|
".network": 2,
|
|
".image": 1,
|
|
".build": 3,
|
|
".pod": 5,
|
|
}
|
|
)
|
|
|
|
// 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...)
|
|
}
|
|
}
|
|
|
|
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) Visited(path string) bool {
|
|
_, visited := s.visitedDirs[path]
|
|
return visited
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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) {
|
|
Logf("%s not a valid file path", eachUnitDir)
|
|
break
|
|
}
|
|
appendSubPaths(paths, eachUnitDir, false, nil)
|
|
}
|
|
return true
|
|
}
|
|
|
|
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 {
|
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
|
return
|
|
}
|
|
appendSubPaths(paths, path.Join(configDir, "containers/systemd"), false, nil)
|
|
|
|
u, err := user.Current()
|
|
if err == nil {
|
|
appendSubPaths(paths, filepath.Join(quadlet.UnitDirAdmin, "users"), true, nonNumericFilter)
|
|
appendSubPaths(paths, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
|
// Add the base directory even if the UID was not found
|
|
paths.Add(filepath.Join(quadlet.UnitDirAdmin, "users"))
|
|
}
|
|
}
|
|
|
|
func getRootDirs(paths *searchPaths, userLevelFilter func(string, bool) bool) {
|
|
appendSubPaths(paths, quadlet.UnitDirTemp, false, userLevelFilter)
|
|
appendSubPaths(paths, quadlet.UnitDirAdmin, false, userLevelFilter)
|
|
appendSubPaths(paths, quadlet.UnitDirDistro, false, nil)
|
|
}
|
|
|
|
func resolveUnitDirAdminUser() string {
|
|
unitDirAdminUser := filepath.Join(quadlet.UnitDirAdmin, "users")
|
|
var err error
|
|
var resolvedUnitDirAdminUser string
|
|
if resolvedUnitDirAdminUser, err = filepath.EvalSymlinks(unitDirAdminUser); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
Debugf("Error occurred resolving path %q: %s", unitDirAdminUser, err)
|
|
}
|
|
resolvedUnitDirAdminUser = unitDirAdminUser
|
|
}
|
|
return resolvedUnitDirAdminUser
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
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) {
|
|
Debugf("Error occurred resolving path %q: %s", path, err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Not a directory nothing to add
|
|
return !stat.IsDir()
|
|
}
|
|
|
|
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 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 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 = err
|
|
} else {
|
|
prevError = fmt.Errorf("%s\n%s", prevError, err)
|
|
}
|
|
} else {
|
|
seen[name] = void
|
|
units = append(units, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return units, prevError
|
|
}
|
|
|
|
func loadUnitDropins(unit *parser.UnitFile, sourcePaths []string) error {
|
|
var prevError error
|
|
reportError := func(err error) {
|
|
if prevError != nil {
|
|
err = fmt.Errorf("%s\n%s", prevError, err)
|
|
}
|
|
prevError = err
|
|
}
|
|
|
|
dropinDirs := []string{}
|
|
unitDropinPaths := unit.GetUnitDropinPaths()
|
|
|
|
for _, sourcePath := range sourcePaths {
|
|
for _, dropinPath := range unitDropinPaths {
|
|
dropinDirs = append(dropinDirs, path.Join(sourcePath, dropinPath))
|
|
}
|
|
}
|
|
|
|
var dropinPaths = make(map[string]string)
|
|
for _, dropinDir := range dropinDirs {
|
|
dropinFiles, err := os.ReadDir(dropinDir)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
reportError(fmt.Errorf("error reading directory %q, %w", dropinDir, err))
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
for _, dropinFile := range dropinFiles {
|
|
dropinName := dropinFile.Name()
|
|
if filepath.Ext(dropinName) != ".conf" {
|
|
continue // Only *.conf supported
|
|
}
|
|
|
|
if _, ok := dropinPaths[dropinName]; ok {
|
|
continue // We already saw this name
|
|
}
|
|
|
|
dropinPaths[dropinName] = path.Join(dropinDir, dropinName)
|
|
}
|
|
}
|
|
|
|
dropinFiles := make([]string, len(dropinPaths))
|
|
i := 0
|
|
for k := range dropinPaths {
|
|
dropinFiles[i] = k
|
|
i++
|
|
}
|
|
|
|
// Merge in alpha-numerical order
|
|
sort.Strings(dropinFiles)
|
|
|
|
for _, dropinFile := range dropinFiles {
|
|
dropinPath := dropinPaths[dropinFile]
|
|
|
|
Debugf("Loading source drop-in file %s", dropinPath)
|
|
|
|
if f, err := parser.ParseUnitFile(dropinPath); err != nil {
|
|
reportError(fmt.Errorf("error loading %q, %w", dropinPath, err))
|
|
} else {
|
|
unit.Merge(f)
|
|
}
|
|
}
|
|
|
|
return 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
|
|
}
|
|
|
|
func gatherDependentSymlinks(service *parser.UnitFile, key, dir, filename string) []string {
|
|
symlinks := make([]string, 0)
|
|
groupBy := service.LookupAllStrv(quadlet.InstallGroup, key)
|
|
for _, groupByUnit := range groupBy {
|
|
// Only allow filenames, not paths
|
|
if !strings.Contains(groupByUnit, "/") {
|
|
symlinks = append(symlinks, fmt.Sprintf("%s.%s/%s", groupByUnit, dir, filename))
|
|
}
|
|
}
|
|
return symlinks
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
serviceFilename := service.Filename
|
|
templateBase, templateInstance, isTemplate := service.GetTemplateParts()
|
|
|
|
// For non-instantiated template service we only support installs if a
|
|
// DefaultInstance is given. Otherwise we ignore the Install group, but
|
|
// it is still useful when instantiating the unit via a symlink.
|
|
if isTemplate && templateInstance == "" {
|
|
if defaultInstance, ok := service.Lookup(quadlet.InstallGroup, "DefaultInstance"); ok {
|
|
serviceFilename = templateBase + "@" + defaultInstance + filepath.Ext(serviceFilename)
|
|
} else {
|
|
serviceFilename = ""
|
|
}
|
|
}
|
|
|
|
if serviceFilename != "" {
|
|
symlinks = append(symlinks, gatherDependentSymlinks(service, "WantedBy", "wants", serviceFilename)...)
|
|
symlinks = append(symlinks, gatherDependentSymlinks(service, "RequiredBy", "requires", serviceFilename)...)
|
|
symlinks = append(symlinks, gatherDependentSymlinks(service, "UpheldBy", "upholds", serviceFilename)...)
|
|
}
|
|
|
|
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, ".build") || 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 generateUnitsInfoMap(units []*parser.UnitFile) map[string]*quadlet.UnitInfo {
|
|
unitsInfoMap := make(map[string]*quadlet.UnitInfo)
|
|
for _, unit := range units {
|
|
var serviceName string
|
|
var containers []string
|
|
var resourceName string
|
|
|
|
switch {
|
|
case strings.HasSuffix(unit.Filename, ".container"):
|
|
serviceName = quadlet.GetContainerServiceName(unit)
|
|
// Prefill resouceNames for .container files. This solves network reusing.
|
|
resourceName = quadlet.GetContainerResourceName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".volume"):
|
|
serviceName = quadlet.GetVolumeServiceName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".kube"):
|
|
serviceName = quadlet.GetKubeServiceName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".network"):
|
|
serviceName = quadlet.GetNetworkServiceName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".image"):
|
|
serviceName = quadlet.GetImageServiceName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".build"):
|
|
serviceName = quadlet.GetBuildServiceName(unit)
|
|
// Prefill resouceNames for .build files. This is significantly less complex than
|
|
// pre-computing all resourceNames for all Quadlet types (which is rather complex for a few
|
|
// types), but still breaks the dependency cycle between .volume and .build ([Volume] can
|
|
// have Image=some.build, and [Build] can have Volume=some.volume:/some-volume)
|
|
resourceName = quadlet.GetBuiltImageName(unit)
|
|
case strings.HasSuffix(unit.Filename, ".pod"):
|
|
serviceName = quadlet.GetPodServiceName(unit)
|
|
containers = make([]string, 0)
|
|
default:
|
|
Logf("Unsupported file type %q", unit.Filename)
|
|
continue
|
|
}
|
|
|
|
unitsInfoMap[unit.Filename] = &quadlet.UnitInfo{
|
|
ServiceName: serviceName,
|
|
ContainersToStart: containers,
|
|
ResourceName: resourceName,
|
|
}
|
|
}
|
|
|
|
return unitsInfoMap
|
|
}
|
|
|
|
func main() {
|
|
if processErred := process(); processErred {
|
|
Logf("processing encountered some errors")
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func process() bool {
|
|
var processErred bool
|
|
|
|
prgname := path.Base(os.Args[0])
|
|
isUserFlag = strings.Contains(prgname, "user")
|
|
|
|
flag.Parse()
|
|
|
|
if versionFlag {
|
|
fmt.Printf("%s\n", rawversion.RawVersion)
|
|
return processErred
|
|
}
|
|
|
|
if verboseFlag || dryRunFlag {
|
|
enableDebug()
|
|
}
|
|
|
|
if noKmsgFlag || dryRunFlag {
|
|
noKmsg = true
|
|
}
|
|
|
|
reportError := func(err error) {
|
|
Logf("%s", err.Error())
|
|
processErred = true
|
|
}
|
|
|
|
if !dryRunFlag && flag.NArg() < 1 {
|
|
reportError(errors.New("missing output directory argument"))
|
|
return processErred
|
|
}
|
|
|
|
var outputPath string
|
|
|
|
if !dryRunFlag {
|
|
outputPath = flag.Arg(0)
|
|
|
|
Debugf("Starting quadlet-generator, output to: %s", outputPath)
|
|
}
|
|
|
|
sourcePathsMap := getUnitDirs(isUserFlag)
|
|
|
|
var units []*parser.UnitFile
|
|
for _, d := range sourcePathsMap {
|
|
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", sourcePathsMap)
|
|
return processErred
|
|
}
|
|
|
|
for _, unit := range units {
|
|
if err := loadUnitDropins(unit, sourcePathsMap); err != nil {
|
|
reportError(err)
|
|
}
|
|
}
|
|
|
|
if !dryRunFlag {
|
|
err := os.MkdirAll(outputPath, os.ModePerm)
|
|
if err != nil {
|
|
reportError(err)
|
|
return processErred
|
|
}
|
|
}
|
|
|
|
// 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
|
|
unitsInfoMap := generateUnitsInfoMap(units)
|
|
|
|
for _, unit := range units {
|
|
var service *parser.UnitFile
|
|
var warnings, err error
|
|
|
|
switch {
|
|
case strings.HasSuffix(unit.Filename, ".container"):
|
|
warnIfAmbiguousName(unit, quadlet.ContainerGroup)
|
|
service, warnings, err = quadlet.ConvertContainer(unit, isUserFlag, unitsInfoMap)
|
|
case strings.HasSuffix(unit.Filename, ".volume"):
|
|
warnIfAmbiguousName(unit, quadlet.VolumeGroup)
|
|
service, warnings, err = quadlet.ConvertVolume(unit, unit.Filename, unitsInfoMap, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".kube"):
|
|
service, err = quadlet.ConvertKube(unit, unitsInfoMap, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".network"):
|
|
service, warnings, err = quadlet.ConvertNetwork(unit, unit.Filename, unitsInfoMap, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".image"):
|
|
warnIfAmbiguousName(unit, quadlet.ImageGroup)
|
|
service, err = quadlet.ConvertImage(unit, unitsInfoMap, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".build"):
|
|
service, warnings, err = quadlet.ConvertBuild(unit, unitsInfoMap, isUserFlag)
|
|
case strings.HasSuffix(unit.Filename, ".pod"):
|
|
service, err = quadlet.ConvertPod(unit, unit.Filename, unitsInfoMap, isUserFlag)
|
|
default:
|
|
Logf("Unsupported file type %q", unit.Filename)
|
|
continue
|
|
}
|
|
|
|
if warnings != nil {
|
|
Logf("%s", warnings.Error())
|
|
}
|
|
|
|
if err != nil {
|
|
reportError(fmt.Errorf("converting %q: %w", unit.Filename, err))
|
|
continue
|
|
}
|
|
|
|
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 processErred
|
|
}
|
|
|
|
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")
|
|
}
|