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 <asarai@suse.de>
This commit is contained in:
Aleksa Sarai 2016-09-03 03:31:54 +10:00
parent f1324a9fc1
commit 7df64f8886
No known key found for this signature in database
GPG Key ID: 9E18AA267DDB8DB4
8 changed files with 152 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

5
run.go
View File

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

16
tty.go
View File

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

View File

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