mirror of https://github.com/containers/podman.git
container wait: support health states
Support two new wait conditions, "healthy" and "unhealthy". This further paves the way for integrating sdnotify with health checks which is currently being tracked in #6160. Fixes: #13627 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
This commit is contained in:
parent
811867249b
commit
1398cbce8a
|
@ -1474,7 +1474,9 @@ func AutocompleteImageSaveFormat(cmd *cobra.Command, args []string, toComplete s
|
||||||
// AutocompleteWaitCondition - Autocomplete wait condition options.
|
// AutocompleteWaitCondition - Autocomplete wait condition options.
|
||||||
// -> "unknown", "configured", "created", "running", "stopped", "paused", "exited", "removing"
|
// -> "unknown", "configured", "created", "running", "stopped", "paused", "exited", "removing"
|
||||||
func AutocompleteWaitCondition(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
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
|
return states, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ with different exit codes, but `podman wait` can only display and detect one.
|
||||||
## OPTIONS
|
## OPTIONS
|
||||||
|
|
||||||
#### **--condition**=*state*
|
#### **--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 "created", "exited", "initialized", "paused", "removing", "running", "stopped", "stopping". 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", "paused", "removing", "running", "stopped", "stopping", "unhealthy". The default condition is "stopped".
|
||||||
|
|
||||||
#### **--help**, **-h**
|
#### **--help**, **-h**
|
||||||
|
|
||||||
|
|
|
@ -698,8 +698,16 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
|
||||||
resultChan := make(chan waitResult)
|
resultChan := make(chan waitResult)
|
||||||
waitForExit := false
|
waitForExit := false
|
||||||
wantedStates := make(map[define.ContainerStatus]bool, len(conditions))
|
wantedStates := make(map[define.ContainerStatus]bool, len(conditions))
|
||||||
|
wantedHealthStates := make(map[string]bool)
|
||||||
|
|
||||||
for _, rawCondition := range conditions {
|
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:
|
||||||
condition, err := define.StringToContainerStatus(rawCondition)
|
condition, err := define.StringToContainerStatus(rawCondition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
|
@ -711,6 +719,7 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
|
||||||
wantedStates[condition] = true
|
wantedStates[condition] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trySend := func(code int32, err error) {
|
trySend := func(code int32, err error) {
|
||||||
select {
|
select {
|
||||||
|
@ -731,12 +740,13 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(wantedStates) > 0 {
|
if len(wantedStates) > 0 || len(wantedHealthStates) > 0 {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
if len(wantedStates) > 0 {
|
||||||
state, err := c.State()
|
state, err := c.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
trySend(-1, err)
|
trySend(-1, err)
|
||||||
|
@ -746,6 +756,18 @@ func (c *Container) WaitForConditionWithInterval(ctx context.Context, waitTimeou
|
||||||
trySend(-1, nil)
|
trySend(-1, nil)
|
||||||
return
|
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 {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|
|
@ -29,7 +29,7 @@ type waitQueryDocker struct {
|
||||||
|
|
||||||
type waitQueryLibpod struct {
|
type waitQueryLibpod struct {
|
||||||
Interval string `schema:"interval"`
|
Interval string `schema:"interval"`
|
||||||
Condition []define.ContainerStatus `schema:"condition"`
|
Conditions []string `schema:"condition"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitContainerDocker(w http.ResponseWriter, r *http.Request) {
|
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)
|
name := GetName(r)
|
||||||
|
reports, err := containerEngine.ContainerWait(r.Context(), []string{name}, opts)
|
||||||
waitFn := createContainerWaitFn(r.Context(), name, interval)
|
|
||||||
exitCode, err := waitFn(query.Condition...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, define.ErrNoSuchCtr) {
|
if errors.Is(err, define.ErrNoSuchCtr) {
|
||||||
ContainerNotFound(w, name, err)
|
ContainerNotFound(w, name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
InternalServerError(w, err)
|
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
|
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)
|
type containerWaitFn func(conditions ...define.ContainerStatus) (int32, error)
|
||||||
|
|
|
@ -1229,12 +1229,15 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
|
||||||
// enum:
|
// enum:
|
||||||
// - configured
|
// - configured
|
||||||
// - created
|
// - created
|
||||||
|
// - exited
|
||||||
|
// - healthy
|
||||||
|
// - initialized
|
||||||
|
// - paused
|
||||||
|
// - removing
|
||||||
// - running
|
// - running
|
||||||
// - stopped
|
// - stopped
|
||||||
// - paused
|
|
||||||
// - exited
|
|
||||||
// - removing
|
|
||||||
// - stopping
|
// - stopping
|
||||||
|
// - unhealthy
|
||||||
// description: "Conditions to wait for. If no condition provided the 'exited' condition is assumed."
|
// description: "Conditions to wait for. If no condition provided the 'exited' condition is assumed."
|
||||||
// - in: query
|
// - in: query
|
||||||
// name: interval
|
// name: interval
|
||||||
|
|
|
@ -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) {
|
func Wait(ctx context.Context, nameOrID string, options *WaitOptions) (int32, error) {
|
||||||
if options == nil {
|
if options == nil {
|
||||||
options = new(WaitOptions)
|
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
|
var exitCode int32
|
||||||
conn, err := bindings.GetClient(ctx)
|
conn, err := bindings.GetClient(ctx)
|
||||||
|
@ -351,6 +353,7 @@ func Wait(ctx context.Context, nameOrID string, options *WaitOptions) (int32, er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return exitCode, err
|
return exitCode, err
|
||||||
}
|
}
|
||||||
|
delete(params, "conditions") // They're called "condition"
|
||||||
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/wait", params, nil, nameOrID)
|
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/wait", params, nil, nameOrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return exitCode, err
|
return exitCode, err
|
||||||
|
|
|
@ -229,8 +229,14 @@ type UnpauseOptions struct{}
|
||||||
//
|
//
|
||||||
//go:generate go run ../generator/generator.go WaitOptions
|
//go:generate go run ../generator/generator.go WaitOptions
|
||||||
type WaitOptions struct {
|
type WaitOptions struct {
|
||||||
Condition []define.ContainerStatus
|
// 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
|
Interval *string
|
||||||
|
// Container status to wait on.
|
||||||
|
// Deprecated: use Conditions instead.
|
||||||
|
Condition []define.ContainerStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopOptions are optional options for stopping containers
|
// StopOptions are optional options for stopping containers
|
||||||
|
|
|
@ -18,19 +18,19 @@ func (o *WaitOptions) ToParams() (url.Values, error) {
|
||||||
return util.ToParams(o)
|
return util.ToParams(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCondition set field Condition to given value
|
// WithConditions set field Conditions to given value
|
||||||
func (o *WaitOptions) WithCondition(value []define.ContainerStatus) *WaitOptions {
|
func (o *WaitOptions) WithConditions(value []string) *WaitOptions {
|
||||||
o.Condition = value
|
o.Conditions = value
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCondition returns value of field Condition
|
// GetConditions returns value of field Conditions
|
||||||
func (o *WaitOptions) GetCondition() []define.ContainerStatus {
|
func (o *WaitOptions) GetConditions() []string {
|
||||||
if o.Condition == nil {
|
if o.Conditions == nil {
|
||||||
var z []define.ContainerStatus
|
var z []string
|
||||||
return z
|
return z
|
||||||
}
|
}
|
||||||
return o.Condition
|
return o.Conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithInterval set field Interval to given value
|
// WithInterval set field Interval to given value
|
||||||
|
@ -47,3 +47,18 @@ func (o *WaitOptions) GetInterval() string {
|
||||||
}
|
}
|
||||||
return *o.Interval
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -38,17 +38,8 @@ func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrID string,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, opts entities.WaitOptions) ([]entities.WaitReport, error) {
|
func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, opts entities.WaitOptions) ([]entities.WaitReport, error) {
|
||||||
conditions := make([]define.ContainerStatus, 0, len(opts.Conditions))
|
|
||||||
for _, condition := range opts.Conditions {
|
|
||||||
cond, err := define.StringToContainerStatus(condition)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conditions = append(conditions, cond)
|
|
||||||
}
|
|
||||||
|
|
||||||
responses := make([]entities.WaitReport, 0, len(namesOrIds))
|
responses := make([]entities.WaitReport, 0, len(namesOrIds))
|
||||||
options := new(containers.WaitOptions).WithCondition(conditions).WithInterval(opts.Interval.String())
|
options := new(containers.WaitOptions).WithConditions(opts.Conditions).WithInterval(opts.Interval.String())
|
||||||
for _, n := range namesOrIds {
|
for _, n := range namesOrIds {
|
||||||
response := entities.WaitReport{}
|
response := entities.WaitReport{}
|
||||||
exitCode, err := containers.Wait(ic.ClientCtx, n, options)
|
exitCode, err := containers.Wait(ic.ClientCtx, n, options)
|
||||||
|
|
|
@ -83,7 +83,7 @@ Log[-1].Output | \"Uh-oh on stdout!\\\nUh-oh on stderr!\"
|
||||||
_build_health_check_image $img cleanfile
|
_build_health_check_image $img cleanfile
|
||||||
run_podman run -d --name $ctr \
|
run_podman run -d --name $ctr \
|
||||||
--health-cmd /healthcheck \
|
--health-cmd /healthcheck \
|
||||||
--health-retries=2 \
|
--health-retries=3 \
|
||||||
--health-interval=disable \
|
--health-interval=disable \
|
||||||
$img
|
$img
|
||||||
|
|
||||||
|
@ -105,6 +105,46 @@ Log[-1].Output | \"Uh-oh on stdout!\\\nUh-oh on stderr!\"
|
||||||
run_podman rmi $img
|
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" {
|
@test "podman healthcheck --health-on-failure" {
|
||||||
run_podman 125 create --health-on-failure=kill $IMAGE
|
run_podman 125 create --health-on-failure=kill $IMAGE
|
||||||
is "$output" "Error: cannot set on-failure action to kill without a health check"
|
is "$output" "Error: cannot set on-failure action to kill without a health check"
|
||||||
|
|
Loading…
Reference in New Issue