Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,8 @@ func AutocompleteImageScpFormat(_ *cobra.Command, _ []string, _ string) ([]strin
func AutocompleteWaitCondition(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
states := []string{
"unknown", "configured", "created", "exited",
"healthy", "initialized", "paused", "removing", "running",
"healthy", "initialized", "next-exit", "not-running",
"paused", "removed", "removing", "running",
"stopped", "stopping", "unhealthy",
}
return states, cobra.ShellCompDirectiveNoFileComp
Expand Down
48 changes: 43 additions & 5 deletions docs/source/markdown/podman-wait.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ podman\-wait - Wait on one or more containers to stop and print their exit codes
Waits on one or more containers to stop. The container can be referred to by its
name or ID. In the case of multiple containers, Podman waits on each consecutively.
After all conditions are satisfied, the containers' return codes are printed
separated by newline in the same order as they were given to the command. An
exit code of -1 is emitted for all conditions other than "stopped" and
"exited".
separated by newline in the same order as they were given to the command.

The conditions `stopped`, `exited`, `next-exit`, `not-running`, and `removed`
print the container's real exit code (or `0` for the conditions that match a
never-started container — see below). All other conditions (`running`, `paused`,
`created`, `configured`, `initialized`, `removing`, `stopping`, the health
conditions `healthy`/`unhealthy`, and `unknown`) are pure state matches and emit
`-1` once they are satisfied. `removed` emits `-1` only in the unusual case that
the container's exit code has already been pruned from the database.

When waiting for containers with a restart policy of `always` or `on-failure`,
such as those created by `podman kube play`, the containers may be repeatedly
Expand All @@ -23,12 +29,25 @@ only display and detect the first exit after the wait command was started.

When running a container with podman run --rm wait does not wait for the
container to be fully removed. To wait for the removal of a container use
`--condition=removing`.
`--condition=removed`.

### Note on never-started containers

For a container that has been created (e.g. via `podman create`) but never
started, `--condition=stopped`, `--condition=exited`, and `--condition=not-running`
all return immediately with exit code `0`. This matches Docker's default
`not-running` semantic — a container that has never run is, by definition, not
running. To block until the container has actually been started and has exited,
use `--condition=next-exit`.

## OPTIONS

#### **--condition**=*state*
Container state or condition to wait for. Can be specified multiple times where at least one condition must match for the command to return. Supported values are "configured", "created", "exited", "healthy", "initialized", "paused", "removing", "running", "stopped", "stopping", "unhealthy". The default condition is "stopped".
Container state or condition to wait for. Can be specified multiple times where at least one condition must match for the command to return. Supported values are "configured", "created", "exited", "healthy", "initialized", "next-exit", "not-running", "paused", "removed", "removing", "running", "stopped", "stopping", "unhealthy". The default condition is "stopped".

The Docker-compatible aliases are: "next-exit" (block until the container
next dies, regardless of current state), "not-running" (match any non-running
state), and "removed" (block until the container has been removed).

#### **--exit-first-match**
Wait for exit of first container which matches conditions, ignore other ones.
Expand Down Expand Up @@ -85,6 +104,25 @@ $ podman wait --ignore does-not-exist
-1
```

Block until the container next exits, regardless of its current state. Useful
for waiting on a `podman create`d container that has not been started yet:
```
$ podman create --name init_2 busybox sh -c "sleep 5 && exit 7"
$ podman wait --condition=next-exit init_2 &
$ podman start init_2
$ wait
7
```

Block until the container has been removed:
```
$ podman run -d --name webserver busybox sleep 30
$ podman wait --condition=removed webserver &
$ podman rm -f webserver
$ wait
137
```

## SEE ALSO
**[podman(1)](podman.1.md)**

Expand Down
89 changes: 89 additions & 0 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou

resultChan := make(chan waitResult)
waitForExit := false
waitForNextExit := false
waitForRemoved := false
wantedStates := make(map[define.ContainerStatus]bool, len(conditions))
wantedHealthStates := make(map[string]bool)

Expand All @@ -749,7 +751,33 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
return -1, fmt.Errorf("cannot use condition %q: container %s has no healthcheck", rawCondition, c.ID())
}
wantedHealthStates[rawCondition] = true
case define.ContainerWaitConditionNextExit:
waitForNextExit = true
case define.ContainerWaitConditionRemoved:
waitForRemoved = true
case define.ContainerWaitConditionNotRunning:
// Reuse the WaitForExit path so we return the real exit
// code instead of -1 (which is what the state-matching
// goroutine would emit). WaitForExit handles every
// "not running" sub-state correctly: Configured/Created
// -> 0 (never ran), Stopped/Exited/Removing -> recorded
// exit code, Running -> blocks for conmon and then
// returns the code. The "Stopping" state intentionally
// isn't matched (conmon is still alive -> WaitForExit
// blocks for it to exit), mirroring the notRunningStates
// slice in pkg/api/handlers/utils/containers.go.
waitForExit = true
default:
// "created" is displayed for both ContainerStateConfigured
// (libpod's "configured") and ContainerStateCreated. Match
// both so the CLI behaves the way users expect. Do not
// modify StringToContainerStatus itself — other callers
// rely on its strict 1:1 mapping.
if rawCondition == define.ContainerStateConfigured.String() {
wantedStates[define.ContainerStateConfigured] = true
wantedStates[define.ContainerStateCreated] = true
continue
}
condition, err := define.StringToContainerStatus(rawCondition)
if err != nil {
return -1, err
Expand Down Expand Up @@ -777,6 +805,67 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
}()
}

if waitForNextExit {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these wait logics are implemented in compat API pkg/api/handlers/utils/containers.go (waitNextExit, waitNotRunning, waitRemoved). I would reuse them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed this should be deduplicated with the API code

if the libpod code can no handl ethese states they could be dropped out of the compat API to avoid differtent special handling.

// Subscribe to died events synchronously (before spawning the
// goroutine) so the subscription is in place before this
// function returns control. Otherwise we race with the
// container actually dying.
eventChan := make(chan events.ReadResult, 1)
eventCtx, eventCancel := context.WithCancel(ctx)
defer eventCancel()
err := c.runtime.Events(eventCtx, events.ReadOptions{
EventChannel: eventChan,
Filters: []string{
"event=died",
"type=container",
fmt.Sprintf("container=%s", c.ID()),
},
Stream: true,
})
if err != nil {
return -1, fmt.Errorf("subscribing to died events: %w", err)
}
go func() {
for evt := range eventChan {
if evt.Error != nil {
trySend(-1, evt.Error)
return
}
if evt.Event != nil && evt.Event.ContainerExitCode != nil {
trySend(int32(*evt.Event.ContainerExitCode), nil)
return
}
}
}()
}

if waitForRemoved {
go func() {
for {
_, err := c.State()
if err != nil {
if errors.Is(err, define.ErrNoSuchCtr) || errors.Is(err, define.ErrCtrRemoved) {
exitCode, exitErr := c.runtime.state.GetContainerExitCode(c.ID())
if exitErr == nil {
trySend(exitCode, nil)
return
}
trySend(-1, nil)
return
}
trySend(-1, err)
return
}
select {
case <-ctx.Done():
return
case <-time.After(waitTimeout):
continue
}
}
}()
}

if len(wantedStates) > 0 || len(wantedHealthStates) > 0 {
go func() {
stoppedCount := 0
Expand Down
18 changes: 18 additions & 0 deletions libpod/define/containerstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ const (
ContainerStateStopping ContainerStatus = iota
)

// Wait condition strings that do not map 1:1 to a ContainerStatus.
// These mirror Docker's wait condition names so users have a way to
// express "block until the container next dies" or "block until the
// container is removed", regardless of the container's current state.
const (
// ContainerWaitConditionNextExit waits for the next exit event,
// regardless of the container's current state. This differs from
// "exited"/"stopped", which return immediately for a container that
// has never been started.
ContainerWaitConditionNextExit = "next-exit"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use ContainerStatus type?

// ContainerWaitConditionNotRunning matches any state that is not
// "running" (configured, created, stopped, exited, removing).
ContainerWaitConditionNotRunning = "not-running"
// ContainerWaitConditionRemoved waits until the container has been
// removed.
ContainerWaitConditionRemoved = "removed"
)

// ContainerStatus returns a string representation for users of a container
// state. All results should match Docker's versions (from `docker ps`) as
// closely as possible, given the different set of states we support.
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/server/register_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,10 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
// - exited
// - healthy
// - initialized
// - next-exit
// - not-running
// - paused
// - removed
// - removing
// - running
// - stopped
Expand Down
25 changes: 25 additions & 0 deletions test/apiv2/26-containersWait.at
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,28 @@ t POST "containers/${CTR2}/wait?condition=next-exit" 200 \
.StatusCode=42 \
.Error=null
podman rm -f "${CTR2}"

# libpod endpoint: Docker-style condition names should also be accepted.
CTR3="libpodWaitNextExit"
podman create --name "${CTR3}" "${IMAGE}" sh -c "exit 9"

# not-running matches the freshly-created container immediately and returns
# 0 (matches Docker: a never-started container is "not running" → exit 0).
t POST "libpod/containers/${CTR3}/wait?condition=not-running" 200 "0"

# next-exit must wait for the container to actually run and exit.
(sleep 2; podman start "${CTR3}") &
child_pid=$!
t POST "libpod/containers/${CTR3}/wait?condition=next-exit" 200 "9"
wait "${child_pid}"

# After the container has exited, not-running must return the recorded
# exit code (regression guard: the state-matching path would return -1).
t POST "libpod/containers/${CTR3}/wait?condition=not-running" 200 "9"

# removed waits for the container to be removed; exit code from the most
# recent run is returned.
(sleep 2; podman rm "${CTR3}") &
child_pid=$!
t POST "libpod/containers/${CTR3}/wait?condition=removed" 200 "9"
wait "${child_pid}"
95 changes: 95 additions & 0 deletions test/e2e/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package integration

import (
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "go.podman.io/podman/v6/test/utils"
Expand Down Expand Up @@ -133,4 +135,97 @@ var _ = Describe("Podman wait", func() {
waitSession.Wait(10)
Expect(waitSession.OutputToString()).To(Equal("2"))
})

It("podman wait --condition=exited on never-started container returns immediately", func() {
// Documents the long-standing semantic: a created-but-never-started
// container is "not running" right now, so --condition=exited and
// --condition=stopped return immediately with 0. Users that want to
// block until the container actually runs and exits must use
// --condition=next-exit.
podmanTest.PodmanExitCleanly("create", "--name", "never_started", ALPINE, "sh", "-c", "exit 7")

session := podmanTest.PodmanExitCleanly("wait", "--condition=exited", "never_started")
Expect(session.OutputToString()).To(Equal("0"))

session = podmanTest.PodmanExitCleanly("wait", "--condition=stopped", "never_started")
Expect(session.OutputToString()).To(Equal("0"))
})

It("podman wait --condition=next-exit waits for an actual exit", func() {
podmanTest.PodmanExitCleanly("create", "--name", "next_exit_ctr", ALPINE, "sh", "-c", "exit 7")

// Start the wait command — it should block until the container actually exits.
waitSession := podmanTest.Podman([]string{"wait", "--condition=next-exit", "next_exit_ctr"})

// Give wait a moment to subscribe before starting the container.
time.Sleep(500 * time.Millisecond)
podmanTest.PodmanExitCleanly("start", "next_exit_ctr")

waitSession.WaitWithDefaultTimeout()
Expect(waitSession).Should(ExitCleanly())
Expect(waitSession.OutputToString()).To(Equal("7"))
})

It("podman wait --condition=next-exit ignores current state for a running container", func() {
runSession := podmanTest.PodmanExitCleanly("run", "-d", "--name", "sleeper", ALPINE, "sleep", "60")
Expect(runSession.OutputToString()).ToNot(BeEmpty())

waitSession := podmanTest.Podman([]string{"wait", "--condition=next-exit", "sleeper"})
time.Sleep(500 * time.Millisecond)

// Stop the container — wait should return after this, not immediately.
podmanTest.PodmanExitCleanly("stop", "-t", "0", "sleeper")

waitSession.WaitWithDefaultTimeout()
Expect(waitSession).Should(ExitCleanly())
// Container was killed by SIGKILL on stop -t 0 → exit code 137.
Expect(waitSession.OutputToString()).ToNot(Equal("137"))
})

It("podman wait --condition=not-running on never-started container returns 0", func() {
// Docker semantic: a container that has never run is "not running"
// right now, so wait returns immediately with exit code 0.
podmanTest.PodmanExitCleanly("create", "--name", "not_running_ctr", ALPINE, "ls")

session := podmanTest.PodmanExitCleanly("wait", "--condition=not-running", "not_running_ctr")
Expect(session.OutputToString()).To(Equal("0"))
})

It("podman wait --condition=not-running on stopped container returns the real exit code", func() {
runSession := podmanTest.Podman([]string{"run", "--name", "not_running_exited_ctr", ALPINE, "sh", "-c", "exit 5"})
runSession.WaitWithDefaultTimeout()
Expect(runSession).Should(ExitWithError(5, ""))

session := podmanTest.PodmanExitCleanly("wait", "--condition=not-running", "not_running_exited_ctr")
Expect(session.OutputToString()).To(Equal("5"))
})

It("podman wait --condition=removed waits for container removal", func() {
runSession := podmanTest.PodmanExitCleanly("run", "-d", "--name", "removed_ctr", ALPINE, "sleep", "60")
Expect(runSession.OutputToString()).ToNot(BeEmpty())

waitSession := podmanTest.Podman([]string{"wait", "--condition=removed", "removed_ctr"})
time.Sleep(500 * time.Millisecond)

// -t 0 skips SIGTERM (sleep here ignores it, which would produce a
// "resorting to SIGKILL" warning on stderr and trip PodmanExitCleanly).
podmanTest.PodmanExitCleanly("rm", "-f", "-t", "0", "removed_ctr")

waitSession.WaitWithDefaultTimeout()
Expect(waitSession).Should(ExitCleanly())
})

It("podman wait --condition=created matches both internal states", func() {
// Configured state.
podmanTest.PodmanExitCleanly("create", "--name", "created_ctr_1", ALPINE, "sleep", "60")
session := podmanTest.PodmanExitCleanly("wait", "--condition=created", "created_ctr_1")
Expect(session.OutputToString()).To(Equal("-1"))

// init transitions to ContainerStateCreated (libpod "initialized");
// the CLI should still match because users see "created" for both.
podmanTest.PodmanExitCleanly("create", "--name", "created_ctr_2", ALPINE, "sleep", "60")
podmanTest.PodmanExitCleanly("init", "created_ctr_2")
session = podmanTest.PodmanExitCleanly("wait", "--condition=created", "created_ctr_2")
Expect(session.OutputToString()).To(Equal("-1"))
})
})
Loading
Loading