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 --- libpod/oci_conmon_linux.go | 264 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) (limited to 'libpod/oci_conmon_linux.go') diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index 5ab0e73c4..0e8a64865 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -5,8 +5,11 @@ package libpod import ( "bufio" "bytes" + "encoding/binary" "fmt" + "io" "io/ioutil" + "net" "os" "os/exec" "path/filepath" @@ -17,6 +20,7 @@ import ( "text/template" "time" + conmonConfig "github.com/containers/conmon/runner/config" "github.com/containers/libpod/libpod/config" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/cgroups" @@ -33,6 +37,13 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" + "k8s.io/client-go/tools/remotecommand" +) + +const ( + // This is Conmon's STDIO_BUF_SIZE. I don't believe we have access to it + // directly from the Go cose, so const it here + bufferSize = conmonConfig.BufSize ) // ConmonOCIRuntime is an OCI runtime managed by Conmon. @@ -465,6 +476,123 @@ func (r *ConmonOCIRuntime) UnpauseContainer(ctr *Container) error { return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "resume", ctr.ID()) } +// HTTPAttach performs an attach for the HTTP API. +// This will consume, and automatically close, the hijacked HTTP session. +// It is not necessary to close it independently. +// The cancel channel is not closed; it is up to the caller to do so after +// this function returns. +// If this is a container with a terminal, we will stream raw. If it is not, we +// will stream with an 8-byte header to multiplex STDOUT and STDERR. +func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, httpConn net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool) (deferredErr error) { + isTerminal := false + if ctr.config.Spec.Process != nil { + isTerminal = ctr.config.Spec.Process.Terminal + } + + // Ensure that our contract of closing the HTTP connection is honored. + defer hijackWriteErrorAndClose(deferredErr, ctr.ID(), httpConn, httpBuf) + + if streams != nil { + if isTerminal { + return errors.Wrapf(define.ErrInvalidArg, "cannot specify which streams to attach as container %s has a terminal", ctr.ID()) + } + if !streams.Stdin && !streams.Stdout && !streams.Stderr { + return errors.Wrapf(define.ErrInvalidArg, "must specify at least one stream to attach to") + } + } + + attachSock, err := r.AttachSocketPath(ctr) + if err != nil { + return err + } + socketPath := buildSocketPath(attachSock) + + conn, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: socketPath, Net: "unixpacket"}) + if err != nil { + return errors.Wrapf(err, "failed to connect to container's attach socket: %v", socketPath) + } + defer func() { + if err := conn.Close(); err != nil { + logrus.Errorf("unable to close container %s attach socket: %q", ctr.ID(), err) + } + }() + + logrus.Debugf("Successfully connected to container %s attach socket %s", ctr.ID(), socketPath) + + detachString := define.DefaultDetachKeys + if detachKeys != nil { + detachString = *detachKeys + } + detach, err := processDetachKeys(detachString) + if err != nil { + return err + } + + // Make a channel to pass errors back + errChan := make(chan error) + + attachStdout := true + attachStderr := true + attachStdin := true + if streams != nil { + attachStdout = streams.Stdout + attachStderr = streams.Stderr + attachStdin = streams.Stdin + } + + // Handle STDOUT/STDERR + go func() { + var err error + if isTerminal { + logrus.Debugf("Performing terminal HTTP attach for container %s", ctr.ID()) + err = httpAttachTerminalCopy(conn, httpBuf, ctr.ID()) + } else { + logrus.Debugf("Performing non-terminal HTTP attach for container %s", ctr.ID()) + err = httpAttachNonTerminalCopy(conn, httpBuf, ctr.ID(), attachStdin, attachStdout, attachStderr) + } + errChan <- err + logrus.Debugf("STDOUT/ERR copy completed") + }() + // Next, STDIN. Avoid entirely if attachStdin unset. + if attachStdin { + go func() { + _, err := utils.CopyDetachable(conn, httpBuf, detach) + logrus.Debugf("STDIN copy completed") + errChan <- err + }() + } + + if cancel != nil { + select { + case err := <-errChan: + return err + case <-cancel: + return nil + } + } else { + var connErr error = <-errChan + return connErr + } +} + +// AttachResize resizes the terminal used by the given container. +func (r *ConmonOCIRuntime) AttachResize(ctr *Container, newSize remotecommand.TerminalSize) error { + // TODO: probably want a dedicated function to get ctl file path? + controlPath := filepath.Join(ctr.bundlePath(), "ctl") + controlFile, err := os.OpenFile(controlPath, unix.O_WRONLY, 0) + if err != nil { + return errors.Wrapf(err, "could not open ctl file for terminal resize") + } + defer controlFile.Close() + + logrus.Debugf("Received a resize event for container %s: %+v", ctr.ID(), newSize) + if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 1, newSize.Height, newSize.Width); err != nil { + return errors.Wrapf(err, "failed to write to ctl file to resize terminal") + } + + return nil +} + // ExecContainer executes a command in a running container // TODO: Split into Create/Start/Attach/Wait func (r *ConmonOCIRuntime) ExecContainer(c *Container, sessionID string, options *ExecOptions) (int, chan error, error) { @@ -1465,3 +1593,139 @@ func (r *ConmonOCIRuntime) getOCIRuntimeVersion() (string, error) { } return strings.TrimSuffix(output, "\n"), nil } + +// Copy data from container to HTTP connection, for terminal attach. +// Container is the container's attach socket connection, http is a buffer for +// the HTTP connection. cid is the ID of the container the attach session is +// running for (used solely for error messages). +func httpAttachTerminalCopy(container *net.UnixConn, http *bufio.ReadWriter, cid string) error { + buf := make([]byte, bufferSize) + for { + numR, err := container.Read(buf) + if numR > 0 { + switch buf[0] { + case AttachPipeStdout: + // Do nothing + default: + logrus.Errorf("Received unexpected attach type %+d, discarding %d bytes", buf[0], numR) + continue + } + + numW, err2 := http.Write(buf[1:numR]) + if err2 != nil { + if err != nil { + logrus.Errorf("Error reading container %s STDOUT: %v", cid, err) + } + return err2 + } else if numW+1 != numR { + return io.ErrShortWrite + } + // We need to force the buffer to write immediately, so + // there isn't a delay on the terminal side. + if err2 := http.Flush(); err2 != nil { + if err != nil { + logrus.Errorf("Error reading container %s STDOUT: %v", cid, err) + } + return err2 + } + } + if err != nil { + if err == io.EOF { + return nil + } + return err + } + } +} + +// Copy data from a container to an HTTP connection, for non-terminal attach. +// Appends a header to multiplex input. +func httpAttachNonTerminalCopy(container *net.UnixConn, http *bufio.ReadWriter, cid string, stdin, stdout, stderr bool) error { + buf := make([]byte, bufferSize) + for { + numR, err := container.Read(buf) + if numR > 0 { + headerBuf := []byte{0, 0, 0, 0} + + // Practically speaking, we could make this buf[0] - 1, + // but we need to validate it anyways... + switch buf[0] { + case AttachPipeStdin: + headerBuf[0] = 0 + if !stdin { + continue + } + case AttachPipeStdout: + if !stdout { + continue + } + headerBuf[0] = 1 + case AttachPipeStderr: + if !stderr { + continue + } + headerBuf[0] = 2 + default: + logrus.Errorf("Received unexpected attach type %+d, discarding %d bytes", buf[0], numR) + continue + } + + // Get big-endian length and append. + // Subtract 1 because we strip the first byte (used for + // multiplexing by Conmon). + lenBuf := []byte{0, 0, 0, 0} + binary.BigEndian.PutUint32(lenBuf, uint32(numR-1)) + headerBuf = append(headerBuf, lenBuf...) + + numH, err2 := http.Write(headerBuf) + if err2 != nil { + if err != nil { + logrus.Errorf("Error reading container %s standard streams: %v", cid, err) + } + + return err2 + } + // Hardcoding header length is pretty gross, but + // fast. Should be safe, as this is a fixed part + // of the protocol. + if numH != 8 { + if err != nil { + logrus.Errorf("Error reading container %s standard streams: %v", cid, err) + } + + return io.ErrShortWrite + } + + numW, err2 := http.Write(buf[1:numR]) + if err2 != nil { + if err != nil { + logrus.Errorf("Error reading container %s standard streams: %v", cid, err) + } + + return err2 + } else if numW+1 != numR { + if err != nil { + logrus.Errorf("Error reading container %s standard streams: %v", cid, err) + } + + return io.ErrShortWrite + } + // We need to force the buffer to write immediately, so + // there isn't a delay on the terminal side. + if err2 := http.Flush(); err2 != nil { + if err != nil { + logrus.Errorf("Error reading container %s STDOUT: %v", cid, err) + } + return err2 + } + } + if err != nil { + if err == io.EOF { + return nil + } + + return err + } + } + +} -- cgit v1.2.3-54-g00ecf