Files
podman/pkg/systemd/generate/containers.go
Valentin Rothberg e5c3432944 generate systemd: ExecStopPost for all units
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>
2020-06-17 18:52:44 +02:00

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
}