package main import ( "errors" "flag" "fmt" "os" "path" "path/filepath" "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...) } } 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 && quadlet.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 var err error serviceName, err = quadlet.GetUnitServiceName(unit) if err != nil { Logf("Error obtaining service name: %v", err) } switch { case strings.HasSuffix(unit.Filename, ".container"): // Prefill resouceNames for .container files. This solves network reusing. resourceName = quadlet.GetContainerResourceName(unit) case strings.HasSuffix(unit.Filename, ".build"): // 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"): containers = make([]string, 0) // Prefill resouceNames for .pod files. // This is requires for referencing the pod from .container files resourceName = quadlet.GetPodResourceName(unit) case strings.HasSuffix(unit.Filename, ".volume"), strings.HasSuffix(unit.Filename, ".kube"), strings.HasSuffix(unit.Filename, ".network"), strings.HasSuffix(unit.Filename, ".image"): // Do nothing for these case. 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 := quadlet.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") }