From 7df64f88866962d3ee56dc67039f0ea1e13c49cd Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 3 Sep 2016 03:31:54 +1000 Subject: [PATCH] runc: implement --console-socket 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 --- create.go | 5 +++ exec.go | 5 +++ libcontainer/console.go | 64 ++++++++++++++++++++++++++++++++--- libcontainer/console_linux.go | 4 +-- libcontainer/init_linux.go | 3 +- run.go | 5 +++ tty.go | 16 ++++++++- utils_linux.go | 62 +++++++++++++++++++++++++++++++-- 8 files changed, 152 insertions(+), 12 deletions(-) diff --git a/create.go b/create.go index 3519ad4f..a91b7746 100644 --- a/create.go +++ b/create.go @@ -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: "", diff --git a/exec.go b/exec.go index ab2d266d..540ffeb6 100644 --- a/exec.go +++ b/exec.go @@ -29,6 +29,10 @@ following will output a list of processes running in the container: # runc exec 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", @@ -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"), } diff --git a/libcontainer/console.go b/libcontainer/console.go index 7222057e..99071481 100644 --- a/libcontainer/console.go +++ b/libcontainer/console.go @@ -1,6 +1,11 @@ package libcontainer -import "io" +import ( + "encoding/json" + "fmt" + "io" + "os" +) // Console represents a pseudo TTY. type Console interface { @@ -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 +} diff --git a/libcontainer/console_linux.go b/libcontainer/console_linux.go index 77588d78..6e38b462 100644 --- a/libcontainer/console_linux.go +++ b/libcontainer/console_linux.go @@ -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 { diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go index b6bb227d..e763bc15 100644 --- a/libcontainer/init_linux.go +++ b/libcontainer/init_linux.go @@ -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 } diff --git a/run.go b/run.go index 8f3ff7c6..a5d42abb 100644 --- a/run.go +++ b/run.go @@ -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", diff --git a/tty.go b/tty.go index dc8be8cb..a3244342 100644 --- a/tty.go +++ b/tty.go @@ -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 { @@ -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 { @@ -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) } diff --git a/utils_linux.go b/utils_linux.go index 81fdb9a6..336955db 100644 --- a/utils_linux.go +++ b/utils_linux.go @@ -5,6 +5,7 @@ package main import ( "errors" "fmt" + "net" "os" "path/filepath" "strconv" @@ -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) } @@ -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 { @@ -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 @@ -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 @@ -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() @@ -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) @@ -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,