mirror of
				https://github.com/containers/podman.git
				synced 2025-10-31 10:00:01 +08:00 
			
		
		
		
	 b4374f2bd9
			
		
	
	b4374f2bd9
	
	
	
		
			
			Reduce the number of top-level packages in ./pkg by moving quadlet packages under ./pkg/systemd. [NO NEW TESTS NEEDED] - no functional change. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
		
			
				
	
	
		
			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")
 | |
| }
 |