Skip to content

Commit

Permalink
runc: implement --console-socket
Browse files Browse the repository at this point in the history
This allows for higher-level orchestrators to be able to have access to
the master pty file descriptor without keeping the runC process running.
This is key to having (detach && createTTY) with a _real_ pty created
inside the container, which is then sent to a higher level orchestrator
over an AF_UNIX socket.

This patch is part of the console rewrite patchset.

Signed-off-by: Aleksa Sarai <[email protected]>
  • Loading branch information
cyphar committed Dec 1, 2016
1 parent f1324a9 commit 7df64f8
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 12 deletions.
5 changes: 5 additions & 0 deletions create.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ command(s) that get executed on start, edit the args parameter of the spec. See
Value: "",
Usage: `path to the root of the bundle directory, defaults to the current directory`,
},
cli.StringFlag{
Name: "console-socket",
Value: "",
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
},
cli.StringFlag{
Name: "pid-file",
Value: "",
Expand Down
5 changes: 5 additions & 0 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ following will output a list of processes running in the container:
# runc exec <container-id> ps`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "console-socket",
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
},
cli.StringFlag{
Name: "cwd",
Usage: "current working directory in the container",
Expand Down Expand Up @@ -127,6 +131,7 @@ func execProcess(context *cli.Context) (int, error) {
enableSubreaper: false,
shouldDestroy: false,
container: container,
consoleSocket: context.String("console-socket"),
detach: detach,
pidFile: context.String("pid-file"),
}
Expand Down
64 changes: 60 additions & 4 deletions libcontainer/console.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package libcontainer

import "io"
import (
"encoding/json"
"fmt"
"io"
"os"
)

// Console represents a pseudo TTY.
type Console interface {
Expand All @@ -11,8 +16,59 @@ type Console interface {
Path() string

// Fd returns the fd for the master of the pty.
Fd() uintptr
File() *os.File
}

// ConsoleData represents arbitrary setup data used when setting up console
// handling. It is
const (
TerminalInfoVersion uint32 = 201610041
TerminalInfoType uint8 = 'T'
)

// TerminalInfo is the structure which is passed as the non-ancillary data
// in the sendmsg(2) call when runc is run with --console-socket. It
// contains some information about the container which the console master fd
// relates to (to allow for consumers to use a single unix socket to handle
// multiple containers). This structure will probably move to runtime-spec
// at some point. But for now it lies in libcontainer.
type TerminalInfo struct {
// Version of the API.
Version uint32 `json:"version"`

// Type of message (future proofing).
Type uint8 `json:"type"`

// Container contains the ID of the container.
ContainerID string `json:"container_id"`
}

func (ti *TerminalInfo) String() string {
encoded, err := json.Marshal(*ti)
if err != nil {
panic(err)
}
return string(encoded)
}

func NewTerminalInfo(containerId string) *TerminalInfo {
return &TerminalInfo{
Version: TerminalInfoVersion,
Type: TerminalInfoType,
ContainerID: containerId,
}
}

func GetTerminalInfo(encoded string) (*TerminalInfo, error) {
ti := new(TerminalInfo)
if err := json.Unmarshal([]byte(encoded), ti); err != nil {
return nil, err
}

if ti.Type != TerminalInfoType {
return nil, fmt.Errorf("terminal info: incorrect type in payload (%q): %q", TerminalInfoType, ti.Type)
}
if ti.Version != TerminalInfoVersion {
return nil, fmt.Errorf("terminal info: incorrect version in payload (%q): %q", TerminalInfoVersion, ti.Version)
}

return ti, nil
}
4 changes: 2 additions & 2 deletions libcontainer/console_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ type linuxConsole struct {
slavePath string
}

func (c *linuxConsole) Fd() uintptr {
return c.master.Fd()
func (c *linuxConsole) File() *os.File {
return c.master
}

func (c *linuxConsole) Path() string {
Expand Down
3 changes: 1 addition & 2 deletions libcontainer/init_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,7 @@ func setupConsole(pipe *os.File, config *initConfig, mount bool) error {
}

// While we can access console.master, using the API is a good idea.
consoleFile := os.NewFile(linuxConsole.Fd(), "[master-pty]")
if err := utils.SendFd(pipe, consoleFile); err != nil {
if err := utils.SendFd(pipe, linuxConsole.File()); err != nil {
return err
}

Expand Down
5 changes: 5 additions & 0 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ command(s) that get executed on start, edit the args parameter of the spec. See
Value: "",
Usage: `path to the root of the bundle directory, defaults to the current directory`,
},
cli.StringFlag{
Name: "console-socket",
Value: "",
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
},
cli.BoolFlag{
Name: "detach, d",
Usage: "detach from the container's process",
Expand Down
16 changes: 15 additions & 1 deletion tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/docker/docker/pkg/term"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/utils"
)

type tty struct {
Expand Down Expand Up @@ -100,6 +101,19 @@ func (t *tty) recvtty(process *libcontainer.Process, detach bool) error {
return nil
}

func (t *tty) sendtty(socket *os.File, ti *libcontainer.TerminalInfo) error {
if t.console == nil {
return fmt.Errorf("tty.console not set")
}

// Create a fake file to contain the terminal info.
console := os.NewFile(t.console.File().Fd(), ti.String())
if err := utils.SendFd(socket, console); err != nil {
return err
}
return nil
}

// ClosePostStart closes any fds that are provided to the container and dup2'd
// so that we no longer have copy in our process.
func (t *tty) ClosePostStart() error {
Expand Down Expand Up @@ -135,5 +149,5 @@ func (t *tty) resize() error {
if err != nil {
return err
}
return term.SetWinsize(t.console.Fd(), ws)
return term.SetWinsize(t.console.File().Fd(), ws)
}
62 changes: 59 additions & 3 deletions utils_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -121,13 +122,13 @@ func setupIO(process *libcontainer.Process, rootuid, rootgid int, createTTY, det
// requirement that we set up anything nice for our caller or the
// container.
if detach {
// TODO: Actually set rootuid, rootgid.
if err := dupStdio(process, rootuid, rootgid); err != nil {
return nil, err
}
return &tty{}, nil
}

// XXX: This doesn't sit right with me. It's ugly.
return createStdioPipes(process, rootuid, rootgid)
}

Expand Down Expand Up @@ -180,10 +181,15 @@ type runner struct {
detach bool
listenFDs []*os.File
pidFile string
consoleSocket string
container libcontainer.Container
create bool
}

func (r *runner) terminalinfo() *libcontainer.TerminalInfo {
return libcontainer.NewTerminalInfo(r.container.ID())
}

func (r *runner) run(config *specs.Process) (int, error) {
process, err := newProcess(*config)
if err != nil {
Expand All @@ -194,16 +200,32 @@ func (r *runner) run(config *specs.Process) (int, error) {
process.Env = append(process.Env, fmt.Sprintf("LISTEN_FDS=%d", len(r.listenFDs)), "LISTEN_PID=1")
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
}

rootuid, err := r.container.Config().HostUID()
if err != nil {
r.destroy()
return -1, err
}

rootgid, err := r.container.Config().HostGID()
if err != nil {
r.destroy()
return -1, err
}

detach := r.detach || r.create

// Check command-line for sanity.
if detach && config.Terminal && r.consoleSocket == "" {
r.destroy()
return -1, fmt.Errorf("cannot allocate tty if runc will detach without setting console socket")
}
// XXX: Should we change this?
if (!detach || !config.Terminal) && r.consoleSocket != "" {
r.destroy()
return -1, fmt.Errorf("cannot use console socket if runc will not detach or allocate tty")
}

startFn := r.container.Start
if !r.create {
startFn = r.container.Run
Expand All @@ -212,7 +234,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
// with detaching containers, and then we get a tty after the container has
// started.
handler := newSignalHandler(r.enableSubreaper)
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, r.detach || r.create)
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach)
if err != nil {
r.destroy()
return -1, err
Expand All @@ -229,6 +251,39 @@ func (r *runner) run(config *specs.Process) (int, error) {
}
}
defer tty.Close()

if config.Terminal && detach {
conn, err := net.Dial("unix", r.consoleSocket)
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
defer conn.Close()

unixconn, ok := conn.(*net.UnixConn)
if !ok {
r.terminate(process)
r.destroy()
return -1, fmt.Errorf("casting to UnixConn failed")
}

socket, err := unixconn.File()
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
defer socket.Close()

err = tty.sendtty(socket, r.terminalinfo())
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
}

if err := tty.ClosePostStart(); err != nil {
r.terminate(process)
r.destroy()
Expand All @@ -241,7 +296,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
return -1, err
}
}
if r.detach || r.create {
if detach {
return 0, nil
}
status, err := handler.forward(process, tty)
Expand Down Expand Up @@ -295,6 +350,7 @@ func startContainer(context *cli.Context, spec *specs.Spec, create bool) (int, e
shouldDestroy: true,
container: container,
listenFDs: listenFDs,
consoleSocket: context.String("console-socket"),
detach: context.Bool("detach"),
pidFile: context.String("pid-file"),
create: create,
Expand Down

0 comments on commit 7df64f8

Please sign in to comment.