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:
parent
f1324a9fc1
commit
7df64f8886
|
@ -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: "",
|
||||
|
|
5
exec.go
5
exec.go
|
@ -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"),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
5
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",
|
||||
|
|
16
tty.go
16
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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue