Files
podman/cmd/quadlet/main.go
Valentin Rothberg 52ae4a2c40 Quadlet: exit 0 when there are no files to process
Quadlet should not exit with failure if no files to process have been
found.  Otherwise, even simple operations such as reloading systemd
will fail as it retriggers generators.

Fixes: #17374
Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
2023-02-07 15:18:03 -05:00

394 lines
9.7 KiB
Go

package main
import (
"errors"
"flag"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"unicode"
"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
noKmsgFlag bool
isUser bool // True if run as quadlet-user-generator executable
dryRun bool // True if -dryrun is used
)
var (
// data saved between logToKmsg calls
noKmsg = false
kmsgFile *os.File
)
var (
void struct{}
supportedExtensions = map[string]struct{}{
".container": void,
".volume": void,
".kube": void,
".network": void,
}
)
// 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 isExtSupported(filename string) bool {
ext := filepath.Ext(filename)
_, ok := supportedExtensions[ext]
return ok
}
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 && isExtSupported(name) {
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("",
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 warnIfAmbigiousName(container *parser.UnitFile) {
imageName, ok := container.Lookup(quadlet.ContainerGroup, quadlet.KeyImage)
if !ok {
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.", container.Filename, imageName)
}
}
func main() {
exitCode := 0
prgname := path.Base(os.Args[0])
isUser = strings.Contains(prgname, "user")
flag.Parse()
if verboseFlag || dryRun {
enableDebug()
}
if noKmsgFlag || dryRun {
noKmsg = true
}
if !dryRun && flag.NArg() < 1 {
Logf("Missing output directory argument")
os.Exit(1)
}
var outputPath string
if !dryRun {
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)
}
if len(units) == 0 {
// containers/podman/issues/17374: exit cleanly but log that we
// had nothing to do
Debugf("No files to parse from %s", sourcePaths)
os.Exit(0)
}
if !dryRun {
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"):
warnIfAmbigiousName(unit)
service, err = quadlet.ConvertContainer(unit, isUser)
case strings.HasSuffix(name, ".volume"):
service, err = quadlet.ConvertVolume(unit, name)
case strings.HasSuffix(name, ".kube"):
service, err = quadlet.ConvertKube(unit, isUser)
case strings.HasSuffix(name, ".network"):
service, err = quadlet.ConvertNetwork(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 dryRun {
data, err := service.ToString()
if err != nil {
Debugf("Error parsing %s\n---\n", service.Path)
exitCode = 1
} else {
fmt.Printf("---%s---\n%s\n", service.Path, data)
}
} else {
if err := generateServiceFile(service); err != nil {
Logf("Error writing '%s'o: %s", service.Path, err)
}
enableServiceFile(outputPath, service)
}
}
}
os.Exit(exitCode)
}
func init() {
flag.BoolVar(&verboseFlag, "v", false, "Print debug information")
flag.BoolVar(&noKmsgFlag, "no-kmsg-log", false, "Don't log to kmsg")
flag.BoolVar(&isUser, "user", false, "Run as systemd user")
flag.BoolVar(&dryRun, "dryrun", false, "run in dryrun mode printing debug information")
}