Merge pull request #6249 from jwhonce/wip/resize

V2 Implement terminal handling in bindings attach
This commit is contained in:
OpenShift Merge Robot 2020-05-18 21:58:23 +02:00 committed by GitHub
commit 9fe49335e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 177 additions and 46 deletions

View File

@ -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, "")
}

View File

@ -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, "")
}

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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
}