mirror of
				https://github.com/containers/podman.git
				synced 2025-10-31 01:50:50 +08:00 
			
		
		
		
	 72f1617fac
			
		
	
	72f1617fac
	
	
	
		
			
			Moving from Go module v4 to v5 prepares us for public releases. Move done using gomove [1] as with the v3 and v4 moves. [1] https://github.com/KSubedi/gomove Signed-off-by: Matt Heon <mheon@redhat.com>
		
			
				
	
	
		
			426 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			426 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| //go:build !remote
 | |
| 
 | |
| package generate
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"text/template"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/containers/podman/v5/libpod"
 | |
| 	"github.com/containers/podman/v5/pkg/domain/entities"
 | |
| 	"github.com/containers/podman/v5/pkg/systemd/define"
 | |
| 	"github.com/containers/podman/v5/version"
 | |
| 	"github.com/sirupsen/logrus"
 | |
| 	"github.com/spf13/pflag"
 | |
| )
 | |
| 
 | |
| // podInfo contains data required for generating a pod's systemd
 | |
| // unit file.
 | |
| type podInfo struct {
 | |
| 	// ServiceName of the systemd service.
 | |
| 	ServiceName string
 | |
| 	// Name or ID of the infra container.
 | |
| 	InfraNameOrID string
 | |
| 	// StopTimeout sets the timeout Podman waits before killing the container
 | |
| 	// during service stop.
 | |
| 	StopTimeout uint
 | |
| 	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
 | |
| 	RestartPolicy string
 | |
| 	// RestartSec of the systemd unit. Configures the time to sleep before restarting a service.
 | |
| 	RestartSec uint
 | |
| 	// PIDFile of the service. Required for forking services. Must point to the
 | |
| 	// PID of the associated conmon process.
 | |
| 	PIDFile string
 | |
| 	// PodIDFile of the unit.
 | |
| 	PodIDFile string
 | |
| 	// GenerateTimestamp, if set the generated unit file has a time stamp.
 | |
| 	GenerateTimestamp bool
 | |
| 	// RequiredServices are services this service requires. Note that this
 | |
| 	// service runs before them.
 | |
| 	RequiredServices []string
 | |
| 	// PodmanVersion for the header. Will be set internally. Will be auto-filled
 | |
| 	// if left empty.
 | |
| 	PodmanVersion string
 | |
| 	// Executable is the path to the podman executable. Will be auto-filled if
 | |
| 	// left empty.
 | |
| 	Executable string
 | |
| 	// RootFlags contains the root flags which were used to create the container
 | |
| 	// Only used with --new
 | |
| 	RootFlags string
 | |
| 	// TimeStamp at the time of creating the unit file. Will be set internally.
 | |
| 	TimeStamp string
 | |
| 	// CreateCommand is the full command plus arguments of the process the
 | |
| 	// container has been created with.
 | |
| 	CreateCommand []string
 | |
| 	// PodCreateCommand - a post-processed variant of CreateCommand to use
 | |
| 	// when creating the pod.
 | |
| 	PodCreateCommand string
 | |
| 	// EnvVariable is generate.EnvVariable and must not be set.
 | |
| 	EnvVariable string
 | |
| 	// ExecStartPre of the unit.
 | |
| 	ExecStartPre string
 | |
| 	// ExecStart of the unit.
 | |
| 	ExecStart string
 | |
| 	// TimeoutStopSec of the unit.
 | |
| 	TimeoutStopSec uint
 | |
| 	// ExecStop of the unit.
 | |
| 	ExecStop string
 | |
| 	// ExecStopPost of the unit.
 | |
| 	ExecStopPost string
 | |
| 	// Removes autogenerated by Podman and timestamp if set to true
 | |
| 	GenerateNoHeader bool
 | |
| 	// Location of the GraphRoot for the pod.  Required for ensuring the
 | |
| 	// volume has finished mounting when coming online at boot.
 | |
| 	GraphRoot string
 | |
| 	// Location of the RunRoot for the pod.  Required for ensuring the tmpfs
 | |
| 	// or volume exists and is mounted when coming online at boot.
 | |
| 	RunRoot string
 | |
| 	// Add %i and %I to description and execute parts - this should not be used
 | |
| 	IdentifySpecifier bool
 | |
| 	// Wants are the list of services that this service is (weak) dependent on. This
 | |
| 	// option does not influence the order in which services are started or stopped.
 | |
| 	Wants []string
 | |
| 	// After ordering dependencies between the list of services and this service.
 | |
| 	After []string
 | |
| 	// Similar to Wants, but declares a stronger requirement dependency.
 | |
| 	Requires []string
 | |
| }
 | |
| 
 | |
| const podTemplate = headerTemplate + `Wants={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
 | |
| Before={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
 | |
| {{{{- if or .Wants .After .Requires }}}}
 | |
| 
 | |
| # User-defined dependencies
 | |
| {{{{- end}}}}
 | |
| {{{{- if .Wants}}}}
 | |
| Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
 | |
| {{{{- end}}}}
 | |
| {{{{- if .After}}}}
 | |
| After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
 | |
| {{{{- end}}}}
 | |
| {{{{- if .Requires}}}}
 | |
| Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
 | |
| {{{{- end}}}}
 | |
| 
 | |
| [Service]
 | |
| Environment={{{{.EnvVariable}}}}=%n
 | |
| Restart={{{{.RestartPolicy}}}}
 | |
| {{{{- if .RestartSec}}}}
 | |
| RestartSec={{{{.RestartSec}}}}
 | |
| {{{{- end}}}}
 | |
| TimeoutStopSec={{{{.TimeoutStopSec}}}}
 | |
| {{{{- if .ExecStartPre}}}}
 | |
| ExecStartPre={{{{.ExecStartPre}}}}
 | |
| {{{{- end}}}}
 | |
| ExecStart={{{{.ExecStart}}}}
 | |
| ExecStop={{{{.ExecStop}}}}
 | |
| ExecStopPost={{{{.ExecStopPost}}}}
 | |
| PIDFile={{{{.PIDFile}}}}
 | |
| Type=forking
 | |
| 
 | |
| [Install]
 | |
| WantedBy=default.target
 | |
| `
 | |
| 
 | |
| // PodUnits generates systemd units for the specified pod and its containers.
 | |
| // Based on the options, the return value might be the content of all units or
 | |
| // the files they been written to.
 | |
| func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) {
 | |
| 	if options.TemplateUnitFile {
 | |
| 		return nil, errors.New("--template is not supported for pods")
 | |
| 	}
 | |
| 	// Error out if the pod has no infra container, which we require to be the
 | |
| 	// main service.
 | |
| 	if !pod.HasInfraContainer() {
 | |
| 		return nil, fmt.Errorf("generating systemd unit files: Pod %q has no infra container", pod.Name())
 | |
| 	}
 | |
| 
 | |
| 	podInfo, err := generatePodInfo(pod, options)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	infraID, err := pod.InfraContainerID()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Compute the container-dependency graph for the Pod.
 | |
| 	containers, err := pod.AllContainers()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if len(containers) == 0 {
 | |
| 		return nil, fmt.Errorf("generating systemd unit files: Pod %q has no containers", pod.Name())
 | |
| 	}
 | |
| 	graph, err := libpod.BuildContainerGraph(containers)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Traverse the dependency graph and create systemdgen.containerInfo's for
 | |
| 	// each container.
 | |
| 	containerInfos := []*containerInfo{}
 | |
| 	for ctr, dependencies := range graph.DependencyMap() {
 | |
| 		// Skip the infra container as we already generated it.
 | |
| 		if ctr.ID() == infraID {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		ctrInfo, err := generateContainerInfo(ctr, options)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		// Now add the container's dependencies and at the container as a
 | |
| 		// required service of the infra container.
 | |
| 		for _, dep := range dependencies {
 | |
| 			if dep.ID() == infraID {
 | |
| 				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName)
 | |
| 			} else {
 | |
| 				_, serviceName := containerServiceName(dep, options)
 | |
| 				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName)
 | |
| 			}
 | |
| 		}
 | |
| 		podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName)
 | |
| 		containerInfos = append(containerInfos, ctrInfo)
 | |
| 	}
 | |
| 
 | |
| 	units := map[string]string{}
 | |
| 	// Now generate the systemd service for all containers.
 | |
| 	out, err := executePodTemplate(podInfo, options)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	units[podInfo.ServiceName] = out
 | |
| 	for _, info := range containerInfos {
 | |
| 		info.Pod = podInfo
 | |
| 		out, err := executeContainerTemplate(info, options)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		units[info.ServiceName] = out
 | |
| 	}
 | |
| 
 | |
| 	return units, nil
 | |
| }
 | |
| 
 | |
| func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
 | |
| 	// Generate a systemdgen.containerInfo for the infra container. This
 | |
| 	// containerInfo acts as the main service of the pod.
 | |
| 	infraCtr, err := pod.InfraContainer()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not find infra container: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	stopTimeout := infraCtr.StopTimeout()
 | |
| 	if options.StopTimeout != nil {
 | |
| 		stopTimeout = *options.StopTimeout
 | |
| 	}
 | |
| 
 | |
| 	config := infraCtr.Config()
 | |
| 	conmonPidFile := config.ConmonPidFile
 | |
| 	if conmonPidFile == "" {
 | |
| 		return nil, errors.New("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
 | |
| 	}
 | |
| 
 | |
| 	createCommand := pod.CreateCommand()
 | |
| 	if options.New && len(createCommand) == 0 {
 | |
| 		return nil, fmt.Errorf("cannot use --new on pod %q: no create command found", pod.ID())
 | |
| 	}
 | |
| 
 | |
| 	nameOrID := pod.ID()
 | |
| 	ctrNameOrID := infraCtr.ID()
 | |
| 	if options.Name {
 | |
| 		nameOrID = pod.Name()
 | |
| 		ctrNameOrID = infraCtr.Name()
 | |
| 	}
 | |
| 
 | |
| 	serviceName := getServiceName(options.PodPrefix, options.Separator, nameOrID)
 | |
| 
 | |
| 	info := podInfo{
 | |
| 		ServiceName:       serviceName,
 | |
| 		InfraNameOrID:     ctrNameOrID,
 | |
| 		PIDFile:           conmonPidFile,
 | |
| 		StopTimeout:       stopTimeout,
 | |
| 		GenerateTimestamp: true,
 | |
| 		CreateCommand:     createCommand,
 | |
| 		RunRoot:           infraCtr.Runtime().RunRoot(),
 | |
| 	}
 | |
| 	return &info, nil
 | |
| }
 | |
| 
 | |
| // Determine whether the command array includes an exit-policy setting
 | |
| func hasPodExitPolicy(cmd []string) bool {
 | |
| 	for _, arg := range cmd {
 | |
| 		if strings.HasPrefix(arg, "--exit-policy=") || arg == "--exit-policy" {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // executePodTemplate executes the pod template on the specified podInfo.  Note
 | |
| // that the podInfo is also post processed and completed, which allows for an
 | |
| // easier unit testing.
 | |
| func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) {
 | |
| 	info.RestartPolicy = define.DefaultRestartPolicy
 | |
| 	if options.RestartPolicy != nil {
 | |
| 		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		info.RestartPolicy = *options.RestartPolicy
 | |
| 	}
 | |
| 
 | |
| 	if options.RestartSec != nil {
 | |
| 		info.RestartSec = *options.RestartSec
 | |
| 	}
 | |
| 
 | |
| 	// Make sure the executable is set.
 | |
| 	if info.Executable == "" {
 | |
| 		executable, err := os.Executable()
 | |
| 		if err != nil {
 | |
| 			executable = "/usr/bin/podman"
 | |
| 			logrus.Warnf("Could not obtain podman executable location, using default %s: %v", executable, err)
 | |
| 		}
 | |
| 		info.Executable = executable
 | |
| 	}
 | |
| 
 | |
| 	info.EnvVariable = define.EnvVariable
 | |
| 	info.ExecStart = formatOptionsString("{{{{.Executable}}}} start {{{{.InfraNameOrID}}}}")
 | |
| 	info.ExecStop = formatOptionsString("{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}} -t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}")
 | |
| 	info.ExecStopPost = formatOptionsString("{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}} -t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}")
 | |
| 
 | |
| 	// Assemble the ExecStart command when creating a new pod.
 | |
| 	//
 | |
| 	// Note that we cannot catch all corner cases here such that users
 | |
| 	// *must* manually check the generated files.  A pod might have been
 | |
| 	// created via a Python script, which would certainly yield an invalid
 | |
| 	// `info.CreateCommand`.  Hence, we're doing a best effort unit
 | |
| 	// generation and don't try aiming at completeness.
 | |
| 	if options.New {
 | |
| 		info.PIDFile = "%t/" + info.ServiceName + ".pid"
 | |
| 		info.PodIDFile = "%t/" + info.ServiceName + ".pod-id"
 | |
| 
 | |
| 		podCreateIndex := 0
 | |
| 		var podRootArgs, podCreateArgs []string
 | |
| 		switch len(info.CreateCommand) {
 | |
| 		case 0, 1, 2:
 | |
| 			return "", fmt.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
 | |
| 		default:
 | |
| 			// Make sure that pod was created with `pod create` and
 | |
| 			// not something else, such as `run --pod new`.
 | |
| 			for i := 1; i < len(info.CreateCommand); i++ {
 | |
| 				if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" {
 | |
| 					podCreateIndex = i
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			if podCreateIndex == 0 {
 | |
| 				return "", fmt.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
 | |
| 			}
 | |
| 			podRootArgs = info.CreateCommand[1 : podCreateIndex-1]
 | |
| 			info.RootFlags = strings.Join(escapeSystemdArguments(podRootArgs), " ")
 | |
| 			podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:], 0)
 | |
| 		}
 | |
| 		// We're hard-coding the first five arguments and append the
 | |
| 		// CreateCommand with a stripped command and subcommand.
 | |
| 		startCommand := []string{info.Executable}
 | |
| 		startCommand = append(startCommand, podRootArgs...)
 | |
| 		startCommand = append(startCommand,
 | |
| 			"pod", "create",
 | |
| 			"--infra-conmon-pidfile", "{{{{.PIDFile}}}}",
 | |
| 			"--pod-id-file", "{{{{.PodIDFile}}}}")
 | |
| 
 | |
| 		// Presence check for certain flags/options.
 | |
| 		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
 | |
| 		fs.ParseErrorsWhitelist.UnknownFlags = true
 | |
| 		fs.Usage = func() {}
 | |
| 		fs.SetInterspersed(false)
 | |
| 		fs.String("name", "", "")
 | |
| 		fs.Bool("replace", false, "")
 | |
| 		if err := fs.Parse(podCreateArgs); err != nil {
 | |
| 			return "", fmt.Errorf("parsing remaining command-line arguments: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		hasNameParam := fs.Lookup("name").Changed
 | |
| 		hasReplaceParam, err := fs.GetBool("replace")
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		if hasNameParam && !hasReplaceParam {
 | |
| 			if fs.Changed("replace") {
 | |
| 				// this can only happen if --replace=false is set
 | |
| 				// in that case we need to remove it otherwise we
 | |
| 				// would overwrite the previous replace arg to false
 | |
| 				podCreateArgs = removeReplaceArg(podCreateArgs, fs.NArg())
 | |
| 			}
 | |
| 			podCreateArgs = append(podCreateArgs, "--replace")
 | |
| 		}
 | |
| 
 | |
| 		if !hasPodExitPolicy(append(startCommand, podCreateArgs...)) {
 | |
| 			startCommand = append(startCommand, "--exit-policy=stop")
 | |
| 		}
 | |
| 		startCommand = append(startCommand, podCreateArgs...)
 | |
| 		startCommand = escapeSystemdArguments(startCommand)
 | |
| 
 | |
| 		info.ExecStartPre = formatOptions(startCommand)
 | |
| 		info.ExecStart = formatOptionsString("{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod start --pod-id-file {{{{.PodIDFile}}}}")
 | |
| 		info.ExecStop = formatOptionsString("{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod stop --ignore --pod-id-file {{{{.PodIDFile}}}} {{{{if (ge .StopTimeout 0)}}}} -t {{{{.StopTimeout}}}}{{{{end}}}}")
 | |
| 		info.ExecStopPost = formatOptionsString("{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod rm --ignore -f --pod-id-file {{{{.PodIDFile}}}}")
 | |
| 	}
 | |
| 	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout
 | |
| 
 | |
| 	if info.PodmanVersion == "" {
 | |
| 		info.PodmanVersion = version.Version.String()
 | |
| 	}
 | |
| 
 | |
| 	if options.NoHeader {
 | |
| 		info.GenerateNoHeader = true
 | |
| 		info.GenerateTimestamp = false
 | |
| 	}
 | |
| 
 | |
| 	if info.GenerateTimestamp {
 | |
| 		info.TimeStamp = time.Now().Format(time.UnixDate)
 | |
| 	}
 | |
| 
 | |
| 	// Sort the slices to assure a deterministic output.
 | |
| 	sort.Strings(info.RequiredServices)
 | |
| 
 | |
| 	// Generate the template and compile it.
 | |
| 	//
 | |
| 	// Note that we need a two-step generation process to allow for fields
 | |
| 	// embedding other fields.  This way we can replace `A -> B -> C` and
 | |
| 	// make the code easier to maintain at the cost of a slightly slower
 | |
| 	// generation.  That's especially needed for embedding the PID and ID
 | |
| 	// files in other fields which will eventually get replaced in the 2nd
 | |
| 	// template execution.
 | |
| 	templ, err := template.New("pod_template").Delims("{{{{", "}}}}").Parse(podTemplate)
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("parsing systemd service template: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var buf bytes.Buffer
 | |
| 	if err := templ.Execute(&buf, info); err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// Now parse the generated template (i.e., buf) and execute it.
 | |
| 	templ, err = template.New("pod_template").Delims("{{{{", "}}}}").Parse(buf.String())
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("parsing systemd service template: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	buf = bytes.Buffer{}
 | |
| 	if err := templ.Execute(&buf, info); err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return buf.String(), nil
 | |
| }
 |