diff options
author | Jhon Honce <jhonce@redhat.com> | 2020-05-15 09:33:21 -0700 |
---|---|---|
committer | Jhon Honce <jhonce@redhat.com> | 2020-05-18 11:21:06 -0700 |
commit | 6287a5585223e5934706d1c1c21d68bfac5b5803 (patch) | |
tree | 835ea460df979073ce36050149b105eed7a4a2ae | |
parent | d4587c6074e4b4e3673cf495f0c4cd2811742791 (diff) | |
download | podman-6287a5585223e5934706d1c1c21d68bfac5b5803.tar.gz podman-6287a5585223e5934706d1c1c21d68bfac5b5803.tar.bz2 podman-6287a5585223e5934706d1c1c21d68bfac5b5803.zip |
V2 Implement terminal handling in bindings attach
* Add support for /exec/{id}/resize
* Add support for ErrSessionNotFound
* Resize container TTY as stdin changes size
* Refactor all resize functions into one handler
Signed-off-by: Jhon Honce <jhonce@redhat.com>
-rw-r--r-- | pkg/api/handlers/compat/containers_attach.go | 36 | ||||
-rw-r--r-- | pkg/api/handlers/compat/resize.go | 68 | ||||
-rw-r--r-- | pkg/api/handlers/utils/errors.go | 8 | ||||
-rw-r--r-- | pkg/api/server/register_containers.go | 6 | ||||
-rw-r--r-- | pkg/api/server/register_exec.go | 4 | ||||
-rw-r--r-- | pkg/bindings/containers/containers.go | 101 |
6 files changed, 177 insertions, 46 deletions
diff --git a/pkg/api/handlers/compat/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index 3c9a6fd69..5fc3117b9 100644 --- a/pkg/api/handlers/compat/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -10,7 +10,6 @@ import ( "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "k8s.io/client-go/tools/remotecommand" ) // AttachHeader is the literal header sent for upgraded/hijacked connections for @@ -127,38 +126,3 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { 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 := utils.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/handlers/compat/resize.go b/pkg/api/handlers/compat/resize.go new file mode 100644 index 000000000..3ead733bc --- /dev/null +++ b/pkg/api/handlers/compat/resize.go @@ -0,0 +1,68 @@ +package compat + +import ( + "net/http" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/gorilla/schema" + "github.com/pkg/errors" + "k8s.io/client-go/tools/remotecommand" +) + +func ResizeTTY(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + // /containers/{id}/resize + query := struct { + height uint16 `schema:"h"` + width uint16 `schema:"w"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + sz := remotecommand.TerminalSize{ + Width: query.width, + Height: query.height, + } + + var status int + name := utils.GetName(r) + switch { + case strings.Contains(r.URL.Path, "/containers/"): + ctnr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if err := ctnr.AttachResize(sz); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "cannot resize container")) + return + } + // This is not a 204, even though we write nothing, for compatibility + // reasons. + status = http.StatusOK + case strings.Contains(r.URL.Path, "/exec/"): + ctnr, err := runtime.GetExecSessionContainer(name) + if err != nil { + utils.SessionNotFound(w, name, err) + return + } + if err := ctnr.ExecResize(name, sz); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "cannot resize session")) + return + } + // This is not a 204, even though we write nothing, for compatibility + // reasons. + status = http.StatusCreated + } + utils.WriteResponse(w, status, "") +} diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index 3253a9be3..c17720694 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -63,6 +63,14 @@ func PodNotFound(w http.ResponseWriter, name string, err error) { Error(w, msg, http.StatusNotFound, err) } +func SessionNotFound(w http.ResponseWriter, name string, err error) { + if errors.Cause(err) != define.ErrNoSuchExecSession { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such exec session: %s", name) + Error(w, msg, http.StatusNotFound, err) +} + func ContainerNotRunning(w http.ResponseWriter, containerID string, err error) { msg := fmt.Sprintf("Container %s is not running", containerID) Error(w, msg, http.StatusConflict, err) diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 378d1e06c..0d78e4cdb 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -584,9 +584,9 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/{name}/resize"), s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost) // Added non version path to URI to support docker non versioned paths - r.HandleFunc("/containers/{name}/resize", s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + r.HandleFunc("/containers/{name}/resize", s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost) // swagger:operation GET /containers/{name}/export compat exportContainer // --- // tags: @@ -1259,7 +1259,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchContainer" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/containers/{name}/resize"), s.APIHandler(compat.ResizeContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/{name}/resize"), s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/{name}/export libpod libpodExportContainer // --- // tags: diff --git a/pkg/api/server/register_exec.go b/pkg/api/server/register_exec.go index 19b7e2fcd..1533edba9 100644 --- a/pkg/api/server/register_exec.go +++ b/pkg/api/server/register_exec.go @@ -145,9 +145,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // $ref: "#/responses/NoSuchExecInstance" // 500: // $ref: "#/responses/InternalError" - r.Handle(VersionedPath("/exec/{id}/resize"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) + r.Handle(VersionedPath("/exec/{id}/resize"), s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost) // Added non version path to URI to support docker non versioned paths - r.Handle("/exec/{id}/resize", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) + r.Handle("/exec/{id}/resize", s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost) // swagger:operation GET /exec/{id}/json compat inspectExec // --- // tags: diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index b77ef208d..f0984b8e3 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -7,8 +7,11 @@ import ( "io" "net/http" "net/url" + "os" + "os/signal" "strconv" "strings" + "syscall" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" @@ -16,6 +19,7 @@ import ( "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh/terminal" ) var ( @@ -374,11 +378,58 @@ func Attach(ctx context.Context, nameOrId string, detachKeys *string, logs, stre params.Add("stderr", "true") } + // Unless all requirements are met, don't use "stdin" is a terminal + file, ok := stdin.(*os.File) + needTTY := ok && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty + if needTTY { + state, err := terminal.MakeRaw(int(file.Fd())) + if err != nil { + return err + } + + logrus.SetFormatter(&rawFormatter{}) + + defer func() { + if err := terminal.Restore(int(file.Fd()), state); err != nil { + logrus.Errorf("unable to restore terminal: %q", err) + } + logrus.SetFormatter(&logrus.TextFormatter{}) + }() + + winChange := make(chan os.Signal, 1) + signal.Notify(winChange, syscall.SIGWINCH) + winCtx, winCancel := context.WithCancel(ctx) + defer winCancel() + + go func() { + // Prime the pump, we need one reset to ensure everything is ready + winChange <- syscall.SIGWINCH + for { + select { + case <-winCtx.Done(): + return + case <-winChange: + h, w, err := terminal.GetSize(int(file.Fd())) + if err != nil { + logrus.Warnf("failed to obtain TTY size: " + err.Error()) + } + + if err := ResizeContainerTTY(ctx, nameOrId, &h, &w); err != nil { + logrus.Warnf("failed to resize TTY: " + err.Error()) + } + } + } + }() + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/attach", params, nameOrId) if err != nil { return err } defer response.Body.Close() + if !(response.IsSuccess() || response.IsInformational()) { + return response.Process(nil) + } if stdin != nil { go func() { @@ -401,11 +452,8 @@ func Attach(ctx context.Context, nameOrId string, detachKeys *string, logs, stre // Read multiplexed channels and write to appropriate stream fd, l, err := DemuxHeader(response.Body, buffer) if err != nil { - switch { - case errors.Is(err, io.EOF): + if errors.Is(err, io.EOF) { return nil - case errors.Is(err, io.ErrUnexpectedEOF): - continue } return err } @@ -437,7 +485,7 @@ func Attach(ctx context.Context, nameOrId string, detachKeys *string, logs, stre } } } - return err + return nil } // DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel @@ -477,3 +525,46 @@ func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error return buffer[0:length], nil } + +// ResizeContainerTTY sets container's TTY height and width in characters +func ResizeContainerTTY(ctx context.Context, nameOrId string, height *int, width *int) error { + return resizeTTY(ctx, bindings.JoinURL("containers", nameOrId, "resize"), height, width) +} + +// ResizeExecTTY sets session's TTY height and width in characters +func ResizeExecTTY(ctx context.Context, nameOrId string, height *int, width *int) error { + return resizeTTY(ctx, bindings.JoinURL("exec", nameOrId, "resize"), height, width) +} + +// resizeTTY set size of TTY of container +func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + + params := url.Values{} + if height != nil { + params.Set("h", strconv.Itoa(*height)) + } + if width != nil { + params.Set("w", strconv.Itoa(*width)) + } + rsp, err := conn.DoRequest(nil, http.MethodPost, endpoint, params) + if err != nil { + return err + } + return rsp.Process(nil) +} + +type rawFormatter struct { + logrus.TextFormatter +} + +func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) { + buffer, err := f.TextFormatter.Format(entry) + if err != nil { + return buffer, err + } + return append(buffer, '\r'), nil +} |