github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/api/handlers/utils/containers.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/hanks177/podman/v4/libpod/events"
    11  	api "github.com/hanks177/podman/v4/pkg/api/types"
    12  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    13  	"github.com/hanks177/podman/v4/pkg/domain/infra/abi"
    14  
    15  	"github.com/hanks177/podman/v4/pkg/api/handlers"
    16  	"github.com/sirupsen/logrus"
    17  
    18  	"github.com/hanks177/podman/v4/libpod/define"
    19  
    20  	"github.com/hanks177/podman/v4/libpod"
    21  	"github.com/gorilla/schema"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  type waitQueryDocker struct {
    26  	Condition string `schema:"condition"`
    27  }
    28  
    29  type waitQueryLibpod struct {
    30  	Interval  string                   `schema:"interval"`
    31  	Condition []define.ContainerStatus `schema:"condition"`
    32  }
    33  
    34  func WaitContainerDocker(w http.ResponseWriter, r *http.Request) {
    35  	var err error
    36  	ctx := r.Context()
    37  
    38  	query := waitQueryDocker{}
    39  
    40  	decoder := ctx.Value(api.DecoderKey).(*schema.Decoder)
    41  	if err = decoder.Decode(&query, r.URL.Query()); err != nil {
    42  		Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
    43  		return
    44  	}
    45  
    46  	interval := time.Nanosecond
    47  
    48  	condition := "not-running"
    49  	if _, found := r.URL.Query()["condition"]; found {
    50  		condition = query.Condition
    51  		if !isValidDockerCondition(query.Condition) {
    52  			BadRequest(w, "condition", condition, errors.New("not a valid docker condition"))
    53  			return
    54  		}
    55  	}
    56  
    57  	name := GetName(r)
    58  
    59  	exists, err := containerExists(ctx, name)
    60  	if err != nil {
    61  		InternalServerError(w, err)
    62  		return
    63  	}
    64  	if !exists {
    65  		ContainerNotFound(w, name, define.ErrNoSuchCtr)
    66  		return
    67  	}
    68  
    69  	// In docker compatibility mode we have to send headers in advance,
    70  	// otherwise docker client would freeze.
    71  	w.Header().Set("Content-Type", "application/json")
    72  	w.WriteHeader(200)
    73  	if flusher, ok := w.(http.Flusher); ok {
    74  		flusher.Flush()
    75  	}
    76  
    77  	exitCode, err := waitDockerCondition(ctx, name, interval, condition)
    78  	var errStruct *struct{ Message string }
    79  	if err != nil {
    80  		logrus.Errorf("While waiting on condition: %q", err)
    81  		errStruct = &struct {
    82  			Message string
    83  		}{
    84  			Message: err.Error(),
    85  		}
    86  	}
    87  
    88  	responseData := handlers.ContainerWaitOKBody{
    89  		StatusCode: int(exitCode),
    90  		Error:      errStruct,
    91  	}
    92  	enc := json.NewEncoder(w)
    93  	enc.SetEscapeHTML(true)
    94  	err = enc.Encode(&responseData)
    95  	if err != nil {
    96  		logrus.Errorf("Unable to write json: %q", err)
    97  	}
    98  }
    99  
   100  func WaitContainerLibpod(w http.ResponseWriter, r *http.Request) {
   101  	var (
   102  		err        error
   103  		interval   = time.Millisecond * 250
   104  		conditions = []define.ContainerStatus{define.ContainerStateStopped, define.ContainerStateExited}
   105  	)
   106  	decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
   107  	query := waitQueryLibpod{}
   108  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
   109  		Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
   110  		return
   111  	}
   112  
   113  	if _, found := r.URL.Query()["interval"]; found {
   114  		interval, err = time.ParseDuration(query.Interval)
   115  		if err != nil {
   116  			InternalServerError(w, err)
   117  			return
   118  		}
   119  	}
   120  
   121  	if _, found := r.URL.Query()["condition"]; found {
   122  		if len(query.Condition) > 0 {
   123  			conditions = query.Condition
   124  		}
   125  	}
   126  
   127  	name := GetName(r)
   128  
   129  	waitFn := createContainerWaitFn(r.Context(), name, interval)
   130  
   131  	exitCode, err := waitFn(conditions...)
   132  	if err != nil {
   133  		if errors.Cause(err) == define.ErrNoSuchCtr {
   134  			ContainerNotFound(w, name, err)
   135  			return
   136  		}
   137  		InternalServerError(w, err)
   138  		return
   139  	}
   140  	WriteResponse(w, http.StatusOK, strconv.Itoa(int(exitCode)))
   141  }
   142  
   143  type containerWaitFn func(conditions ...define.ContainerStatus) (int32, error)
   144  
   145  func createContainerWaitFn(ctx context.Context, containerName string, interval time.Duration) containerWaitFn {
   146  	runtime := ctx.Value(api.RuntimeKey).(*libpod.Runtime)
   147  	var containerEngine entities.ContainerEngine = &abi.ContainerEngine{Libpod: runtime}
   148  
   149  	return func(conditions ...define.ContainerStatus) (int32, error) {
   150  		opts := entities.WaitOptions{
   151  			Condition: conditions,
   152  			Interval:  interval,
   153  		}
   154  		ctrWaitReport, err := containerEngine.ContainerWait(ctx, []string{containerName}, opts)
   155  		if err != nil {
   156  			return -1, err
   157  		}
   158  		if len(ctrWaitReport) != 1 {
   159  			return -1, fmt.Errorf("the ContainerWait() function returned unexpected count of reports: %d", len(ctrWaitReport))
   160  		}
   161  		return ctrWaitReport[0].ExitCode, ctrWaitReport[0].Error
   162  	}
   163  }
   164  
   165  func isValidDockerCondition(cond string) bool {
   166  	switch cond {
   167  	case "next-exit", "removed", "not-running", "":
   168  		return true
   169  	}
   170  	return false
   171  }
   172  
   173  func waitDockerCondition(ctx context.Context, containerName string, interval time.Duration, dockerCondition string) (int32, error) {
   174  	containerWait := createContainerWaitFn(ctx, containerName, interval)
   175  
   176  	var err error
   177  	var code int32
   178  	switch dockerCondition {
   179  	case "next-exit":
   180  		code, err = waitNextExit(ctx, containerName)
   181  	case "removed":
   182  		code, err = waitRemoved(containerWait)
   183  	case "not-running", "":
   184  		code, err = waitNotRunning(containerWait)
   185  	default:
   186  		panic("not a valid docker condition")
   187  	}
   188  	return code, err
   189  }
   190  
   191  var notRunningStates = []define.ContainerStatus{
   192  	define.ContainerStateCreated,
   193  	define.ContainerStateRemoving,
   194  	define.ContainerStateStopped,
   195  	define.ContainerStateExited,
   196  	define.ContainerStateConfigured,
   197  }
   198  
   199  func waitRemoved(ctrWait containerWaitFn) (int32, error) {
   200  	code, err := ctrWait(define.ContainerStateUnknown)
   201  	if err != nil && errors.Cause(err) == define.ErrNoSuchCtr {
   202  		return code, nil
   203  	}
   204  	return code, err
   205  }
   206  
   207  func waitNextExit(ctx context.Context, containerName string) (int32, error) {
   208  	runtime := ctx.Value(api.RuntimeKey).(*libpod.Runtime)
   209  	containerEngine := &abi.ContainerEngine{Libpod: runtime}
   210  	eventChannel := make(chan *events.Event)
   211  	errChannel := make(chan error)
   212  	opts := entities.EventsOptions{
   213  		EventChan: eventChannel,
   214  		Filter:    []string{"event=died", fmt.Sprintf("container=%s", containerName)},
   215  		Stream:    true,
   216  	}
   217  
   218  	// ctx is used to cancel event watching goroutine
   219  	ctx, cancel := context.WithCancel(ctx)
   220  	defer cancel()
   221  	go func() {
   222  		errChannel <- containerEngine.Events(ctx, opts)
   223  	}()
   224  
   225  	evt, ok := <-eventChannel
   226  	if ok {
   227  		return int32(evt.ContainerExitCode), nil
   228  	}
   229  	// if ok == false then containerEngine.Events() has exited
   230  	// it may happen if request was canceled (e.g. client closed connection prematurely) or
   231  	// the server is in process of shutting down
   232  	return -1, <-errChannel
   233  }
   234  
   235  func waitNotRunning(ctrWait containerWaitFn) (int32, error) {
   236  	return ctrWait(notRunningStates...)
   237  }
   238  
   239  func containerExists(ctx context.Context, name string) (bool, error) {
   240  	runtime := ctx.Value(api.RuntimeKey).(*libpod.Runtime)
   241  	var containerEngine entities.ContainerEngine = &abi.ContainerEngine{Libpod: runtime}
   242  
   243  	var ctrExistsOpts entities.ContainerExistsOptions
   244  	ctrExistRep, err := containerEngine.ContainerExists(ctx, name, ctrExistsOpts)
   245  	if err != nil {
   246  		return false, err
   247  	}
   248  	return ctrExistRep.Value, nil
   249  }