diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/container_api.go | 69 | ||||
-rw-r--r-- | libpod/oci.go | 29 | ||||
-rw-r--r-- | libpod/oci_conmon_linux.go | 264 | ||||
-rw-r--r-- | libpod/oci_missing.go | 13 | ||||
-rw-r--r-- | libpod/util.go | 20 |
5 files changed, 393 insertions, 2 deletions
diff --git a/libpod/container_api.go b/libpod/container_api.go index e36623529..d74a14f15 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -5,6 +5,7 @@ import ( "context" "io" "io/ioutil" + "net" "os" "time" @@ -374,7 +375,9 @@ type AttachStreams struct { AttachInput bool } -// Attach attaches to a container +// Attach attaches to a container. +// This function returns when the attach finishes. It does not hold the lock for +// the duration of its runtime, only using it at the beginning to verify state. func (c *Container) Attach(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize) error { if !c.batched { c.lock.Lock() @@ -382,6 +385,7 @@ func (c *Container) Attach(streams *AttachStreams, keys string, resize <-chan re c.lock.Unlock() return err } + // We are NOT holding the lock for the duration of the function. c.lock.Unlock() } @@ -389,10 +393,71 @@ func (c *Container) Attach(streams *AttachStreams, keys string, resize <-chan re return errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers") } - defer c.newContainerEvent(events.Attach) + c.newContainerEvent(events.Attach) return c.attach(streams, keys, resize, false, nil) } +// HTTPAttach forwards an attach session over a hijacked HTTP session. +// HTTPAttach will consume and close the included httpCon, which is expected to +// be sourced from a hijacked HTTP connection. +// The cancel channel is optional, and can be used to asyncronously cancel the +// attach session. +// The streams variable is only supported if the container was not a terminal, +// and allows specifying which of the container's standard streams will be +// forwarded to the client. +// This function returns when the attach finishes. It does not hold the lock for +// the duration of its runtime, only using it at the beginning to verify state. +func (c *Container) HTTPAttach(httpCon net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool) error { + if !c.batched { + c.lock.Lock() + if err := c.syncContainer(); err != nil { + c.lock.Unlock() + + // Write any errors to the HTTP buffer before we close. + hijackWriteErrorAndClose(err, c.ID(), httpCon, httpBuf) + + return err + } + // We are NOT holding the lock for the duration of the function. + c.lock.Unlock() + } + + if !c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning) { + toReturn := errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers") + + // Write any errors to the HTTP buffer before we close. + hijackWriteErrorAndClose(toReturn, c.ID(), httpCon, httpBuf) + + return toReturn + } + + logrus.Infof("Performing HTTP Hijack attach to container %s", c.ID()) + + c.newContainerEvent(events.Attach) + return c.ociRuntime.HTTPAttach(c, httpCon, httpBuf, streams, detachKeys, cancel) +} + +// AttachResize resizes the container's terminal, which is displayed by Attach +// and HTTPAttach. +func (c *Container) AttachResize(newSize remotecommand.TerminalSize) error { + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + if !c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning) { + return errors.Wrapf(define.ErrCtrStateInvalid, "can only resize created or running containers") + } + + logrus.Infof("Resizing TTY of container %s", c.ID()) + + return c.ociRuntime.AttachResize(c, newSize) +} + // Mount mounts a container's filesystem on the host // The path where the container has been mounted is returned func (c *Container) Mount() (string, error) { diff --git a/libpod/oci.go b/libpod/oci.go index 05a2f37db..2ea61851f 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -1,6 +1,9 @@ package libpod import ( + "bufio" + "net" + "k8s.io/client-go/tools/remotecommand" ) @@ -47,6 +50,23 @@ type OCIRuntime interface { // UnpauseContainer unpauses the given container. UnpauseContainer(ctr *Container) error + // HTTPAttach performs an attach intended to be transported over HTTP. + // For terminal attach, the container's output will be directly streamed + // to output; otherwise, STDOUT and STDERR will be multiplexed, with + // a header prepended as follows: 1-byte STREAM (0, 1, 2 for STDIN, + // STDOUT, STDERR), 3 null (0x00) bytes, 4-byte big endian length. + // If a cancel channel is provided, it can be used to asyncronously + // termninate the attach session. Detach keys, if given, will also cause + // the attach session to be terminated if provided via the STDIN + // channel. If they are not provided, the default detach keys will be + // used instead. Detach keys of "" will disable detaching via keyboard. + // The streams parameter may be passed for containers that did not + // create a terminal and will determine which streams to forward to the + // client. + HTTPAttach(ctr *Container, httpConn net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool) error + // AttachResize resizes the terminal in use by the given container. + AttachResize(ctr *Container, newSize remotecommand.TerminalSize) error + // ExecContainer executes a command in a running container. // Returns an int (exit code), error channel (errors from attach), and // error (errors that occurred attempting to start the exec session). @@ -130,3 +150,12 @@ type ExecOptions struct { // detach from the container. DetachKeys string } + +// HTTPAttachStreams informs the HTTPAttach endpoint which of the container's +// standard streams should be streamed to the client. If this is passed, at +// least one of the streams must be set to true. +type HTTPAttachStreams struct { + Stdin bool + Stdout bool + Stderr bool +} diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index 7cd42f9ab..722012386 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) { @@ -1476,3 +1604,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 + } + } + +} diff --git a/libpod/oci_missing.go b/libpod/oci_missing.go index 0faa1805b..ff7eea625 100644 --- a/libpod/oci_missing.go +++ b/libpod/oci_missing.go @@ -1,13 +1,16 @@ package libpod import ( + "bufio" "fmt" + "net" "path/filepath" "sync" "github.com/containers/libpod/libpod/define" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/remotecommand" ) var ( @@ -107,6 +110,16 @@ func (r *MissingRuntime) UnpauseContainer(ctr *Container) error { return r.printError() } +// HTTPAttach is not available as the runtime is missing +func (r *MissingRuntime) HTTPAttach(ctr *Container, httpConn net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool) error { + return r.printError() +} + +// AttachResize is not available as the runtime is missing +func (r *MissingRuntime) AttachResize(ctr *Container, newSize remotecommand.TerminalSize) error { + return r.printError() +} + // ExecContainer is not available as the runtime is missing func (r *MissingRuntime) ExecContainer(ctr *Container, sessionID string, options *ExecOptions) (int, chan error, error) { return -1, nil, r.printError() diff --git a/libpod/util.go b/libpod/util.go index 30e5cd4c3..f79d6c09b 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -1,7 +1,9 @@ package libpod import ( + "bufio" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -16,6 +18,7 @@ import ( "github.com/fsnotify/fsnotify" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // Runtime API constants @@ -231,3 +234,20 @@ func checkDependencyContainer(depCtr, ctr *Container) error { return nil } + +// hijackWriteErrorAndClose writes an error to a hijacked HTTP session and +// closes it. Intended to HTTPAttach function. +// If error is nil, it will not be written; we'll only close the connection. +func hijackWriteErrorAndClose(toWrite error, cid string, httpCon io.Closer, httpBuf *bufio.ReadWriter) { + if toWrite != nil { + if _, err := httpBuf.Write([]byte(toWrite.Error())); err != nil { + logrus.Errorf("Error writing error %q to container %s HTTP attach connection: %v", toWrite, cid, err) + } else if err := httpBuf.Flush(); err != nil { + logrus.Errorf("Error flushing HTTP buffer for container %s HTTP attach connection: %v", cid, err) + } + } + + if err := httpCon.Close(); err != nil { + logrus.Errorf("Error closing container %s HTTP attach connection: %v", cid, err) + } +} |