Files
podman/cmd/quadlet/main.go
Daniel J Walsh f17479c711 Run codespell on code
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
2022-11-28 15:13:43 -05:00

362 lines
8.9 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
)
var (
// data saved between logToKmsg calls
noKmsg = false
kmsgFile *os.File
)
var (
void struct{}
supportedExtensions = map[string]struct{}{
".container": void,
".volume": void,
".kube": 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 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() {
prgname := path.Base(os.Args[0])
isUser = strings.Contains(prgname, "user")
flag.Parse()
if verboseFlag {
enableDebug()
}
if noKmsgFlag {
noKmsg = true
}
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"):
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)
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")
flag.BoolVar(&noKmsgFlag, "no-kmsg-log", false, "Don't log to kmsg")
}