mirror of
https://github.com/containers/podman.git
synced 2025-06-23 10:38:20 +08:00

Truncate the container and pod ID files instead of throwing an error. The main motivation is to prevent redundant work when starting systemd units. Throwing an error when the file already exists is not preventing races or file corruptions, so let's leave that to the user which in almost all cases are generated (and tested) systemd units. Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
527 lines
18 KiB
Go
527 lines
18 KiB
Go
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 string
|
|
ContainerNameOrID string
|
|
Type string
|
|
NotifyAccess string
|
|
StopTimeout uint
|
|
RestartPolicy string
|
|
StartLimitBurst string
|
|
PIDFile string
|
|
ContainerIDFile string
|
|
GenerateTimestamp bool
|
|
BoundToServices []string
|
|
PodmanVersion string
|
|
Executable string
|
|
RootFlags string
|
|
TimeStamp string
|
|
CreateCommand []string
|
|
containerEnv []string
|
|
ExtraEnvs []string
|
|
EnvVariable string
|
|
AdditionalEnvVariables []string
|
|
ExecStart string
|
|
TimeoutStartSec uint
|
|
TimeoutStopSec uint
|
|
ExecStartPre string
|
|
ExecStop string
|
|
ExecStopPost string
|
|
GenerateNoHeader bool
|
|
Pod *podInfo
|
|
GraphRoot string
|
|
RunRoot string
|
|
IdentifySpecifier bool
|
|
Wants []string
|
|
After []string
|
|
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}}}}
|
|
{{{{- if .AdditionalEnvVariables}}}}
|
|
{{{{- range $index, $value := .AdditionalEnvVariables -}}}}{{{{if $index}}}}{{{{end}}}}
|
|
Environment={{{{ $value }}}}{{{{end}}}}
|
|
{{{{- end}}}}
|
|
Restart={{{{.RestartPolicy}}}}
|
|
{{{{- if .StartLimitBurst}}}}
|
|
StartLimitBurst={{{{.StartLimitBurst}}}}
|
|
{{{{- end}}}}
|
|
{{{{- if ne .TimeoutStartSec 0}}}}
|
|
TimeoutStartSec={{{{.TimeoutStartSec}}}}
|
|
{{{{- end}}}}
|
|
TimeoutStopSec={{{{.TimeoutStopSec}}}}
|
|
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")
|
|
}
|
|
|
|
// #15284: old units generated without --new can lead to issues on
|
|
// shutdown when the containers are created with a custom restart
|
|
// policy.
|
|
if !options.New {
|
|
switch config.RestartPolicy {
|
|
case libpodDefine.RestartPolicyNo, libpodDefine.RestartPolicyNone:
|
|
// All good
|
|
default:
|
|
logrus.Warnf("Container %s has restart policy %q which can lead to issues on shutdown: consider recreating the container without a restart policy and use systemd's restart mechanism instead", ctr.ID(), config.RestartPolicy)
|
|
}
|
|
}
|
|
|
|
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,
|
|
AdditionalEnvVariables: options.AdditionalEnvVariables,
|
|
}
|
|
|
|
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 formatOptionsString(cmd string) string {
|
|
return formatOptions(strings.Split(cmd, " "))
|
|
}
|
|
|
|
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 = formatOptionsString("{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}} -t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}")
|
|
info.ExecStopPost = formatOptionsString("{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}} -t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}")
|
|
for i, env := range info.AdditionalEnvVariables {
|
|
info.AdditionalEnvVariables[i] = escapeSystemdArg(env)
|
|
}
|
|
|
|
// 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.ExecStop = formatOptionsString("{{{{.Executable}}}} stop --ignore {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} --cidfile={{{{.ContainerIDFile}}}}")
|
|
info.ExecStopPost = formatOptionsString("{{{{.Executable}}}} rm -f --ignore {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} --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("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("parsing systemd service template: %w", err)
|
|
}
|
|
|
|
buf = bytes.Buffer{}
|
|
if err := templ.Execute(&buf, info); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|