Skip to content

Commit

Permalink
add "healthy" sdnotify policy
Browse files Browse the repository at this point in the history
Add a new "healthy" sdnotify policy that instructs Podman to send the
READY message once the container has turned healthy.

Fixes: #6160
Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg committed Jul 25, 2023
1 parent 2a25d1d commit 0cfd127
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 14 deletions.
2 changes: 1 addition & 1 deletion cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -1545,7 +1545,7 @@ func AutocompleteLogLevel(cmd *cobra.Command, args []string, toComplete string)
// AutocompleteSDNotify - Autocomplete sdnotify options.
// -> "container", "conmon", "ignore"
func AutocompleteSDNotify(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
types := []string{define.SdNotifyModeContainer, define.SdNotifyModeContainer, define.SdNotifyModeIgnore}
types := []string{define.SdNotifyModeConmon, define.SdNotifyModeContainer, define.SdNotifyModeHealthy, define.SdNotifyModeIgnore}
return types, cobra.ShellCompDirectiveNoFileComp
}

Expand Down
4 changes: 3 additions & 1 deletion docs/source/markdown/options/sdnotify.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
####> podman create, run
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--sdnotify**=**container** | *conmon* | *ignore*
#### **--sdnotify**=**container** | *conmon* | *healthy* | *ignore*

Determines how to use the NOTIFY_SOCKET, as passed with systemd and Type=notify.

Default is **container**, which means allow the OCI runtime to proxy the socket into the
container to receive ready notification. Podman sets the MAINPID to conmon's pid.
The **conmon** option sets MAINPID to conmon's pid, and sends READY when the container
has started. The socket is never passed to the runtime or the container.
The **healthy** option sets MAINPID to conmon's pid, and sends READY when the container
has turned healthy; requires a healthcheck to be set. The socket is never passed to the runtime or the container.
The **ignore** option removes NOTIFY_SOCKET from the environment for itself and child processes,
for the case where some other process above Podman uses NOTIFY_SOCKET and Podman does not use it.
2 changes: 1 addition & 1 deletion libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (c *Container) Start(ctx context.Context, recursive bool) (finalErr error)
}

// Start the container
return c.start()
return c.start(ctx)
}

// Update updates the given container.
Expand Down
37 changes: 32 additions & 5 deletions libpod/container_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func (c *Container) handleRestartPolicy(ctx context.Context) (_ bool, retErr err
return false, err
}
}
if err := c.start(); err != nil {
if err := c.start(ctx); err != nil {
return false, err
}
return true, nil
Expand Down Expand Up @@ -1198,11 +1198,11 @@ func (c *Container) initAndStart(ctx context.Context) (retErr error) {
}

// Now start the container
return c.start()
return c.start(ctx)
}

// Internal, non-locking function to start a container
func (c *Container) start() error {
func (c *Container) start(ctx context.Context) error {
if c.config.Spec.Process != nil {
logrus.Debugf("Starting container %s with command %v", c.ID(), c.config.Spec.Process.Args)
}
Expand All @@ -1214,9 +1214,11 @@ func (c *Container) start() error {

c.state.State = define.ContainerStateRunning

// Unless being ignored, set the MAINPID to conmon.
if c.config.SdNotifyMode != define.SdNotifyModeIgnore {
payload := fmt.Sprintf("MAINPID=%d", c.state.ConmonPID)
if c.config.SdNotifyMode == define.SdNotifyModeConmon {
// Also send the READY message for the "conmon" policy.
payload += "\n"
payload += daemon.SdNotifyReady
}
Expand All @@ -1241,7 +1243,32 @@ func (c *Container) start() error {

defer c.newContainerEvent(events.Start)

return c.save()
if err := c.save(); err != nil {
return err
}

if c.config.SdNotifyMode != define.SdNotifyModeHealthy {
return nil
}

// Wait for the container to turn healthy before sending the READY
// message. This implies that we need to unlock and re-lock the
// container.
if !c.batched {
c.lock.Unlock()
defer c.lock.Lock()
}

if _, err := c.WaitForConditionWithInterval(ctx, DefaultWaitInterval, define.HealthCheckHealthy); err != nil {
return err
}

if err := notifyproxy.SendMessage(c.config.SdNotifySocket, daemon.SdNotifyReady); err != nil {
logrus.Errorf("Sending READY message after turning healthy: %s", err.Error())
} else {
logrus.Debugf("Notify sent successfully")
}
return nil
}

// Internal, non-locking function to stop container
Expand Down Expand Up @@ -1487,7 +1514,7 @@ func (c *Container) restartWithTimeout(ctx context.Context, timeout uint) (retEr
return err
}
}
return c.start()
return c.start(ctx)
}

// mountStorage sets up the container's root filesystem
Expand Down
7 changes: 4 additions & 3 deletions libpod/define/sdnotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import "fmt"

// Strings used for --sdnotify option to podman
const (
SdNotifyModeContainer = "container"
SdNotifyModeConmon = "conmon"
SdNotifyModeContainer = "container"
SdNotifyModeHealthy = "healthy"
SdNotifyModeIgnore = "ignore"
)

// ValidateSdNotifyMode validates the specified mode.
func ValidateSdNotifyMode(mode string) error {
switch mode {
case "", SdNotifyModeContainer, SdNotifyModeConmon, SdNotifyModeIgnore:
case "", SdNotifyModeContainer, SdNotifyModeConmon, SdNotifyModeIgnore, SdNotifyModeHealthy:
return nil
default:
return fmt.Errorf("%w: invalid sdnotify value %q: must be %s, %s or %s", ErrInvalidArg, mode, SdNotifyModeContainer, SdNotifyModeConmon, SdNotifyModeIgnore)
return fmt.Errorf("%w: invalid sdnotify value %q: must be %s, %s, %s or %s", ErrInvalidArg, mode, SdNotifyModeConmon, SdNotifyModeContainer, SdNotifyModeHealthy, SdNotifyModeIgnore)
}
}
3 changes: 2 additions & 1 deletion libpod/oci_conmon_attach_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package libpod

import (
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -86,7 +87,7 @@ func (r *ConmonOCIRuntime) Attach(c *Container, params *AttachOptions) error {
// If starting was requested, start the container and notify when that's
// done.
if params.Start {
if err := c.start(); err != nil {
if err := c.start(context.TODO()); err != nil {
return err
}
params.Started <- true
Expand Down
2 changes: 0 additions & 2 deletions pkg/specgen/container_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ var (
ErrInvalidSpecConfig = errors.New("invalid configuration")
// SystemDValues describes the only values that SystemD can be
SystemDValues = []string{"true", "false", "always"}
// SdNotifyModeValues describes the only values that SdNotifyMode can be
SdNotifyModeValues = []string{define.SdNotifyModeContainer, define.SdNotifyModeConmon, define.SdNotifyModeIgnore}
// ImageVolumeModeValues describes the only values that ImageVolumeMode can be
ImageVolumeModeValues = []string{"ignore", define.TypeTmpfs, "anonymous"}
)
Expand Down
7 changes: 7 additions & 0 deletions pkg/specgen/generate/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,18 +601,25 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l
}
options = append(options, libpod.WithRestartRetries(retries), libpod.WithRestartPolicy(restartPolicy))

healthCheckSet := false
if s.ContainerHealthCheckConfig.HealthConfig != nil {
options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig))
logrus.Debugf("New container has a health check")
healthCheckSet = true
}
if s.ContainerHealthCheckConfig.StartupHealthConfig != nil {
options = append(options, libpod.WithStartupHealthcheck(s.ContainerHealthCheckConfig.StartupHealthConfig))
healthCheckSet = true
}

if s.ContainerHealthCheckConfig.HealthCheckOnFailureAction != define.HealthCheckOnFailureActionNone {
options = append(options, libpod.WithHealthCheckOnFailureAction(s.ContainerHealthCheckConfig.HealthCheckOnFailureAction))
}

if s.SdNotifyMode == define.SdNotifyModeHealthy && !healthCheckSet {
return nil, fmt.Errorf("%w: sdnotify policy %q requires a healthcheck to be set", define.ErrInvalidArg, s.SdNotifyMode)
}

if len(s.Secrets) != 0 {
manager, err := rt.SecretsManager()
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions test/system/260-sdnotify.bats
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,65 @@ READY=1"
_stop_socat
}

# These tests can fail in dev. environment because of SELinux.
# quick fix: chcon -t container_runtime_exec_t ./bin/podman
@test "sdnotify : healthy" {
export NOTIFY_SOCKET=$PODMAN_TMPDIR/container.sock
_start_socat

wait_file="$PODMAN_TMPDIR/$(random_string).wait_for_me"
run_podman 125 create --sdnotify=healthy $IMAGE
is "$output" "Error: invalid argument: sdnotify policy \"healthy\" requires a healthcheck to be set"

# Create a container with a simple `/bin/true` healthcheck that we need to
# run manually.
ctr=$(random_string)
run_podman create --name $ctr \
--health-cmd=/bin/true \
--health-retries=1 \
--health-interval=disable \
--sdnotify=healthy \
$IMAGE sleep infinity

# Start the container in the background which will block until the
# container turned healthy. After that, create the wait_file which
# indicates that start has returned.
(timeout --foreground -v --kill=5 20 $PODMAN start $ctr && touch $wait_file) &

run_podman wait --condition=running $ctr

# Make sure that the MAINPID is set but without the READY message.
run_podman container inspect $ctr --format "{{.State.ConmonPid}}"
mainPID="$output"
# With container, READY=1 isn't necessarily the last message received;
# just look for it anywhere in received messages
run cat $_SOCAT_LOG
# The 'echo's help us debug failed runs
echo "socat log:"
echo "$output"

is "$output" "MAINPID=$mainPID" "Container is not healthy yet, so we only know the main PID"

# Now run the healthcheck and look for the READY message.
run_podman healthcheck run $ctr
is "$output" "" "output from 'podman healthcheck run'"

# Wait for start to return. At that point the READY message must have been
# sent.
wait_for_file $wait_file
run cat $_SOCAT_LOG
echo "socat log:"
echo "$output"
is "$output" "MAINPID=$mainPID
READY=1"

run_podman container inspect --format "{{.State.Status}}" $ctr
is "$output" "running" "make sure container is still running"

run_podman rm -f -t0 $ctr
_stop_socat
}

@test "sdnotify : play kube - no policies" {
# Create the YAMl file
yaml_source="$PODMAN_TMPDIR/test.yaml"
Expand Down

0 comments on commit 0cfd127

Please sign in to comment.