Merge pull request #18974 from vrothberg/fix-13627

podman wait: support healthy/unhealthy
This commit is contained in:
OpenShift Merge Robot
2023-06-23 16:03:50 +02:00
committed by GitHub
15 changed files with 177 additions and 69 deletions

View File

@ -1474,7 +1474,9 @@ func AutocompleteImageSaveFormat(cmd *cobra.Command, args []string, toComplete s
// AutocompleteWaitCondition - Autocomplete wait condition options.
// -> "unknown", "configured", "created", "running", "stopped", "paused", "exited", "removing"
func AutocompleteWaitCondition(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
states := []string{"unknown", "configured", "created", "running", "stopped", "paused", "exited", "removing"}
states := []string{"unknown", "configured", "created", "exited",
"healthy", "initialized", "paused", "removing", "running",
"stopped", "stopping", "unhealthy"}
return states, cobra.ShellCompDirectiveNoFileComp
}

View File

@ -185,15 +185,11 @@ func execWait(ctr string, seconds int32) error {
ctx, cancel := context.WithTimeout(registry.Context(), maxDuration)
defer cancel()
cond, err := define.StringToContainerStatus("running")
if err != nil {
return err
}
waitOptions.Condition = append(waitOptions.Condition, cond)
waitOptions.Conditions = []string{define.ContainerStateRunning.String()}
startTime := time.Now()
for time.Since(startTime) < maxDuration {
_, err = registry.ContainerEngine().ContainerWait(ctx, []string{ctr}, waitOptions)
_, err := registry.ContainerEngine().ContainerWait(ctx, []string{ctr}, waitOptions)
if err == nil {
return nil
}

View File

@ -11,7 +11,6 @@ import (
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/cmd/podman/validate"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/spf13/cobra"
)
@ -41,9 +40,8 @@ var (
)
var (
waitOptions = entities.WaitOptions{}
waitConditions []string
waitInterval string
waitOptions = entities.WaitOptions{}
waitInterval string
)
func waitFlags(cmd *cobra.Command) {
@ -56,7 +54,7 @@ func waitFlags(cmd *cobra.Command) {
flags.BoolVarP(&waitOptions.Ignore, "ignore", "", false, "Ignore if a container does not exist")
conditionFlagName := "condition"
flags.StringSliceVar(&waitConditions, conditionFlagName, []string{}, "Condition to wait on")
flags.StringSliceVar(&waitOptions.Conditions, conditionFlagName, []string{}, "Condition to wait on")
_ = cmd.RegisterFlagCompletionFunc(conditionFlagName, common.AutocompleteWaitCondition)
}
@ -95,14 +93,6 @@ func wait(cmd *cobra.Command, args []string) error {
return errors.New("--latest and containers cannot be used together")
}
for _, condition := range waitConditions {
cond, err := define.StringToContainerStatus(condition)
if err != nil {
return err
}
waitOptions.Condition = append(waitOptions.Condition, cond)
}
responses, err := registry.ContainerEngine().ContainerWait(context.Background(), args, waitOptions)
if err != nil {
return err

View File

@ -11,8 +11,10 @@ podman\-wait - Wait on one or more containers to stop and print their exit codes
## DESCRIPTION
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 specified containers are stopped, the containers' return codes are printed
separated by newline in the same order as they were given to the command.
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".
NOTE: there is an inherent race condition when waiting for containers with a
restart policy of `always` or `on-failure`, such as those created by `podman
@ -22,7 +24,7 @@ with different exit codes, but `podman wait` can only display and detect one.
## OPTIONS
#### **--condition**=*state*
Condition to wait on (default "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", "paused", "removing", "running", "stopped", "stopping", "unhealthy". The default condition is "stopped".
#### **--help**, **-h**

View File

@ -683,7 +683,7 @@ type waitResult struct {
err error
}
func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeout time.Duration, conditions ...define.ContainerStatus) (int32, error) {
func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeout time.Duration, conditions ...string) (int32, error) {
if !c.valid {
return -1, define.ErrCtrRemoved
}
@ -698,13 +698,26 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
resultChan := make(chan waitResult)
waitForExit := false
wantedStates := make(map[define.ContainerStatus]bool, len(conditions))
wantedHealthStates := make(map[string]bool)
for _, condition := range conditions {
switch condition {
case define.ContainerStateExited, define.ContainerStateStopped:
waitForExit = true
for _, rawCondition := range conditions {
switch rawCondition {
case define.HealthCheckHealthy, define.HealthCheckUnhealthy:
if !c.HasHealthCheck() {
return -1, fmt.Errorf("cannot use condition %q: container %s has no healthcheck", rawCondition, c.ID())
}
wantedHealthStates[rawCondition] = true
default:
wantedStates[condition] = true
condition, err := define.StringToContainerStatus(rawCondition)
if err != nil {
return -1, err
}
switch condition {
case define.ContainerStateExited, define.ContainerStateStopped:
waitForExit = true
default:
wantedStates[condition] = true
}
}
}
@ -727,20 +740,33 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
}()
}
if len(wantedStates) > 0 {
if len(wantedStates) > 0 || len(wantedHealthStates) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
for {
state, err := c.State()
if err != nil {
trySend(-1, err)
return
if len(wantedStates) > 0 {
state, err := c.State()
if err != nil {
trySend(-1, err)
return
}
if _, found := wantedStates[state]; found {
trySend(-1, nil)
return
}
}
if _, found := wantedStates[state]; found {
trySend(-1, nil)
return
if len(wantedHealthStates) > 0 {
status, err := c.HealthCheckStatus()
if err != nil {
trySend(-1, err)
return
}
if _, found := wantedHealthStates[status]; found {
trySend(-1, nil)
return
}
}
select {
case <-ctx.Done():

View File

@ -256,8 +256,8 @@ func KillContainer(w http.ResponseWriter, r *http.Request) {
}
if sig == 0 || sig == syscall.SIGKILL {
opts := entities.WaitOptions{
Condition: []define.ContainerStatus{define.ContainerStateExited, define.ContainerStateStopped},
Interval: time.Millisecond * 250,
Conditions: []string{define.ContainerStateExited.String(), define.ContainerStateStopped.String()},
Interval: time.Millisecond * 250,
}
if _, err := containerEngine.ContainerWait(r.Context(), []string{name}, opts); err != nil {
utils.Error(w, http.StatusInternalServerError, err)

View File

@ -28,8 +28,8 @@ type waitQueryDocker struct {
}
type waitQueryLibpod struct {
Interval string `schema:"interval"`
Condition []define.ContainerStatus `schema:"condition"`
Interval string `schema:"interval"`
Conditions []string `schema:"condition"`
}
func WaitContainerDocker(w http.ResponseWriter, r *http.Request) {
@ -118,19 +118,27 @@ func WaitContainerLibpod(w http.ResponseWriter, r *http.Request) {
}
}
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
containerEngine := &abi.ContainerEngine{Libpod: runtime}
opts := entities.WaitOptions{
Conditions: query.Conditions,
Interval: interval,
}
name := GetName(r)
waitFn := createContainerWaitFn(r.Context(), name, interval)
exitCode, err := waitFn(query.Condition...)
reports, err := containerEngine.ContainerWait(r.Context(), []string{name}, opts)
if err != nil {
if errors.Is(err, define.ErrNoSuchCtr) {
ContainerNotFound(w, name, err)
return
}
InternalServerError(w, err)
}
if len(reports) != 1 {
Error(w, http.StatusInternalServerError, fmt.Errorf("the ContainerWait() function returned unexpected count of reports: %d", len(reports)))
return
}
WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode)))
WriteResponse(w, http.StatusOK, strconv.Itoa(int(reports[0].ExitCode)))
}
type containerWaitFn func(conditions ...define.ContainerStatus) (int32, error)
@ -140,9 +148,13 @@ func createContainerWaitFn(ctx context.Context, containerName string, interval t
var containerEngine entities.ContainerEngine = &abi.ContainerEngine{Libpod: runtime}
return func(conditions ...define.ContainerStatus) (int32, error) {
var rawConditions []string
for _, con := range conditions {
rawConditions = append(rawConditions, con.String())
}
opts := entities.WaitOptions{
Condition: conditions,
Interval: interval,
Conditions: rawConditions,
Interval: interval,
}
ctrWaitReport, err := containerEngine.ContainerWait(ctx, []string{containerName}, opts)
if err != nil {

View File

@ -1229,12 +1229,15 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
// enum:
// - configured
// - created
// - exited
// - healthy
// - initialized
// - paused
// - removing
// - running
// - stopped
// - paused
// - exited
// - removing
// - stopping
// - unhealthy
// description: "Conditions to wait for. If no condition provided the 'exited' condition is assumed."
// - in: query
// name: interval

View File

@ -341,6 +341,8 @@ func Unpause(ctx context.Context, nameOrID string, options *UnpauseOptions) erro
func Wait(ctx context.Context, nameOrID string, options *WaitOptions) (int32, error) {
if options == nil {
options = new(WaitOptions)
} else if len(options.Condition) > 0 && len(options.Conditions) > 0 {
return -1, fmt.Errorf("%q field cannot be used with deprecated %q field", "Conditions", "Condition")
}
var exitCode int32
conn, err := bindings.GetClient(ctx)
@ -351,6 +353,7 @@ func Wait(ctx context.Context, nameOrID string, options *WaitOptions) (int32, er
if err != nil {
return exitCode, err
}
delete(params, "conditions") // They're called "condition"
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/wait", params, nil, nameOrID)
if err != nil {
return exitCode, err

View File

@ -229,8 +229,14 @@ type UnpauseOptions struct{}
//
//go:generate go run ../generator/generator.go WaitOptions
type WaitOptions struct {
// Conditions to wait on. Includes container statuses such as
// "running" or "stopped" and health-related values such "healthy".
Conditions []string `schema:"condition"`
// Time interval to wait before polling for completion.
Interval *string
// Container status to wait on.
// Deprecated: use Conditions instead.
Condition []define.ContainerStatus
Interval *string
}
// StopOptions are optional options for stopping containers

View File

@ -18,19 +18,19 @@ func (o *WaitOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithCondition set field Condition to given value
func (o *WaitOptions) WithCondition(value []define.ContainerStatus) *WaitOptions {
o.Condition = value
// WithConditions set field Conditions to given value
func (o *WaitOptions) WithConditions(value []string) *WaitOptions {
o.Conditions = value
return o
}
// GetCondition returns value of field Condition
func (o *WaitOptions) GetCondition() []define.ContainerStatus {
if o.Condition == nil {
var z []define.ContainerStatus
// GetConditions returns value of field Conditions
func (o *WaitOptions) GetConditions() []string {
if o.Conditions == nil {
var z []string
return z
}
return o.Condition
return o.Conditions
}
// WithInterval set field Interval to given value
@ -47,3 +47,18 @@ func (o *WaitOptions) GetInterval() string {
}
return *o.Interval
}
// WithCondition set field Condition to given value
func (o *WaitOptions) WithCondition(value []define.ContainerStatus) *WaitOptions {
o.Condition = value
return o
}
// GetCondition returns value of field Condition
func (o *WaitOptions) GetCondition() []define.ContainerStatus {
if o.Condition == nil {
var z []define.ContainerStatus
return z
}
return o.Condition
}

View File

@ -49,15 +49,25 @@ type ContainerRunlabelOptions struct {
// ContainerRunlabelReport contains the results from executing container-runlabel.
type ContainerRunlabelReport struct{}
// WaitOptions are arguments for waiting for a container.
type WaitOptions struct {
Condition []define.ContainerStatus
Interval time.Duration
Ignore bool
Latest bool
// Conditions to wait on. Includes container statuses such as
// "running" or "stopped" and health-related values such "healthy".
Conditions []string
// Time interval to wait before polling for completion.
Interval time.Duration
// Ignore errors when a specified container is missing and mark its
// return code as -1.
Ignore bool
// Use the latest created container.
Latest bool
}
// WaitReport is the result of waiting a container.
type WaitReport struct {
Error error
// Error while waiting.
Error error
// ExitCode of the container.
ExitCode int32
}

View File

@ -185,10 +185,13 @@ func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []strin
}
response := entities.WaitReport{}
if options.Condition == nil {
options.Condition = []define.ContainerStatus{define.ContainerStateStopped, define.ContainerStateExited}
var conditions []string
if len(options.Conditions) == 0 {
conditions = []string{define.ContainerStateStopped.String(), define.ContainerStateExited.String()}
} else {
conditions = options.Conditions
}
exitCode, err := c.WaitForConditionWithInterval(ctx, options.Interval, options.Condition...)
exitCode, err := c.WaitForConditionWithInterval(ctx, options.Interval, conditions...)
if err != nil {
response.Error = err
} else {

View File

@ -39,7 +39,7 @@ func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrID string,
func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, opts entities.WaitOptions) ([]entities.WaitReport, error) {
responses := make([]entities.WaitReport, 0, len(namesOrIds))
options := new(containers.WaitOptions).WithCondition(opts.Condition).WithInterval(opts.Interval.String())
options := new(containers.WaitOptions).WithConditions(opts.Conditions).WithInterval(opts.Interval.String())
for _, n := range namesOrIds {
response := entities.WaitReport{}
exitCode, err := containers.Wait(ic.ClientCtx, n, options)

View File

@ -83,7 +83,7 @@ Log[-1].Output | \"Uh-oh on stdout!\\\nUh-oh on stderr!\"
_build_health_check_image $img cleanfile
run_podman run -d --name $ctr \
--health-cmd /healthcheck \
--health-retries=2 \
--health-retries=3 \
--health-interval=disable \
$img
@ -105,6 +105,46 @@ Log[-1].Output | \"Uh-oh on stdout!\\\nUh-oh on stderr!\"
run_podman rmi $img
}
@test "podman wait --condition={healthy,unhealthy}" {
ctr="healthcheck_c"
img="healthcheck_i"
wait_file="$PODMAN_TMPDIR/$(random_string).wait_for_me"
_build_health_check_image $img
for condition in healthy unhealthy;do
rm -f $wait_file
run_podman run -d --name $ctr \
--health-cmd /healthcheck \
--health-retries=1 \
--health-interval=disable \
$img
if [[ $condition == "unhealthy" ]];then
# create the uh-oh file to let the health check fail
run_podman exec $ctr touch /uh-oh
fi
# Wait for the container in the background and create the $wait_file to
# signal the specified wait condition was met.
(timeout --foreground -v --kill=5 5 $PODMAN wait --condition=$condition $ctr && touch $wait_file) &
# Sleep 1 second to make sure above commands are running
sleep 1
if [[ -f $wait_file ]]; then
die "the wait file should only be created after the container turned healthy"
fi
if [[ $condition == "healthy" ]];then
run_podman healthcheck run $ctr
else
run_podman 1 healthcheck run $ctr
fi
wait_for_file $wait_file
run_podman rm -f -t0 $ctr
done
run_podman rmi $img
}
@test "podman healthcheck --health-on-failure" {
run_podman 125 create --health-on-failure=kill $IMAGE
is "$output" "Error: cannot set on-failure action to kill without a health check"