Implement Docker wait conditions

Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
Matej Vasek 2021-02-01 20:13:04 +01:00
parent fc385806df
commit 4a219aa234
3 changed files with 208 additions and 73 deletions

View File

@ -23,10 +23,8 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/gorilla/mux"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
func RemoveContainer(w http.ResponseWriter, r *http.Request) { func RemoveContainer(w http.ResponseWriter, r *http.Request) {
@ -233,8 +231,11 @@ func KillContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
if sig == 0 || syscall.Signal(sig) == syscall.SIGKILL { if sig == 0 || syscall.Signal(sig) == syscall.SIGKILL {
if _, err := utils.WaitContainer(w, r); err != nil { opts := entities.WaitOptions{
Condition: []define.ContainerStatus{define.ContainerStateExited, define.ContainerStateStopped},
Interval: time.Millisecond * 250,
}
if _, err := containerEngine.ContainerWait(r.Context(), []string{name}, opts); err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err)
return return
} }
@ -245,26 +246,8 @@ func KillContainer(w http.ResponseWriter, r *http.Request) {
} }
func WaitContainer(w http.ResponseWriter, r *http.Request) { func WaitContainer(w http.ResponseWriter, r *http.Request) {
var msg string
// /{version}/containers/(name)/wait // /{version}/containers/(name)/wait
exitCode, err := utils.WaitContainer(w, r) utils.WaitContainerDocker(w, r)
if err != nil {
if errors.Cause(err) == define.ErrNoSuchCtr {
logrus.Warnf("container not found %q: %v", utils.GetName(r), err)
return
}
logrus.Warnf("failed to wait on container %q: %v", mux.Vars(r)["name"], err)
return
}
utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{
StatusCode: int(exitCode),
Error: struct {
Message string
}{
Message: msg,
},
})
} }
func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error) { func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error) {

View File

@ -4,7 +4,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"strconv"
"github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/define"
@ -146,17 +145,7 @@ func GetContainer(w http.ResponseWriter, r *http.Request) {
} }
func WaitContainer(w http.ResponseWriter, r *http.Request) { func WaitContainer(w http.ResponseWriter, r *http.Request) {
exitCode, err := utils.WaitContainer(w, r) utils.WaitContainerLibpod(w, r)
if err != nil {
name := utils.GetName(r)
if errors.Cause(err) == define.ErrNoSuchCtr {
utils.ContainerNotFound(w, name, err)
return
}
logrus.Warnf("failed to wait on container %q: %v", name, err)
return
}
utils.WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode)))
} }
func UnmountContainer(w http.ResponseWriter, r *http.Request) { func UnmountContainer(w http.ResponseWriter, r *http.Request) {

View File

@ -1,67 +1,230 @@
package utils package utils
import ( import (
"context"
"fmt"
"net/http" "net/http"
"strconv"
"time" "time"
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/pkg/domain/entities" "github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/domain/infra/abi" "github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/containers/podman/v2/pkg/api/handlers"
"github.com/sirupsen/logrus"
"github.com/containers/podman/v2/libpod/define"
"github.com/containers/podman/v2/libpod"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { type waitQueryDocker struct {
var ( Condition string `schema:"condition"`
err error }
interval time.Duration
) type waitQueryLibpod struct {
runtime := r.Context().Value("runtime").(*libpod.Runtime)
// Now use the ABI implementation to prevent us from having duplicate
// code.
containerEngine := abi.ContainerEngine{Libpod: runtime}
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Interval string `schema:"interval"` Interval string `schema:"interval"`
Condition []define.ContainerStatus `schema:"condition"` Condition []define.ContainerStatus `schema:"condition"`
}{
// Override golang default values for types
} }
func WaitContainerDocker(w http.ResponseWriter, r *http.Request) {
var err error
ctx := r.Context()
query := waitQueryDocker{}
decoder := ctx.Value("decoder").(*schema.Decoder)
if err = decoder.Decode(&query, r.URL.Query()); err != nil {
Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return
}
interval := time.Nanosecond
condition := "not-running"
if _, found := r.URL.Query()["condition"]; found {
condition = query.Condition
if !isValidDockerCondition(query.Condition) {
BadRequest(w, "condition", condition, errors.New("not a valid docker condition"))
return
}
}
name := GetName(r)
exists, err := containerExists(ctx, name)
if err != nil {
InternalServerError(w, err)
return
}
if !exists {
ContainerNotFound(w, name, define.ErrNoSuchCtr)
return
}
// In docker compatibility mode we have to send headers in advance,
// otherwise docker client would freeze.
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
exitCode, err := waitDockerCondition(ctx, name, interval, condition)
msg := ""
if err != nil {
logrus.Errorf("error while waiting on condtion: %q", err)
msg = err.Error()
}
responseData := handlers.ContainerWaitOKBody{
StatusCode: int(exitCode),
Error: struct {
Message string
}{
Message: msg,
},
}
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
err = enc.Encode(&responseData)
if err != nil {
logrus.Errorf("unable to write json: %q", err)
}
}
func WaitContainerLibpod(w http.ResponseWriter, r *http.Request) {
var (
err error
interval = time.Millisecond * 250
conditions = []define.ContainerStatus{define.ContainerStateStopped, define.ContainerStateExited}
)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := waitQueryLibpod{}
if err := decoder.Decode(&query, r.URL.Query()); err != nil { if err := decoder.Decode(&query, r.URL.Query()); err != nil {
Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return 0, err
} }
options := entities.WaitOptions{
Condition: []define.ContainerStatus{define.ContainerStateStopped},
}
name := GetName(r)
if _, found := r.URL.Query()["interval"]; found { if _, found := r.URL.Query()["interval"]; found {
interval, err = time.ParseDuration(query.Interval) interval, err = time.ParseDuration(query.Interval)
if err != nil { if err != nil {
InternalServerError(w, err) InternalServerError(w, err)
return 0, err return
}
} else {
interval, err = time.ParseDuration("250ms")
if err != nil {
InternalServerError(w, err)
return 0, err
} }
} }
options.Interval = interval
if _, found := r.URL.Query()["condition"]; found { if _, found := r.URL.Query()["condition"]; found {
options.Condition = query.Condition if len(query.Condition) > 0 {
conditions = query.Condition
}
} }
report, err := containerEngine.ContainerWait(r.Context(), []string{name}, options) name := GetName(r)
waitFn := createContainerWaitFn(r.Context(), name, interval)
exitCode, err := waitFn(conditions...)
if err != nil { if err != nil {
return 0, err if errors.Cause(err) == define.ErrNoSuchCtr {
ContainerNotFound(w, name, err)
return
} else {
InternalServerError(w, err)
return
} }
if len(report) == 0 {
InternalServerError(w, errors.New("No reports returned"))
return 0, err
} }
return report[0].ExitCode, report[0].Error WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode)))
}
type containerWaitFn func(conditions ...define.ContainerStatus) (int32, error)
func createContainerWaitFn(ctx context.Context, containerName string, interval time.Duration) containerWaitFn {
runtime := ctx.Value("runtime").(*libpod.Runtime)
var containerEngine entities.ContainerEngine = &abi.ContainerEngine{Libpod: runtime}
return func(conditions ...define.ContainerStatus) (int32, error) {
opts := entities.WaitOptions{
Condition: conditions,
Interval: interval,
}
ctrWaitReport, err := containerEngine.ContainerWait(ctx, []string{containerName}, opts)
if err != nil {
return -1, err
}
if len(ctrWaitReport) != 1 {
return -1, fmt.Errorf("the ContainerWait() function returned unexpected count of reports: %d", len(ctrWaitReport))
}
return ctrWaitReport[0].ExitCode, ctrWaitReport[0].Error
}
}
func isValidDockerCondition(cond string) bool {
switch cond {
case "next-exit", "removed", "not-running", "":
return true
}
return false
}
func waitDockerCondition(ctx context.Context, containerName string, interval time.Duration, dockerCondition string) (int32, error) {
containerWait := createContainerWaitFn(ctx, containerName, interval)
var err error
var code int32
switch dockerCondition {
case "next-exit":
code, err = waitNextExit(containerWait)
case "removed":
code, err = waitRemoved(containerWait)
case "not-running", "":
code, err = waitNotRunning(containerWait)
default:
panic("not a valid docker condition")
}
return code, err
}
var notRunningStates = []define.ContainerStatus{
define.ContainerStateCreated,
define.ContainerStateRemoving,
define.ContainerStateStopped,
define.ContainerStateExited,
define.ContainerStateConfigured,
}
func waitRemoved(ctrWait containerWaitFn) (int32, error) {
code, err := ctrWait(define.ContainerStateUnknown)
if err != nil && errors.Cause(err) == define.ErrNoSuchCtr {
return code, nil
} else {
return code, err
}
}
func waitNextExit(ctrWait containerWaitFn) (int32, error) {
_, err := ctrWait(define.ContainerStateRunning)
if err != nil {
return -1, err
}
return ctrWait(notRunningStates...)
}
func waitNotRunning(ctrWait containerWaitFn) (int32, error) {
return ctrWait(notRunningStates...)
}
func containerExists(ctx context.Context, name string) (bool, error) {
runtime := ctx.Value("runtime").(*libpod.Runtime)
var containerEngine entities.ContainerEngine = &abi.ContainerEngine{Libpod: runtime}
var ctrExistsOpts entities.ContainerExistsOptions
ctrExistRep, err := containerEngine.ContainerExists(ctx, name, ctrExistsOpts)
if err != nil {
return false, err
}
return ctrExistRep.Value, nil
} }