From fc385806dfe1d13a7d4e4bdaeea93a22a55bd3d4 Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Mon, 1 Feb 2021 21:23:44 +0100 Subject: Improve ContainerEngine.ContainerWait() Signed-off-by: Matej Vasek --- pkg/api/handlers/utils/containers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'pkg/api') diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index fac237f87..7443f9b46 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -23,8 +23,8 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { containerEngine := abi.ContainerEngine{Libpod: runtime} decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Interval string `schema:"interval"` - Condition define.ContainerStatus `schema:"condition"` + Interval string `schema:"interval"` + Condition []define.ContainerStatus `schema:"condition"` }{ // Override golang default values for types } @@ -33,7 +33,7 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { return 0, err } options := entities.WaitOptions{ - Condition: define.ContainerStateStopped, + Condition: []define.ContainerStatus{define.ContainerStateStopped}, } name := GetName(r) if _, found := r.URL.Query()["interval"]; found { -- cgit v1.2.3-54-g00ecf From 4a219aa234ff4ed3cd9d139ca88d8d5da6406493 Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Mon, 1 Feb 2021 20:13:04 +0100 Subject: Implement Docker wait conditions Signed-off-by: Matej Vasek --- pkg/api/handlers/compat/containers.go | 29 +---- pkg/api/handlers/libpod/containers.go | 13 +- pkg/api/handlers/utils/containers.go | 231 +++++++++++++++++++++++++++++----- 3 files changed, 204 insertions(+), 69 deletions(-) (limited to 'pkg/api') diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 86508f938..9c0893a80 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -23,10 +23,8 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" "github.com/docker/go-units" - "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func RemoveContainer(w http.ResponseWriter, r *http.Request) { @@ -233,8 +231,11 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { return } 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) return } @@ -245,26 +246,8 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { } func WaitContainer(w http.ResponseWriter, r *http.Request) { - var msg string // /{version}/containers/(name)/wait - exitCode, err := utils.WaitContainer(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, - }, - }) + utils.WaitContainerDocker(w, r) } func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error) { diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index f6e348cef..619cbfd8b 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -4,7 +4,6 @@ import ( "io/ioutil" "net/http" "os" - "strconv" "github.com/containers/podman/v2/libpod" "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) { - exitCode, err := utils.WaitContainer(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))) + utils.WaitContainerLibpod(w, r) } func UnmountContainer(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/utils/containers.go b/pkg/api/handlers/utils/containers.go index 7443f9b46..518309a03 100644 --- a/pkg/api/handlers/utils/containers.go +++ b/pkg/api/handlers/utils/containers.go @@ -1,67 +1,230 @@ package utils import ( + "context" + "fmt" "net/http" + "strconv" "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/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/pkg/errors" ) -func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) { +type waitQueryDocker struct { + Condition string `schema:"condition"` +} + +type waitQueryLibpod struct { + Interval string `schema:"interval"` + Condition []define.ContainerStatus `schema:"condition"` +} + +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.Duration + err error + interval = time.Millisecond * 250 + conditions = []define.ContainerStatus{define.ContainerStateStopped, define.ContainerStateExited} ) - 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"` - Condition []define.ContainerStatus `schema:"condition"` - }{ - // Override golang default values for types - } + query := waitQueryLibpod{} 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 0, err - } - options := entities.WaitOptions{ - Condition: []define.ContainerStatus{define.ContainerStateStopped}, } - name := GetName(r) + if _, found := r.URL.Query()["interval"]; found { interval, err = time.ParseDuration(query.Interval) if err != nil { InternalServerError(w, err) - return 0, err + return } - } else { - interval, err = time.ParseDuration("250ms") - if err != nil { + } + + if _, found := r.URL.Query()["condition"]; found { + if len(query.Condition) > 0 { + conditions = query.Condition + } + } + + name := GetName(r) + + waitFn := createContainerWaitFn(r.Context(), name, interval) + + exitCode, err := waitFn(conditions...) + if err != nil { + if errors.Cause(err) == define.ErrNoSuchCtr { + ContainerNotFound(w, name, err) + return + } else { InternalServerError(w, err) - return 0, err + return } } - options.Interval = interval + WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode))) +} - if _, found := r.URL.Query()["condition"]; found { - options.Condition = query.Condition +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 } +} - report, err := containerEngine.ContainerWait(r.Context(), []string{name}, options) +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 0, err + return -1, err } - if len(report) == 0 { - InternalServerError(w, errors.New("No reports returned")) - return 0, 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 report[0].ExitCode, report[0].Error + return ctrExistRep.Value, nil } -- cgit v1.2.3-54-g00ecf