package generate import ( "bytes" "errors" "fmt" "os" "sort" "strings" "text/template" "time" "github.com/containers/podman/v4/libpod" libpodDefine "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/systemd/define" "github.com/containers/podman/v4/version" "github.com/sirupsen/logrus" "github.com/spf13/pflag" ) // containerInfo contains data required for generating a container's systemd // unit file. type containerInfo struct { // ServiceName of the systemd service. ServiceName string // Name or ID of the container. ContainerNameOrID string // Type of the unit. Type string // NotifyAccess of the unit. NotifyAccess 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 // Custom number of restart attempts. StartLimitBurst string // PIDFile of the service. Required for forking services. Must point to the // PID of the associated conmon process. PIDFile string // ContainerIDFile to be used in the unit. ContainerIDFile string // GenerateTimestamp, if set the generated unit file has a time stamp. GenerateTimestamp bool // BoundToServices are the services this service binds to. Note that this // service runs after them. BoundToServices []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 // containerEnv stores the container environment variables containerEnv []string // ExtraEnvs contains the container environment variables referenced // by only the key in the container create command, e.g. --env FOO. // This is only used with --new ExtraEnvs []string // EnvVariable is generate.EnvVariable and must not be set. EnvVariable string // ExecStartPre of the unit. ExecStartPre string // ExecStart of the unit. ExecStart string // TimeoutStartSec of the unit. TimeoutStartSec uint // 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 // If not nil, the container is part of the pod. We can use the // podInfo to extract the relevant data. Pod *podInfo // Location of the GraphRoot for the container. Required for ensuring the // volume has finished mounting when coming online at boot. GraphRoot string // Location of the RunRoot for the container. 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 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 containerTemplate = headerTemplate + ` {{{{- if .BoundToServices}}}} BindsTo={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} After={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} {{{{- 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{{{{- if (eq .IdentifySpecifier true) }}}}-%i{{{{- end}}}} {{{{- if .ExtraEnvs}}}} Environment={{{{- range $index, $value := .ExtraEnvs -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} {{{{- end}}}} Restart={{{{.RestartPolicy}}}} {{{{- if .StartLimitBurst}}}} StartLimitBurst={{{{.StartLimitBurst}}}} {{{{- end}}}} {{{{- if ne .TimeoutStartSec 0}}}} TimeoutStartSec={{{{.TimeoutStartSec}}}} {{{{- end}}}} TimeoutStopSec={{{{.TimeoutStopSec}}}} {{{{- if .ExecStartPre}}}} ExecStartPre={{{{.ExecStartPre}}}} {{{{- end}}}} ExecStart={{{{.ExecStart}}}} {{{{- if .ExecStop}}}} ExecStop={{{{.ExecStop}}}} {{{{- end}}}} {{{{- if .ExecStopPost}}}} ExecStopPost={{{{.ExecStopPost}}}} {{{{- end}}}} {{{{- if .PIDFile}}}} PIDFile={{{{.PIDFile}}}} {{{{- end}}}} Type={{{{.Type}}}} {{{{- if .NotifyAccess}}}} NotifyAccess={{{{.NotifyAccess}}}} {{{{- end}}}} [Install] WantedBy=default.target ` // ContainerUnit generates a systemd unit for the specified container. Based // on the options, the return value might be the entire unit or a file it has // been written to. func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) { info, err := generateContainerInfo(ctr, options) if err != nil { return "", "", err } content, err := executeContainerTemplate(info, options) if err != nil { return "", "", err } return info.ServiceName, content, nil } func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) { stopTimeout := ctr.StopTimeout() if options.StopTimeout != nil { stopTimeout = *options.StopTimeout } startTimeout := uint(0) if options.StartTimeout != nil { startTimeout = *options.StartTimeout } config := ctr.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 := []string{} if config.CreateCommand != nil { createCommand = config.CreateCommand } else if options.New { return nil, fmt.Errorf("cannot use --new on container %q: no create command found: only works on containers created directly with podman but not via REST API", ctr.ID()) } nameOrID, serviceName := containerServiceName(ctr, options) var runRoot string if options.New { runRoot = "%t/containers" } else { runRoot = ctr.Runtime().RunRoot() if runRoot == "" { return nil, errors.New("could not look up container's runroot: got empty string") } } envs := config.Spec.Process.Env info := containerInfo{ ServiceName: serviceName, ContainerNameOrID: nameOrID, RestartPolicy: define.DefaultRestartPolicy, PIDFile: conmonPidFile, TimeoutStartSec: startTimeout, StopTimeout: stopTimeout, GenerateTimestamp: true, CreateCommand: createCommand, RunRoot: runRoot, containerEnv: envs, Wants: options.Wants, After: options.After, Requires: options.Requires, } return &info, nil } // containerServiceName returns the nameOrID and the service name of the // container. func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) { nameOrID := ctr.ID() if options.Name { nameOrID = ctr.Name() } serviceName := getServiceName(options.ContainerPrefix, options.Separator, nameOrID) return nameOrID, serviceName } // setContainerNameForTemplate updates startCommand to contain the name argument with // a value that includes the identify specifier. // In case startCommand doesn't contain that argument it's added after "run" and its // value will be set to info.ServiceName concated with the identify specifier %i. func setContainerNameForTemplate(startCommand []string, info *containerInfo) ([]string, error) { // find the index of "--name" in the command slice nameIx := -1 for argIx, arg := range startCommand { if arg == "--name" { nameIx = argIx + 1 break } if strings.HasPrefix(arg, "--name=") { nameIx = argIx break } } switch { case nameIx == -1: // if not found, add --name argument in the command slice before the "run" argument. // it's assumed that the command slice contains this argument. runIx := -1 for argIx, arg := range startCommand { if arg == "run" { runIx = argIx break } } if runIx == -1 { return startCommand, fmt.Errorf("\"run\" is missing in the command arguments") } startCommand = append(startCommand[:runIx+1], startCommand[runIx:]...) startCommand[runIx+1] = fmt.Sprintf("--name=%s-%%i", info.ServiceName) default: // append the identity specifier (%i) to the end of the --name value startCommand[nameIx] = fmt.Sprintf("%s-%%i", startCommand[nameIx]) } return startCommand, nil } func formatOptions(options []string) string { var formatted strings.Builder if len(options) == 0 { return "" } formatted.WriteString(options[0]) for _, o := range options[1:] { if strings.HasPrefix(o, "-") { formatted.WriteString(" \\\n\t" + o) continue } formatted.WriteString(" " + o) } return formatted.String() } // executeContainerTemplate executes the container template on the specified // containerInfo. Note that the containerInfo is also post processed and // completed, which allows for an easier unit testing. func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) { if options.RestartPolicy != nil { if err := validateRestartPolicy(*options.RestartPolicy); err != nil { return "", err } info.RestartPolicy = *options.RestartPolicy } // 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", executable) } info.Executable = executable } info.Type = "forking" info.EnvVariable = define.EnvVariable info.ExecStart = "{{{{.Executable}}}} start {{{{.ContainerNameOrID}}}}" info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}" info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}" // Assemble the ExecStart command when creating a new container. // // Note that we cannot catch all corner cases here such that users // *must* manually check the generated files. A container 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.Type = "notify" info.NotifyAccess = "all" info.PIDFile = "" info.ContainerIDFile = "%t/%n.ctr-id" info.ExecStartPre = "/bin/rm -f {{{{.ContainerIDFile}}}}" info.ExecStop = "{{{{.Executable}}}} stop --ignore --cidfile={{{{.ContainerIDFile}}}}" info.ExecStopPost = "{{{{.Executable}}}} rm -f --ignore --cidfile={{{{.ContainerIDFile}}}}" // The create command must at least have three arguments: // /usr/bin/podman run $IMAGE index := 0 for i, arg := range info.CreateCommand { if arg == "run" || arg == "create" { index = i + 1 break } } if index == 0 { return "", fmt.Errorf("container's create command is too short or invalid: %v", info.CreateCommand) } // We're hard-coding the first five arguments and append the // CreateCommand with a stripped command and subcommand. startCommand := []string{info.Executable} if index > 2 { // include root flags info.RootFlags = strings.Join(escapeSystemdArguments(info.CreateCommand[1:index-1]), " ") startCommand = append(startCommand, info.CreateCommand[1:index-1]...) } startCommand = append(startCommand, "run", "--cidfile={{{{.ContainerIDFile}}}}", "--cgroups=no-conmon", "--rm", ) remainingCmd := info.CreateCommand[index:] // Presence check for certain flags/options. fs := pflag.NewFlagSet("args", pflag.ContinueOnError) fs.ParseErrorsWhitelist.UnknownFlags = true fs.Usage = func() {} fs.SetInterspersed(false) fs.BoolP("detach", "d", false, "") fs.String("name", "", "") fs.Bool("replace", false, "") fs.StringArrayP("env", "e", nil, "") fs.String("sdnotify", "", "") fs.String("restart", "", "") // have to define extra -h flag to prevent help error when parsing -h hostname // https://github.com/containers/podman/issues/15124 fs.StringP("help", "h", "", "") if err := fs.Parse(remainingCmd); err != nil { return "", fmt.Errorf("parsing remaining command-line arguments: %w", err) } remainingCmd = filterCommonContainerFlags(remainingCmd, fs.NArg()) // If the container is in a pod, make sure that the // --pod-id-file is set correctly. if info.Pod != nil { podFlags := []string{"--pod-id-file", "{{{{.Pod.PodIDFile}}}}"} startCommand = append(startCommand, podFlags...) remainingCmd = filterPodFlags(remainingCmd, fs.NArg()) } hasDetachParam, err := fs.GetBool("detach") if err != nil { return "", err } hasNameParam := fs.Lookup("name").Changed hasReplaceParam, err := fs.GetBool("replace") if err != nil { return "", err } // Default to --sdnotify=conmon unless already set by the // container. sdnotifyFlag := fs.Lookup("sdnotify") if !sdnotifyFlag.Changed { startCommand = append(startCommand, "--sdnotify=conmon") } else if sdnotifyFlag.Value.String() == libpodDefine.SdNotifyModeIgnore { // If ignore is set force conmon otherwise the unit with Type=notify will fail. logrus.Infof("Forcing --sdnotify=conmon for container %s", info.ContainerNameOrID) remainingCmd = removeSdNotifyArg(remainingCmd, fs.NArg()) startCommand = append(startCommand, "--sdnotify=conmon") } if !hasDetachParam { // Enforce detaching // // since we use systemd `Type=forking` service @see // https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= // when we generated systemd service file with the // --new param, `ExecStart` will have `/usr/bin/podman // run ...` if `info.CreateCommand` has no `-d` or // `--detach` param, podman will run the container in // default attached mode, as a result, `systemd start` // will wait the `podman run` command exit until failed // with timeout error. startCommand = append(startCommand, "-d") if fs.Changed("detach") { // this can only happen if --detach=false is set // in that case we need to remove it otherwise we // would overwrite the previous detach arg to false remainingCmd = removeDetachArg(remainingCmd, fs.NArg()) } } if hasNameParam && !hasReplaceParam { // Enforce --replace for named containers. This will // make systemd units more robust as it allows them to // start after system crashes (see // github.com/containers/podman/issues/5485). startCommand = append(startCommand, "--replace") 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 remainingCmd = removeReplaceArg(remainingCmd, fs.NArg()) } } // Unless the user explicitly set a restart policy, check // whether the container was created with a custom one and use // it instead of the default. if options.RestartPolicy == nil { restartPolicy, err := fs.GetString("restart") if err != nil { return "", err } if restartPolicy != "" { if strings.HasPrefix(restartPolicy, "on-failure:") { // Special case --restart=on-failure:5 spl := strings.Split(restartPolicy, ":") restartPolicy = spl[0] info.StartLimitBurst = spl[1] } else if restartPolicy == libpodDefine.RestartPolicyUnlessStopped { restartPolicy = libpodDefine.RestartPolicyAlways } info.RestartPolicy = restartPolicy } } envs, err := fs.GetStringArray("env") if err != nil { return "", err } for _, env := range envs { // if env arg does not contain a equal sign we have to add the envar to the unit // because it does try to red the value from the environment if !strings.Contains(env, "=") { for _, containerEnv := range info.containerEnv { split := strings.SplitN(containerEnv, "=", 2) if split[0] == env { info.ExtraEnvs = append(info.ExtraEnvs, escapeSystemdArg(containerEnv)) } } } } startCommand = append(startCommand, remainingCmd...) startCommand = escapeSystemdArguments(startCommand) if options.TemplateUnitFile { info.IdentifySpecifier = true startCommand, err = setContainerNameForTemplate(startCommand, info) if err != nil { return "", err } } info.ExecStart = formatOptions(startCommand) } 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 = fmt.Sprintf("%v", time.Now().Format(time.UnixDate)) } // Sort the slices to assure a deterministic output. sort.Strings(info.BoundToServices) // 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("container_template").Delims("{{{{", "}}}}").Parse(containerTemplate) if err != nil { return "", fmt.Errorf("error 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("container_template").Delims("{{{{", "}}}}").Parse(buf.String()) if err != nil { return "", fmt.Errorf("error parsing systemd service template: %w", err) } buf = bytes.Buffer{} if err := templ.Execute(&buf, info); err != nil { return "", err } return buf.String(), nil }