mirror of https://github.com/containers/podman.git
274 lines
6.5 KiB
Go
274 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/containers/podman/v4/pkg/systemd/parser"
|
|
"github.com/containers/podman/v4/pkg/systemd/quadlet"
|
|
)
|
|
|
|
// 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
|
|
isUser bool // True if run as quadlet-user-generator executable
|
|
)
|
|
|
|
var (
|
|
// data saved between logToKmsg calls
|
|
noKmsg = false
|
|
kmsgFile *os.File
|
|
)
|
|
|
|
// 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.Write([]byte(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) {
|
|
// If we can't log, print to stderr
|
|
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 live in $XDG_CONFIG_HOME/containers/systemd
|
|
func getUnitDirs(user bool) []string {
|
|
// Allow overdiding source dir, this is mainly for the CI tests
|
|
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
|
if len(unitDirsEnv) > 0 {
|
|
return strings.Split(unitDirsEnv, ":")
|
|
}
|
|
|
|
dirs := make([]string, 0)
|
|
if user {
|
|
if configDir, err := os.UserConfigDir(); err == nil {
|
|
dirs = append(dirs, path.Join(configDir, "containers/systemd"))
|
|
}
|
|
} else {
|
|
dirs = append(dirs, quadlet.UnitDirAdmin)
|
|
dirs = append(dirs, quadlet.UnitDirDistro)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
func loadUnitsFromDir(sourcePath string, units map[string]*parser.UnitFile) {
|
|
files, err := os.ReadDir(sourcePath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
Logf("Can't read \"%s\": %s", sourcePath, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, file := range files {
|
|
name := file.Name()
|
|
if units[name] == nil &&
|
|
(strings.HasSuffix(name, ".container") ||
|
|
strings.HasSuffix(name, ".volume")) {
|
|
path := path.Join(sourcePath, name)
|
|
|
|
Debugf("Loading source unit file %s", path)
|
|
|
|
if f, err := parser.ParseUnitFile(path); err != nil {
|
|
Logf("Error loading '%s', ignoring: %s", path, err)
|
|
} else {
|
|
units[name] = f
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func generateServiceFile(service *parser.UnitFile) error {
|
|
Debugf("writing '%s'", service.Path)
|
|
|
|
service.PrependComment("",
|
|
"Automatically generated by quadlet-generator",
|
|
"")
|
|
|
|
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 main() {
|
|
prgname := path.Base(os.Args[0])
|
|
isUser = strings.Contains(prgname, "user")
|
|
|
|
flag.Parse()
|
|
|
|
if verboseFlag {
|
|
enableDebug()
|
|
}
|
|
|
|
if flag.NArg() < 1 {
|
|
Logf("Missing output directory argument")
|
|
os.Exit(1)
|
|
}
|
|
|
|
outputPath := flag.Arg(0)
|
|
|
|
Debugf("Starting quadlet-generator, output to: %s", outputPath)
|
|
|
|
sourcePaths := getUnitDirs(isUser)
|
|
|
|
units := make(map[string]*parser.UnitFile)
|
|
for _, d := range sourcePaths {
|
|
loadUnitsFromDir(d, units)
|
|
}
|
|
|
|
err := os.MkdirAll(outputPath, os.ModePerm)
|
|
if err != nil {
|
|
Logf("Can't create dir %s: %s", outputPath, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for name, unit := range units {
|
|
var service *parser.UnitFile
|
|
var err error
|
|
|
|
switch {
|
|
case strings.HasSuffix(name, ".container"):
|
|
service, err = quadlet.ConvertContainer(unit, isUser)
|
|
case strings.HasSuffix(name, ".volume"):
|
|
service, err = quadlet.ConvertVolume(unit, name)
|
|
default:
|
|
Logf("Unsupported file type '%s'", name)
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
Logf("Error converting '%s', ignoring: %s", name, err)
|
|
} else {
|
|
service.Path = path.Join(outputPath, service.Filename)
|
|
|
|
if err := generateServiceFile(service); err != nil {
|
|
Logf("Error writing '%s'o: %s", service.Path, err)
|
|
}
|
|
enableServiceFile(outputPath, service)
|
|
}
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
flag.BoolVar(&verboseFlag, "v", false, "Print debug information")
|
|
}
|