Files
podman/libpod/healthcheck_linux.go
Paul Holzinger e8ea1e7632 libpod: do not leak systemd hc startup unit timer
This fixes a regression added in commit 4fd84190b8, because the name was
overwritten by the createTimer() timer call the removeTransientFiles()
call removed the new timer and not the startup healthcheck. And then
when the container was stopped we leaked it as the wrong unit name was
in the state.

A new test has been added to ensure the logic works and we never leak
the system timers.

Fixes #22884

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2024-06-04 18:03:46 +02:00

189 lines
5.6 KiB
Go

//go:build !remote && systemd
package libpod
import (
"context"
"fmt"
"math/rand"
"os"
"os/exec"
"strings"
systemdCommon "github.com/containers/common/pkg/systemd"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/containers/podman/v5/pkg/systemd"
"github.com/sirupsen/logrus"
)
// createTimer systemd timers for healthchecks of a container
func (c *Container) createTimer(interval string, isStartup bool) error {
if c.disableHealthCheckSystemd(isStartup) {
return nil
}
hcUnitName := c.hcUnitName(isStartup, false)
podman, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get path for podman for a health check timer: %w", err)
}
var cmd = []string{"--property", "LogLevelMax=notice"}
if rootless.IsRootless() {
cmd = append(cmd, "--user")
}
path := os.Getenv("PATH")
if path != "" {
cmd = append(cmd, "--setenv=PATH="+path)
}
cmd = append(cmd, "--unit", hcUnitName, fmt.Sprintf("--on-unit-inactive=%s", interval), "--timer-property=AccuracySec=1s", podman)
if logrus.IsLevelEnabled(logrus.DebugLevel) {
cmd = append(cmd, "--log-level=debug", "--syslog")
}
cmd = append(cmd, "healthcheck", "run", c.ID())
conn, err := systemd.ConnectToDBUS()
if err != nil {
return fmt.Errorf("unable to get systemd connection to add healthchecks: %w", err)
}
conn.Close()
logrus.Debugf("creating systemd-transient files: %s %s", "systemd-run", cmd)
systemdRun := exec.Command("systemd-run", cmd...)
if output, err := systemdRun.CombinedOutput(); err != nil {
return fmt.Errorf("%s", output)
}
c.state.HCUnitName = hcUnitName
if err := c.save(); err != nil {
return fmt.Errorf("saving container %s healthcheck unit name: %w", c.ID(), err)
}
return nil
}
// Wait for a message on the channel. Throw an error if the message is not "done".
func systemdOpSuccessful(c chan string) error {
msg := <-c
switch msg {
case "done":
return nil
default:
return fmt.Errorf("expected %q but received %q", "done", msg)
}
}
// startTimer starts a systemd timer for the healthchecks
func (c *Container) startTimer(isStartup bool) error {
if c.disableHealthCheckSystemd(isStartup) {
return nil
}
hcUnitName := c.state.HCUnitName
if hcUnitName == "" {
hcUnitName = c.hcUnitName(isStartup, true)
}
conn, err := systemd.ConnectToDBUS()
if err != nil {
return fmt.Errorf("unable to get systemd connection to start healthchecks: %w", err)
}
defer conn.Close()
startFile := fmt.Sprintf("%s.service", hcUnitName)
startChan := make(chan string)
if _, err := conn.RestartUnitContext(context.Background(), startFile, "fail", startChan); err != nil {
return err
}
if err := systemdOpSuccessful(startChan); err != nil {
return fmt.Errorf("starting systemd health-check timer %q: %w", startFile, err)
}
return nil
}
// removeTransientFiles removes the systemd timer and unit files
// for the container
func (c *Container) removeTransientFiles(ctx context.Context, isStartup bool, unitName string) error {
if c.disableHealthCheckSystemd(isStartup) {
return nil
}
conn, err := systemd.ConnectToDBUS()
if err != nil {
return fmt.Errorf("unable to get systemd connection to remove healthchecks: %w", err)
}
defer conn.Close()
// Errors are returned at the very end. Let's make sure to stop and
// clean up as much as possible.
stopErrors := []error{}
if unitName == "" {
unitName = c.hcUnitName(isStartup, true)
}
// Stop the timer before the service to make sure the timer does not
// fire after the service is stopped.
timerChan := make(chan string)
timerFile := fmt.Sprintf("%s.timer", unitName)
if _, err := conn.StopUnitContext(ctx, timerFile, "ignore-dependencies", timerChan); err != nil {
if !strings.HasSuffix(err.Error(), ".timer not loaded.") {
stopErrors = append(stopErrors, fmt.Errorf("removing health-check timer %q: %w", timerFile, err))
}
} else if err := systemdOpSuccessful(timerChan); err != nil {
stopErrors = append(stopErrors, fmt.Errorf("stopping systemd health-check timer %q: %w", timerFile, err))
}
// Reset the service before stopping it to make sure it's being removed
// on stop.
serviceChan := make(chan string)
serviceFile := fmt.Sprintf("%s.service", unitName)
if err := conn.ResetFailedUnitContext(ctx, serviceFile); err != nil {
logrus.Debugf("Failed to reset unit file: %q", err)
}
if _, err := conn.StopUnitContext(ctx, serviceFile, "ignore-dependencies", serviceChan); err != nil {
if !strings.HasSuffix(err.Error(), ".service not loaded.") {
stopErrors = append(stopErrors, fmt.Errorf("removing health-check service %q: %w", serviceFile, err))
}
} else if err := systemdOpSuccessful(serviceChan); err != nil {
stopErrors = append(stopErrors, fmt.Errorf("stopping systemd health-check service %q: %w", serviceFile, err))
}
return errorhandling.JoinErrors(stopErrors)
}
func (c *Container) disableHealthCheckSystemd(isStartup bool) bool {
if !systemdCommon.RunsOnSystemd() || os.Getenv("DISABLE_HC_SYSTEMD") == "true" {
return true
}
if isStartup {
if c.config.StartupHealthCheckConfig.Interval == 0 {
return true
}
}
if c.config.HealthCheckConfig.Interval == 0 {
return true
}
return false
}
// Systemd unit name for the healthcheck systemd unit.
// Bare indicates that a random suffix should not be applied to the name. This
// was default behavior previously, and is used for backwards compatibility.
func (c *Container) hcUnitName(isStartup, bare bool) string {
unitName := c.ID()
if isStartup {
unitName += "-startup"
}
if !bare {
// Ensure that unit names are unique from run to run by appending
// a random suffix.
// Ref: RH Jira RHEL-26105
unitName += fmt.Sprintf("-%x", rand.Int())
}
return unitName
}