From ac47e80b07ddc1e56e7c4fd6b0deca9f3bdc5f54 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Fri, 10 Jan 2020 13:37:10 -0500 Subject: Add an API for Attach over HTTP API The new APIv2 branch provides an HTTP-based remote API to Podman. The requirements of this are, unfortunately, incompatible with the existing Attach API. For non-terminal attach, we need append a header to what was copied from the container, to multiplex STDOUT and STDERR; to do this with the old API, we'd need to copy into an intermediate buffer first, to handle the headers. To avoid this, provide a new API to handle all aspects of terminal and non-terminal attach, including closing the hijacked HTTP connection. This might be a bit too specific, but for now, it seems to be the simplest approach. At the same time, add a Resize endpoint. This needs to be a separate endpoint, so our existing channel approach does not work here. I wanted to rework the rest of attach at the same time (some parts of it, particularly how we start the Attach session and how we do resizing, are (in my opinion) handled much better here. That may still be on the table, but I wanted to avoid breaking existing APIs in this already massive change. Signed-off-by: Matthew Heon --- pkg/api/handlers/containers_attach.go | 159 +++++++++++++++++++++++++++++++ pkg/api/server/register_containers.go | 170 ++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 pkg/api/handlers/containers_attach.go (limited to 'pkg') diff --git a/pkg/api/handlers/containers_attach.go b/pkg/api/handlers/containers_attach.go new file mode 100644 index 000000000..eb306348b --- /dev/null +++ b/pkg/api/handlers/containers_attach.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/mux" + "github.com/gorilla/schema" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/remotecommand" +) + +func AttachContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + DetachKeys string `schema:"detachKeys"` + Logs bool `schema:"logs"` + Stream bool `schema:"stream"` + Stdin bool `schema:"stdin"` + Stdout bool `schema:"stdout"` + Stderr bool `schema:"stderr"` + }{} + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Error parsing parameters", http.StatusBadRequest, err) + return + } + + muxVars := mux.Vars(r) + + // Detach keys: explicitly set to "" is very different from unset + // TODO: Our format for parsing these may be different from Docker. + var detachKeys *string + if _, found := muxVars["detachKeys"]; found { + detachKeys = &query.DetachKeys + } + + streams := new(libpod.HTTPAttachStreams) + streams.Stdout = true + streams.Stderr = true + streams.Stdin = true + useStreams := false + if _, found := muxVars["stdin"]; found { + streams.Stdin = query.Stdin + useStreams = true + } + if _, found := muxVars["stdout"]; found { + streams.Stdout = query.Stdout + useStreams = true + } + if _, found := muxVars["stderr"]; found { + streams.Stderr = query.Stderr + useStreams = true + } + if !useStreams { + streams = nil + } + if useStreams && !streams.Stdout && !streams.Stderr && !streams.Stdin { + utils.Error(w, "Parameter conflict", http.StatusBadRequest, errors.Errorf("at least one of stdin, stdout, stderr must be true")) + return + } + + // TODO: Investigate supporting these. + // Logs replays container logs over the attach socket. + // Stream seems to break things up somehow? Not 100% clear. + if query.Logs { + utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the logs parameter to attach is not presently supported")) + return + } + // We only support stream=true or unset + if _, found := muxVars["stream"]; found && query.Stream { + utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the stream parameter to attach is not presently supported")) + return + } + + name := getName(r) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + state, err := ctr.State() + if err != nil { + utils.InternalServerError(w, err) + return + } + if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { + utils.InternalServerError(w, errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers")) + return + } + + // Hijack the connection + hijacker, ok := w.(http.Hijacker) + if !ok { + utils.InternalServerError(w, errors.Errorf("unable to hijack connection")) + return + } + + w.WriteHeader(http.StatusSwitchingProtocols) + + connection, buffer, err := hijacker.Hijack() + if err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error hijacking connection")) + return + } + + logrus.Debugf("Hijack for attach of container %s successful", ctr.ID()) + + // Perform HTTP attach. + // HTTPAttach will handle everything about the connection from here on + // (including closing it and writing errors to it). + if err := ctr.HTTPAttach(connection, buffer, streams, detachKeys, nil); err != nil { + // We can't really do anything about errors anymore. HTTPAttach + // should be writing them to the connection. + logrus.Errorf("Error attaching to container %s: %v", ctr.ID(), err) + } + + logrus.Debugf("Attach for container %s completed successfully", ctr.ID()) +} + +func ResizeContainer(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + Height uint16 `schema:"h"` + Width uint16 `schema:"w"` + }{} + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + // This is not a 400, despite the fact that is should be, for + // compatibility reasons. + utils.InternalServerError(w, errors.Wrapf(err, "error parsing query options")) + return + } + + name := getName(r) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + newSize := remotecommand.TerminalSize{ + Width: query.Width, + Height: query.Height, + } + if err := ctr.AttachResize(newSize); err != nil { + utils.InternalServerError(w, err) + return + } + // This is not a 204, even though we write nothing, for compatibility + // reasons. + utils.WriteResponse(w, http.StatusOK, "") +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index b275fa4d1..dbe194cd4 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -428,6 +428,91 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // '500': // "$ref": "#/responses/InternalError" r.HandleFunc(VersionedPath("/containers/{name:..*}/wait"), APIHandler(s.Context, generic.WaitContainer)).Methods(http.MethodPost) + // swagger:operation POST /containers/{nameOrID}/attach compat attach + // --- + // tags: + // - containers (compat) + // summary: Attach to a container + // description: Hijacks the connection to forward the container's standard streams to the client. + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: detachKeys + // required: false + // type: string + // description: keys to use for detaching from the container + // - in: query + // name: logs + // required: false + // type: bool + // description: Not yet supported + // - in: query + // name: stream + // required: false + // type: bool + // default: true + // description: If passed, must be set to true; stream=false is not yet supported + // - in: query + // name: stdout + // required: false + // type: bool + // description: Attach to container STDOUT + // - in: query + // name: stderr + // required: false + // type: bool + // description: Attach to container STDERR + // - in: query + // name: stdin + // required: false + // type: bool + // description: Attach to container STDIN + // produces: + // - application/json + // responses: + // '101': + // description: No error, connection has been hijacked for transporting streams. + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/attach"), APIHandler(s.Context, handlers.AttachContainer)).Methods(http.MethodPost) + // swagger:operation POST /containers/{nameOrID}/resize compat resize + // --- + // tags: + // - containers (compat) + // summary: Resize a container's TTY + // description: Resize the terminal attached to a container (for use with Attach). + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: h + // type: int + // required: false + // description: Height to set for the terminal, in characters + // - in: query + // name: w + // type: int + // required: false + // description: Width to set for the terminal, in characters + // produces: + // - application/json + // responses: + // '200': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/containers/{name:..*}/resize"), APIHandler(s.Context, handlers.ResizeContainer)).Methods(http.MethodPost) /* libpod endpoints @@ -823,5 +908,90 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // '500': // "$ref": "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/stop"), APIHandler(s.Context, handlers.StopContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/attach libpod attach + // --- + // tags: + // - containers + // summary: Attach to a container + // description: Hijacks the connection to forward the container's standard streams to the client. + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: detachKeys + // required: false + // type: string + // description: keys to use for detaching from the container + // - in: query + // name: logs + // required: false + // type: bool + // description: Not yet supported + // - in: query + // name: stream + // required: false + // type: bool + // default: true + // description: If passed, must be set to true; stream=false is not yet supported + // - in: query + // name: stdout + // required: false + // type: bool + // description: Attach to container STDOUT + // - in: query + // name: stderr + // required: false + // type: bool + // description: Attach to container STDERR + // - in: query + // name: stdin + // required: false + // type: bool + // description: Attach to container STDIN + // produces: + // - application/json + // responses: + // '101': + // description: No error, connection has been hijacked for transporting streams. + // '400': + // "$ref": "#/responses/BadParamError" + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/attach"), APIHandler(s.Context, handlers.AttachContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{nameOrID}/resize libpod resize + // --- + // tags: + // - containers + // summary: Resize a container's TTY + // description: Resize the terminal attached to a container (for use with Attach). + // parameters: + // - in: path + // name: nameOrID + // required: true + // description: the name or ID of the container + // - in: query + // name: h + // type: int + // required: false + // description: Height to set for the terminal, in characters + // - in: query + // name: w + // type: int + // required: false + // description: Width to set for the terminal, in characters + // produces: + // - application/json + // responses: + // '200': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchContainer" + // '500': + // "$ref": "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name:..*}/resize"), APIHandler(s.Context, handlers.ResizeContainer)).Methods(http.MethodPost) return nil } -- cgit v1.2.3-54-g00ecf