mirror of
https://github.com/containers/podman.git
synced 2025-05-17 06:59:07 +08:00
783 lines
22 KiB
Go
783 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{}
|
|
)
|
|
|
|
// 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 := quadlet.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)
|
|
// Prefill resouceNames for .pod files.
|
|
// This is requires for referencing the pod from .container files
|
|
resourceName = quadlet.GetPodResourceName(unit)
|
|
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 := quadlet.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, warnings, 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")
|
|
}
|