//go:build !remote package utils import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/containers/podman/v5/libpod/events" api "github.com/containers/podman/v5/pkg/api/types" "github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/infra/abi" "github.com/containers/podman/v5/pkg/api/handlers" "github.com/sirupsen/logrus" "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/libpod" "github.com/gorilla/schema" ) type waitQueryDocker struct { Condition string `schema:"condition"` } type waitQueryLibpod struct { Interval string `schema:"interval"` Conditions []string `schema:"condition"` } func WaitContainerDocker(w http.ResponseWriter, r *http.Request) { var err error ctx := r.Context() query := waitQueryDocker{} decoder := ctx.Value(api.DecoderKey).(*schema.Decoder) if err = decoder.Decode(&query, r.URL.Query()); err != nil { Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } interval := time.Millisecond * 250 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().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } exitCode, err := waitDockerCondition(ctx, name, interval, condition) var errStruct *struct{ Message string } if err != nil { logrus.Errorf("While waiting on condition: %q", err) errStruct = &struct { Message string }{ Message: err.Error(), } } responseData := handlers.ContainerWaitOKBody{ StatusCode: int(exitCode), Error: errStruct, } 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 ) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := waitQueryLibpod{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } if _, found := r.URL.Query()["interval"]; found { interval, err = time.ParseDuration(query.Interval) if err != nil { InternalServerError(w, err) return } } runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) containerEngine := &abi.ContainerEngine{Libpod: runtime} opts := entities.WaitOptions{ Conditions: query.Conditions, Interval: interval, } name := GetName(r) reports, err := containerEngine.ContainerWait(r.Context(), []string{name}, opts) if err != nil { if errors.Is(err, define.ErrNoSuchCtr) { // Special case: In the common scenario of podman-remote run --rm // the API is required to attach + start + wait to get exit code. // This has the problem that the wait call races against the container // removal from the cleanup process so it may not get the exit code back. // However we keep the exit code around for longer than the container so // we can just look it up here. Of course this only works when we get a // full id as param but podman-remote will do that if len(opts.Conditions) == 0 { if code, err := runtime.GetContainerExitCode(name); err == nil { WriteResponse(w, http.StatusOK, strconv.Itoa(int(code))) return } } 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(reports[0].ExitCode))) } type containerWaitFn func(conditions ...define.ContainerStatus) (int32, error) func createContainerWaitFn(ctx context.Context, containerName string, interval time.Duration) containerWaitFn { runtime := ctx.Value(api.RuntimeKey).(*libpod.Runtime) 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{ Conditions: rawConditions, 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(ctx, containerName) 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.ContainerStateExited, define.ContainerStateConfigured, } func waitRemoved(ctrWait containerWaitFn) (int32, error) { var code int32 for { c, err := ctrWait(define.ContainerStateExited) if errors.Is(err, define.ErrNoSuchCtr) { // Make sure to wait until the container has been removed. break } if err != nil { return code, err } // If the container doesn't exist, the return code is -1, so // only set it in case of success. code = c } return code, nil } func waitNextExit(ctx context.Context, containerName string) (int32, error) { runtime := ctx.Value(api.RuntimeKey).(*libpod.Runtime) containerEngine := &abi.ContainerEngine{Libpod: runtime} eventChannel := make(chan events.ReadResult) opts := entities.EventsOptions{ EventChan: eventChannel, Filter: []string{"event=died", fmt.Sprintf("container=%s", containerName)}, Stream: true, } // ctx is used to cancel event watching goroutine ctx, cancel := context.WithCancel(ctx) defer cancel() err := containerEngine.Events(ctx, opts) if err != nil { return -1, err } for evt := range eventChannel { if evt.Error == nil { if evt.Event.ContainerExitCode != nil { return int32(*evt.Event.ContainerExitCode), nil } } } // if we are here then containerEngine.Events() has exited // it may happen if request was canceled (e.g. client closed connection prematurely) or // the server is in process of shutting down return -1, nil } func waitNotRunning(ctrWait containerWaitFn) (int32, error) { return ctrWait(notRunningStates...) } func containerExists(ctx context.Context, name string) (bool, error) { runtime := ctx.Value(api.RuntimeKey).(*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 } // PSTitles merges CAPS headers from ps output. All PS headers are single words, except for // CAPS. Function compines CAP Headers into single field separated by a space. func PSTitles(output string) []string { var titles []string for _, title := range strings.Fields(output) { switch title { case "AMBIENT", "INHERITED", "PERMITTED", "EFFECTIVE", "BOUNDING": { titles = append(titles, title+" CAPS") } case "CAPS": continue default: titles = append(titles, title) } } return titles }