Skip to content

Commit 7df64f8

Browse files
committed
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 <[email protected]>
1 parent f1324a9 commit 7df64f8

File tree

8 files changed

+152
-12
lines changed

8 files changed

+152
-12
lines changed

create.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ command(s) that get executed on start, edit the args parameter of the spec. See
2929
Value: "",
3030
Usage: `path to the root of the bundle directory, defaults to the current directory`,
3131
},
32+
cli.StringFlag{
33+
Name: "console-socket",
34+
Value: "",
35+
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
36+
},
3237
cli.StringFlag{
3338
Name: "pid-file",
3439
Value: "",

exec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ following will output a list of processes running in the container:
2929
3030
# runc exec <container-id> ps`,
3131
Flags: []cli.Flag{
32+
cli.StringFlag{
33+
Name: "console-socket",
34+
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
35+
},
3236
cli.StringFlag{
3337
Name: "cwd",
3438
Usage: "current working directory in the container",
@@ -127,6 +131,7 @@ func execProcess(context *cli.Context) (int, error) {
127131
enableSubreaper: false,
128132
shouldDestroy: false,
129133
container: container,
134+
consoleSocket: context.String("console-socket"),
130135
detach: detach,
131136
pidFile: context.String("pid-file"),
132137
}

libcontainer/console.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package libcontainer
22

3-
import "io"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
)
49

510
// Console represents a pseudo TTY.
611
type Console interface {
@@ -11,8 +16,59 @@ type Console interface {
1116
Path() string
1217

1318
// Fd returns the fd for the master of the pty.
14-
Fd() uintptr
19+
File() *os.File
1520
}
1621

17-
// ConsoleData represents arbitrary setup data used when setting up console
18-
// handling. It is
22+
const (
23+
TerminalInfoVersion uint32 = 201610041
24+
TerminalInfoType uint8 = 'T'
25+
)
26+
27+
// TerminalInfo is the structure which is passed as the non-ancillary data
28+
// in the sendmsg(2) call when runc is run with --console-socket. It
29+
// contains some information about the container which the console master fd
30+
// relates to (to allow for consumers to use a single unix socket to handle
31+
// multiple containers). This structure will probably move to runtime-spec
32+
// at some point. But for now it lies in libcontainer.
33+
type TerminalInfo struct {
34+
// Version of the API.
35+
Version uint32 `json:"version"`
36+
37+
// Type of message (future proofing).
38+
Type uint8 `json:"type"`
39+
40+
// Container contains the ID of the container.
41+
ContainerID string `json:"container_id"`
42+
}
43+
44+
func (ti *TerminalInfo) String() string {
45+
encoded, err := json.Marshal(*ti)
46+
if err != nil {
47+
panic(err)
48+
}
49+
return string(encoded)
50+
}
51+
52+
func NewTerminalInfo(containerId string) *TerminalInfo {
53+
return &TerminalInfo{
54+
Version: TerminalInfoVersion,
55+
Type: TerminalInfoType,
56+
ContainerID: containerId,
57+
}
58+
}
59+
60+
func GetTerminalInfo(encoded string) (*TerminalInfo, error) {
61+
ti := new(TerminalInfo)
62+
if err := json.Unmarshal([]byte(encoded), ti); err != nil {
63+
return nil, err
64+
}
65+
66+
if ti.Type != TerminalInfoType {
67+
return nil, fmt.Errorf("terminal info: incorrect type in payload (%q): %q", TerminalInfoType, ti.Type)
68+
}
69+
if ti.Version != TerminalInfoVersion {
70+
return nil, fmt.Errorf("terminal info: incorrect version in payload (%q): %q", TerminalInfoVersion, ti.Version)
71+
}
72+
73+
return ti, nil
74+
}

libcontainer/console_linux.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ type linuxConsole struct {
3636
slavePath string
3737
}
3838

39-
func (c *linuxConsole) Fd() uintptr {
40-
return c.master.Fd()
39+
func (c *linuxConsole) File() *os.File {
40+
return c.master
4141
}
4242

4343
func (c *linuxConsole) Path() string {

libcontainer/init_linux.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ func setupConsole(pipe *os.File, config *initConfig, mount bool) error {
197197
}
198198

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

run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ command(s) that get executed on start, edit the args parameter of the spec. See
3131
Value: "",
3232
Usage: `path to the root of the bundle directory, defaults to the current directory`,
3333
},
34+
cli.StringFlag{
35+
Name: "console-socket",
36+
Value: "",
37+
Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
38+
},
3439
cli.BoolFlag{
3540
Name: "detach, d",
3641
Usage: "detach from the container's process",

tty.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/docker/docker/pkg/term"
1313
"github.com/opencontainers/runc/libcontainer"
14+
"github.com/opencontainers/runc/libcontainer/utils"
1415
)
1516

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

104+
func (t *tty) sendtty(socket *os.File, ti *libcontainer.TerminalInfo) error {
105+
if t.console == nil {
106+
return fmt.Errorf("tty.console not set")
107+
}
108+
109+
// Create a fake file to contain the terminal info.
110+
console := os.NewFile(t.console.File().Fd(), ti.String())
111+
if err := utils.SendFd(socket, console); err != nil {
112+
return err
113+
}
114+
return nil
115+
}
116+
103117
// ClosePostStart closes any fds that are provided to the container and dup2'd
104118
// so that we no longer have copy in our process.
105119
func (t *tty) ClosePostStart() error {
@@ -135,5 +149,5 @@ func (t *tty) resize() error {
135149
if err != nil {
136150
return err
137151
}
138-
return term.SetWinsize(t.console.Fd(), ws)
152+
return term.SetWinsize(t.console.File().Fd(), ws)
139153
}

utils_linux.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55
import (
66
"errors"
77
"fmt"
8+
"net"
89
"os"
910
"path/filepath"
1011
"strconv"
@@ -121,13 +122,13 @@ func setupIO(process *libcontainer.Process, rootuid, rootgid int, createTTY, det
121122
// requirement that we set up anything nice for our caller or the
122123
// container.
123124
if detach {
124-
// TODO: Actually set rootuid, rootgid.
125125
if err := dupStdio(process, rootuid, rootgid); err != nil {
126126
return nil, err
127127
}
128128
return &tty{}, nil
129129
}
130130

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

@@ -180,10 +181,15 @@ type runner struct {
180181
detach bool
181182
listenFDs []*os.File
182183
pidFile string
184+
consoleSocket string
183185
container libcontainer.Container
184186
create bool
185187
}
186188

189+
func (r *runner) terminalinfo() *libcontainer.TerminalInfo {
190+
return libcontainer.NewTerminalInfo(r.container.ID())
191+
}
192+
187193
func (r *runner) run(config *specs.Process) (int, error) {
188194
process, err := newProcess(*config)
189195
if err != nil {
@@ -194,16 +200,32 @@ func (r *runner) run(config *specs.Process) (int, error) {
194200
process.Env = append(process.Env, fmt.Sprintf("LISTEN_FDS=%d", len(r.listenFDs)), "LISTEN_PID=1")
195201
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
196202
}
203+
197204
rootuid, err := r.container.Config().HostUID()
198205
if err != nil {
199206
r.destroy()
200207
return -1, err
201208
}
209+
202210
rootgid, err := r.container.Config().HostGID()
203211
if err != nil {
204212
r.destroy()
205213
return -1, err
206214
}
215+
216+
detach := r.detach || r.create
217+
218+
// Check command-line for sanity.
219+
if detach && config.Terminal && r.consoleSocket == "" {
220+
r.destroy()
221+
return -1, fmt.Errorf("cannot allocate tty if runc will detach without setting console socket")
222+
}
223+
// XXX: Should we change this?
224+
if (!detach || !config.Terminal) && r.consoleSocket != "" {
225+
r.destroy()
226+
return -1, fmt.Errorf("cannot use console socket if runc will not detach or allocate tty")
227+
}
228+
207229
startFn := r.container.Start
208230
if !r.create {
209231
startFn = r.container.Run
@@ -212,7 +234,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
212234
// with detaching containers, and then we get a tty after the container has
213235
// started.
214236
handler := newSignalHandler(r.enableSubreaper)
215-
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, r.detach || r.create)
237+
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach)
216238
if err != nil {
217239
r.destroy()
218240
return -1, err
@@ -229,6 +251,39 @@ func (r *runner) run(config *specs.Process) (int, error) {
229251
}
230252
}
231253
defer tty.Close()
254+
255+
if config.Terminal && detach {
256+
conn, err := net.Dial("unix", r.consoleSocket)
257+
if err != nil {
258+
r.terminate(process)
259+
r.destroy()
260+
return -1, err
261+
}
262+
defer conn.Close()
263+
264+
unixconn, ok := conn.(*net.UnixConn)
265+
if !ok {
266+
r.terminate(process)
267+
r.destroy()
268+
return -1, fmt.Errorf("casting to UnixConn failed")
269+
}
270+
271+
socket, err := unixconn.File()
272+
if err != nil {
273+
r.terminate(process)
274+
r.destroy()
275+
return -1, err
276+
}
277+
defer socket.Close()
278+
279+
err = tty.sendtty(socket, r.terminalinfo())
280+
if err != nil {
281+
r.terminate(process)
282+
r.destroy()
283+
return -1, err
284+
}
285+
}
286+
232287
if err := tty.ClosePostStart(); err != nil {
233288
r.terminate(process)
234289
r.destroy()
@@ -241,7 +296,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
241296
return -1, err
242297
}
243298
}
244-
if r.detach || r.create {
299+
if detach {
245300
return 0, nil
246301
}
247302
status, err := handler.forward(process, tty)
@@ -295,6 +350,7 @@ func startContainer(context *cli.Context, spec *specs.Spec, create bool) (int, e
295350
shouldDestroy: true,
296351
container: container,
297352
listenFDs: listenFDs,
353+
consoleSocket: context.String("console-socket"),
298354
detach: context.Bool("detach"),
299355
pidFile: context.String("pid-file"),
300356
create: create,

0 commit comments

Comments
 (0)