mirror of
				https://github.com/containers/podman.git
				synced 2025-11-04 08:56:05 +08:00 
			
		
		
		
	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
 | 
						|
}
 |