mirror of
https://github.com/containers/podman.git
synced 2025-06-17 15:08:08 +08:00

Add an `ExecStopPost` run even for units generated without `--new`. Although it may seem redundant to run `container/pod stop` twice at first glance, we really need the post run. If the main PID (i.e., conmon) is killed, systemd will not execute `ExecStop` but only the post one. We made this obeservation in a customer issue and could reproduce the behavior consistently. Hence, the post run is needed to properly clean up when conmon is killed and it's pretty much a NOP in all other cases. Credits to Ulrich Obergfell for throrough and detailed analyses, which ultimately lead to this fix. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
307 lines
10 KiB
Go
307 lines
10 KiB
Go
package generate
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/containers/libpod/libpod"
|
|
"github.com/containers/libpod/pkg/domain/entities"
|
|
"github.com/containers/libpod/version"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// 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
|
|
// 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
|
|
// 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
|
|
// 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
|
|
// EnvVariable is generate.EnvVariable and must not be set.
|
|
EnvVariable string
|
|
// ExecStartPre of the unit.
|
|
ExecStartPre string
|
|
// ExecStart of the unit.
|
|
ExecStart string
|
|
// ExecStop of the unit.
|
|
ExecStop string
|
|
// ExecStopPost of the unit.
|
|
ExecStopPost string
|
|
|
|
// If not nil, the container is part of the pod. We can use the
|
|
// podInfo to extract the relevant data.
|
|
pod *podInfo
|
|
}
|
|
|
|
const containerTemplate = headerTemplate + `
|
|
{{- if .BoundToServices}}
|
|
RefuseManualStart=yes
|
|
RefuseManualStop=yes
|
|
BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
|
|
After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
|
|
{{- end}}
|
|
|
|
[Service]
|
|
Environment={{.EnvVariable}}=%n
|
|
Restart={{.RestartPolicy}}
|
|
{{- if .ExecStartPre}}
|
|
ExecStartPre={{.ExecStartPre}}
|
|
{{- end}}
|
|
ExecStart={{.ExecStart}}
|
|
ExecStop={{.ExecStop}}
|
|
ExecStopPost={{.ExecStopPost}}
|
|
PIDFile={{.PIDFile}}
|
|
KillMode=none
|
|
Type=forking
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target 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, error) {
|
|
info, err := generateContainerInfo(ctr, options)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return executeContainerTemplate(info, options)
|
|
}
|
|
|
|
func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
|
|
timeout := ctr.StopTimeout()
|
|
if options.StopTimeout != nil {
|
|
timeout = *options.StopTimeout
|
|
}
|
|
|
|
config := ctr.Config()
|
|
conmonPidFile := config.ConmonPidFile
|
|
if conmonPidFile == "" {
|
|
return nil, errors.Errorf("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, errors.Errorf("cannot use --new on container %q: no create command found", ctr.ID())
|
|
}
|
|
|
|
nameOrID, serviceName := containerServiceName(ctr, options)
|
|
|
|
info := containerInfo{
|
|
ServiceName: serviceName,
|
|
ContainerNameOrID: nameOrID,
|
|
RestartPolicy: options.RestartPolicy,
|
|
PIDFile: conmonPidFile,
|
|
StopTimeout: timeout,
|
|
GenerateTimestamp: true,
|
|
CreateCommand: createCommand,
|
|
}
|
|
|
|
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 := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
|
|
return nameOrID, serviceName
|
|
}
|
|
|
|
// 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 err := validateRestartPolicy(info.RestartPolicy); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 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.EnvVariable = 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.PIDFile = "%t/" + info.ServiceName + ".pid"
|
|
info.ContainerIDFile = "%t/" + info.ServiceName + ".ctr-id"
|
|
// The create command must at least have three arguments:
|
|
// /usr/bin/podman run $IMAGE
|
|
index := 2
|
|
if info.CreateCommand[1] == "container" {
|
|
index = 3
|
|
}
|
|
if len(info.CreateCommand) < index+1 {
|
|
return "", errors.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 subcomand.
|
|
startCommand := []string{
|
|
info.Executable,
|
|
"run",
|
|
"--conmon-pidfile", "{{.PIDFile}}",
|
|
"--cidfile", "{{.ContainerIDFile}}",
|
|
"--cgroups=no-conmon",
|
|
}
|
|
// 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", info.pod.PodIDFile}
|
|
startCommand = append(startCommand, podFlags...)
|
|
info.CreateCommand = filterPodFlags(info.CreateCommand)
|
|
}
|
|
|
|
// Presence check for certain flags/options.
|
|
hasDetachParam := false
|
|
hasNameParam := false
|
|
hasReplaceParam := false
|
|
for _, p := range info.CreateCommand[index:] {
|
|
switch p {
|
|
case "--detach", "-d":
|
|
hasDetachParam = true
|
|
case "--name":
|
|
hasNameParam = true
|
|
case "--replace":
|
|
hasReplaceParam = true
|
|
}
|
|
}
|
|
|
|
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 hasNameParam && !hasReplaceParam {
|
|
// Enforce --replace for named containers. This will
|
|
// make systemd units more robuts as it allows them to
|
|
// start after system crashes (see
|
|
// github.com/containers/libpod/issues/5485).
|
|
startCommand = append(startCommand, "--replace")
|
|
}
|
|
startCommand = append(startCommand, info.CreateCommand[index:]...)
|
|
|
|
info.ExecStartPre = "/usr/bin/rm -f {{.PIDFile}} {{.ContainerIDFile}}"
|
|
info.ExecStart = strings.Join(startCommand, " ")
|
|
info.ExecStop = "{{.Executable}} stop --ignore --cidfile {{.ContainerIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}"
|
|
info.ExecStopPost = "{{.Executable}} rm --ignore -f --cidfile {{.ContainerIDFile}}"
|
|
}
|
|
|
|
if info.PodmanVersion == "" {
|
|
info.PodmanVersion = version.Version
|
|
}
|
|
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").Parse(containerTemplate)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error parsing systemd service template")
|
|
}
|
|
|
|
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").Parse(buf.String())
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error parsing systemd service template")
|
|
}
|
|
|
|
buf = bytes.Buffer{}
|
|
if err := templ.Execute(&buf, info); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !options.Files {
|
|
return buf.String(), nil
|
|
}
|
|
|
|
buf.WriteByte('\n')
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error getting current working directory")
|
|
}
|
|
path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
|
|
if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
|
|
return "", errors.Wrap(err, "error generating systemd unit")
|
|
}
|
|
return path, nil
|
|
}
|