mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Add support for TTY in multi-container
Fixes #2714 PiperOrigin-RevId: 342950412
This commit is contained in:
committed by
gVisor bot
parent
7492ed6bd6
commit
e2d9a68eef
+3
-1
@@ -570,6 +570,8 @@ func (c *Client) Version() uint32 {
|
||||
func (c *Client) Close() {
|
||||
// unet.Socket.Shutdown() has no effect if unet.Socket.Close() has already
|
||||
// been called (by c.watch()).
|
||||
c.socket.Shutdown()
|
||||
if err := c.socket.Shutdown(); err != nil {
|
||||
log.Warningf("Socket.Shutdown() failed (FD: %d): %v", c.socket.FD(), err)
|
||||
}
|
||||
c.closedWg.Wait()
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -417,33 +416,35 @@ func StartReaper() func() {
|
||||
|
||||
// WaitUntilRead reads from the given reader until the wanted string is found
|
||||
// or until timeout.
|
||||
func WaitUntilRead(r io.Reader, want string, split bufio.SplitFunc, timeout time.Duration) error {
|
||||
func WaitUntilRead(r io.Reader, want string, timeout time.Duration) error {
|
||||
sc := bufio.NewScanner(r)
|
||||
if split != nil {
|
||||
sc.Split(split)
|
||||
}
|
||||
// done must be accessed atomically. A value greater than 0 indicates
|
||||
// that the read loop can exit.
|
||||
var done uint32
|
||||
doneCh := make(chan struct{})
|
||||
doneCh := make(chan bool)
|
||||
defer close(doneCh)
|
||||
go func() {
|
||||
for sc.Scan() {
|
||||
t := sc.Text()
|
||||
if strings.Contains(t, want) {
|
||||
atomic.StoreUint32(&done, 1)
|
||||
close(doneCh)
|
||||
break
|
||||
doneCh <- true
|
||||
return
|
||||
}
|
||||
if atomic.LoadUint32(&done) > 0 {
|
||||
break
|
||||
select {
|
||||
case <-doneCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
doneCh <- false
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
atomic.StoreUint32(&done, 1)
|
||||
return fmt.Errorf("timeout waiting to read %q", want)
|
||||
case <-doneCh:
|
||||
case res := <-doneCh:
|
||||
if !res {
|
||||
return fmt.Errorf("reader closed while waiting to read %q", want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+58
-16
@@ -211,10 +211,31 @@ func (cm *containerManager) Processes(cid *string, out *[]*control.Process) erro
|
||||
return control.Processes(cm.l.k, *cid, out)
|
||||
}
|
||||
|
||||
// CreateArgs contains arguments to the Create method.
|
||||
type CreateArgs struct {
|
||||
// CID is the ID of the container to start.
|
||||
CID string
|
||||
|
||||
// FilePayload may contain a TTY file for the terminal, if enabled.
|
||||
urpc.FilePayload
|
||||
}
|
||||
|
||||
// Create creates a container within a sandbox.
|
||||
func (cm *containerManager) Create(cid *string, _ *struct{}) error {
|
||||
log.Debugf("containerManager.Create, cid: %s", *cid)
|
||||
return cm.l.createContainer(*cid)
|
||||
func (cm *containerManager) Create(args *CreateArgs, _ *struct{}) error {
|
||||
log.Debugf("containerManager.Create: %s", args.CID)
|
||||
|
||||
if len(args.Files) > 1 {
|
||||
return fmt.Errorf("start arguments must have at most 1 files for TTY")
|
||||
}
|
||||
var tty *fd.FD
|
||||
if len(args.Files) == 1 {
|
||||
var err error
|
||||
tty, err = fd.NewFromFile(args.Files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dup'ing TTY file: %w", err)
|
||||
}
|
||||
}
|
||||
return cm.l.createContainer(args.CID, tty)
|
||||
}
|
||||
|
||||
// StartArgs contains arguments to the Start method.
|
||||
@@ -229,9 +250,8 @@ type StartArgs struct {
|
||||
CID string
|
||||
|
||||
// FilePayload contains, in order:
|
||||
// * stdin, stdout, and stderr.
|
||||
// * the file descriptor over which the sandbox will
|
||||
// request files from its root filesystem.
|
||||
// * stdin, stdout, and stderr (optional: if terminal is disabled).
|
||||
// * file descriptors to connect to gofer to serve the root filesystem.
|
||||
urpc.FilePayload
|
||||
}
|
||||
|
||||
@@ -251,23 +271,45 @@ func (cm *containerManager) Start(args *StartArgs, _ *struct{}) error {
|
||||
if args.CID == "" {
|
||||
return errors.New("start argument missing container ID")
|
||||
}
|
||||
if len(args.FilePayload.Files) < 4 {
|
||||
return fmt.Errorf("start arguments must contain stdin, stderr, and stdout followed by at least one file for the container root gofer")
|
||||
if len(args.Files) < 1 {
|
||||
return fmt.Errorf("start arguments must contain at least one file for the container root gofer")
|
||||
}
|
||||
|
||||
// All validation passed, logs the spec for debugging.
|
||||
specutils.LogSpec(args.Spec)
|
||||
|
||||
fds, err := fd.NewFromFiles(args.FilePayload.Files)
|
||||
if err != nil {
|
||||
return err
|
||||
goferFiles := args.Files
|
||||
var stdios []*fd.FD
|
||||
if !args.Spec.Process.Terminal {
|
||||
// When not using a terminal, stdios come as the first 3 files in the
|
||||
// payload.
|
||||
if l := len(args.Files); l < 4 {
|
||||
return fmt.Errorf("start arguments (len: %d) must contain stdios and files for the container root gofer", l)
|
||||
}
|
||||
var err error
|
||||
stdios, err = fd.NewFromFiles(goferFiles[:3])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dup'ing stdio files: %w", err)
|
||||
}
|
||||
goferFiles = goferFiles[3:]
|
||||
}
|
||||
defer func() {
|
||||
for _, fd := range fds {
|
||||
for _, fd := range stdios {
|
||||
_ = fd.Close()
|
||||
}
|
||||
}()
|
||||
if err := cm.l.startContainer(args.Spec, args.Conf, args.CID, fds); err != nil {
|
||||
|
||||
goferFDs, err := fd.NewFromFiles(goferFiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dup'ing gofer files: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
for _, fd := range goferFDs {
|
||||
_ = fd.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cm.l.startContainer(args.Spec, args.Conf, args.CID, stdios, goferFDs); err != nil {
|
||||
log.Debugf("containerManager.Start failed, cid: %s, args: %+v, err: %v", args.CID, args, err)
|
||||
return err
|
||||
}
|
||||
@@ -330,18 +372,18 @@ func (cm *containerManager) Restore(o *RestoreOpts, _ *struct{}) error {
|
||||
log.Debugf("containerManager.Restore")
|
||||
|
||||
var specFile, deviceFile *os.File
|
||||
switch numFiles := len(o.FilePayload.Files); numFiles {
|
||||
switch numFiles := len(o.Files); numFiles {
|
||||
case 2:
|
||||
// The device file is donated to the platform.
|
||||
// Can't take ownership away from os.File. dup them to get a new FD.
|
||||
fd, err := syscall.Dup(int(o.FilePayload.Files[1].Fd()))
|
||||
fd, err := syscall.Dup(int(o.Files[1].Fd()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dup file: %v", err)
|
||||
}
|
||||
deviceFile = os.NewFile(uintptr(fd), "platform device")
|
||||
fallthrough
|
||||
case 1:
|
||||
specFile = o.FilePayload.Files[0]
|
||||
specFile = o.Files[0]
|
||||
case 0:
|
||||
return fmt.Errorf("at least one file must be passed to Restore")
|
||||
default:
|
||||
|
||||
+9
-9
@@ -22,15 +22,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
// Include filesystem types that OCI spec might mount.
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/dev"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/host"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/proc"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/sys"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/tmpfs"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/tty"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
@@ -48,9 +39,18 @@ import (
|
||||
tmpfsvfs2 "gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
"gvisor.dev/gvisor/runsc/config"
|
||||
"gvisor.dev/gvisor/runsc/specutils"
|
||||
|
||||
// Include filesystem types that OCI spec might mount.
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/dev"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/host"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/proc"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/sys"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/tmpfs"
|
||||
_ "gvisor.dev/gvisor/pkg/sentry/fs/tty"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+44
-36
@@ -157,6 +157,11 @@ type execProcess struct {
|
||||
|
||||
// pidnsPath is the pid namespace path in spec
|
||||
pidnsPath string
|
||||
|
||||
// hostTTY is present when creating a sub-container with terminal enabled.
|
||||
// TTY file is passed during container create and must be saved until
|
||||
// container start.
|
||||
hostTTY *fd.FD
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -588,7 +593,9 @@ func (l *Loader) run() error {
|
||||
|
||||
// Create the root container init task. It will begin running
|
||||
// when the kernel is started.
|
||||
if _, err := l.createContainerProcess(true, l.sandboxID, &l.root, ep); err != nil {
|
||||
var err error
|
||||
_, ep.tty, ep.ttyVFS2, err = l.createContainerProcess(true, l.sandboxID, &l.root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -627,7 +634,7 @@ func (l *Loader) run() error {
|
||||
}
|
||||
|
||||
// createContainer creates a new container inside the sandbox.
|
||||
func (l *Loader) createContainer(cid string) error {
|
||||
func (l *Loader) createContainer(cid string, tty *fd.FD) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
@@ -635,14 +642,14 @@ func (l *Loader) createContainer(cid string) error {
|
||||
if _, ok := l.processes[eid]; ok {
|
||||
return fmt.Errorf("container %q already exists", cid)
|
||||
}
|
||||
l.processes[eid] = &execProcess{}
|
||||
l.processes[eid] = &execProcess{hostTTY: tty}
|
||||
return nil
|
||||
}
|
||||
|
||||
// startContainer starts a child container. It returns the thread group ID of
|
||||
// the newly created process. Used FDs are either closed or released. It's safe
|
||||
// for the caller to close any remaining files upon return.
|
||||
func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid string, files []*fd.FD) error {
|
||||
func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid string, stdioFDs, goferFDs []*fd.FD) error {
|
||||
// Create capabilities.
|
||||
caps, err := specutils.Capabilities(conf.EnableRaw, spec.Process.Capabilities)
|
||||
if err != nil {
|
||||
@@ -695,36 +702,41 @@ func (l *Loader) startContainer(spec *specs.Spec, conf *config.Config, cid strin
|
||||
info := &containerInfo{
|
||||
conf: conf,
|
||||
spec: spec,
|
||||
stdioFDs: files[:3],
|
||||
goferFDs: files[3:],
|
||||
goferFDs: goferFDs,
|
||||
}
|
||||
info.procArgs, err = createProcessArgs(cid, spec, creds, l.k, pidns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new process: %v", err)
|
||||
}
|
||||
tg, err := l.createContainerProcess(false, cid, info, ep)
|
||||
|
||||
// Use stdios or TTY depending on the spec configuration.
|
||||
if spec.Process.Terminal {
|
||||
if len(stdioFDs) > 0 {
|
||||
return fmt.Errorf("using TTY, stdios not expected: %v", stdioFDs)
|
||||
}
|
||||
if ep.hostTTY == nil {
|
||||
return fmt.Errorf("terminal enabled but no TTY provided. Did you set --console-socket on create?")
|
||||
}
|
||||
info.stdioFDs = []*fd.FD{ep.hostTTY, ep.hostTTY, ep.hostTTY}
|
||||
ep.hostTTY = nil
|
||||
} else {
|
||||
info.stdioFDs = stdioFDs
|
||||
}
|
||||
|
||||
ep.tg, ep.tty, ep.ttyVFS2, err = l.createContainerProcess(false, cid, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Success!
|
||||
l.k.StartProcess(tg)
|
||||
ep.tg = tg
|
||||
l.k.StartProcess(ep.tg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loader) createContainerProcess(root bool, cid string, info *containerInfo, ep *execProcess) (*kernel.ThreadGroup, error) {
|
||||
console := false
|
||||
if root {
|
||||
// Only root container supports terminal for now.
|
||||
console = info.spec.Process.Terminal
|
||||
}
|
||||
|
||||
func (l *Loader) createContainerProcess(root bool, cid string, info *containerInfo) (*kernel.ThreadGroup, *host.TTYFileOperations, *hostvfs2.TTYFileDescription, error) {
|
||||
// Create the FD map, which will set stdin, stdout, and stderr.
|
||||
ctx := info.procArgs.NewContext(l.k)
|
||||
fdTable, ttyFile, ttyFileVFS2, err := createFDTable(ctx, console, info.stdioFDs)
|
||||
fdTable, ttyFile, ttyFileVFS2, err := createFDTable(ctx, info.spec.Process.Terminal, info.stdioFDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("importing fds: %v", err)
|
||||
return nil, nil, nil, fmt.Errorf("importing fds: %v", err)
|
||||
}
|
||||
// CreateProcess takes a reference on fdTable if successful. We won't need
|
||||
// ours either way.
|
||||
@@ -736,11 +748,11 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
|
||||
mntr := newContainerMounter(info.spec, info.goferFDs, l.k, l.mountHints)
|
||||
if root {
|
||||
if err := mntr.processHints(info.conf, info.procArgs.Credentials); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
if err := setupContainerFS(ctx, info.conf, mntr, &info.procArgs); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Add the HOME environment variable if it is not already set.
|
||||
@@ -754,29 +766,25 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
|
||||
info.procArgs.Credentials.RealKUID, info.procArgs.Envv)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
info.procArgs.Envv = envv
|
||||
|
||||
// Create and start the new process.
|
||||
tg, _, err := l.k.CreateProcess(info.procArgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating process: %v", err)
|
||||
return nil, nil, nil, fmt.Errorf("creating process: %v", err)
|
||||
}
|
||||
// CreateProcess takes a reference on FDTable if successful.
|
||||
info.procArgs.FDTable.DecRef(ctx)
|
||||
|
||||
// Set the foreground process group on the TTY to the global init process
|
||||
// group, since that is what we are about to start running.
|
||||
if root {
|
||||
switch {
|
||||
case ttyFileVFS2 != nil:
|
||||
ep.ttyVFS2 = ttyFileVFS2
|
||||
ttyFileVFS2.InitForegroundProcessGroup(tg.ProcessGroup())
|
||||
case ttyFile != nil:
|
||||
ep.tty = ttyFile
|
||||
ttyFile.InitForegroundProcessGroup(tg.ProcessGroup())
|
||||
}
|
||||
switch {
|
||||
case ttyFileVFS2 != nil:
|
||||
ttyFileVFS2.InitForegroundProcessGroup(tg.ProcessGroup())
|
||||
case ttyFile != nil:
|
||||
ttyFile.InitForegroundProcessGroup(tg.ProcessGroup())
|
||||
}
|
||||
|
||||
// Install seccomp filters with the new task if there are any.
|
||||
@@ -784,7 +792,7 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
|
||||
if info.spec.Linux != nil && info.spec.Linux.Seccomp != nil {
|
||||
program, err := seccomp.BuildProgram(info.spec.Linux.Seccomp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building seccomp program: %v", err)
|
||||
return nil, nil, nil, fmt.Errorf("building seccomp program: %v", err)
|
||||
}
|
||||
|
||||
if log.IsLogging(log.Debug) {
|
||||
@@ -795,7 +803,7 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
|
||||
task := tg.Leader()
|
||||
// NOTE: It seems Flags are ignored by runc so we ignore them too.
|
||||
if err := task.AppendSyscallFilter(program, true); err != nil {
|
||||
return nil, fmt.Errorf("appending seccomp filters: %v", err)
|
||||
return nil, nil, nil, fmt.Errorf("appending seccomp filters: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -804,7 +812,7 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn
|
||||
}
|
||||
}
|
||||
|
||||
return tg, nil
|
||||
return tg, ttyFile, ttyFileVFS2, nil
|
||||
}
|
||||
|
||||
// startGoferMonitor runs a goroutine to monitor gofer's health. It polls on
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ func (ex *Exec) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
|
||||
}
|
||||
|
||||
func (ex *Exec) exec(c *container.Container, e *control.ExecArgs, waitStatus *syscall.WaitStatus) subcommands.ExitStatus {
|
||||
// Start the new process and get it pid.
|
||||
// Start the new process and get its pid.
|
||||
pid, err := c.Execute(e)
|
||||
if err != nil {
|
||||
return Errorf("executing processes for container: %v", err)
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// NewWithSocket creates pty master/replica pair, sends the master FD over the given
|
||||
// socket, and returns the replica.
|
||||
// NewWithSocket creates pty master/replica pair, sends the master FD over the
|
||||
// given socket, and returns the replica.
|
||||
func NewWithSocket(socketPath string) (*os.File, error) {
|
||||
// Create a new pty master and replica.
|
||||
ptyMaster, ptyReplica, err := pty.Open()
|
||||
|
||||
@@ -24,6 +24,7 @@ go_library(
|
||||
"//runsc/boot",
|
||||
"//runsc/cgroup",
|
||||
"//runsc/config",
|
||||
"//runsc/console",
|
||||
"//runsc/sandbox",
|
||||
"//runsc/specutils",
|
||||
"@com_github_cenkalti_backoff//:go_default_library",
|
||||
|
||||
+191
-14
@@ -18,6 +18,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/kr/pty"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/sentry/control"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
"gvisor.dev/gvisor/pkg/test/testutil"
|
||||
"gvisor.dev/gvisor/pkg/unet"
|
||||
@@ -38,19 +38,22 @@ import (
|
||||
// path is under 108 charactors (the unix socket path length limit),
|
||||
// relativizing the path if necessary.
|
||||
func socketPath(bundleDir string) (string, error) {
|
||||
path := filepath.Join(bundleDir, "socket")
|
||||
num := rand.Intn(10000)
|
||||
path := filepath.Join(bundleDir, fmt.Sprintf("socket-%4d", num))
|
||||
const maxPathLen = 108
|
||||
if len(path) <= maxPathLen {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Path is too large, try to make it smaller.
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting cwd: %v", err)
|
||||
}
|
||||
relPath, err := filepath.Rel(cwd, path)
|
||||
path, err = filepath.Rel(cwd, path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting relative path for %q from cwd %q: %v", path, cwd, err)
|
||||
}
|
||||
if len(path) > len(relPath) {
|
||||
path = relPath
|
||||
}
|
||||
const maxPathLen = 108
|
||||
if len(path) > maxPathLen {
|
||||
return "", fmt.Errorf("could not get socket path under length limit %d: %s", maxPathLen, path)
|
||||
}
|
||||
@@ -159,6 +162,82 @@ func TestConsoleSocket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an pty FD is sent over the console socket if one is provided.
|
||||
func TestMultiContainerConsoleSocket(t *testing.T) {
|
||||
for name, conf := range configsWithVFS2(t, all...) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rootDir, cleanup, err := testutil.SetupRootDir()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating root dir: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
conf.RootDir = rootDir
|
||||
|
||||
// Setup the containers.
|
||||
sleep := []string{"sleep", "100"}
|
||||
tru := []string{"true"}
|
||||
testSpecs, ids := createSpecs(sleep, tru)
|
||||
testSpecs[1].Process.Terminal = true
|
||||
|
||||
bundleDir, cleanup, err := testutil.SetupBundleDir(testSpecs[0])
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up container: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
args := Args{
|
||||
ID: ids[0],
|
||||
Spec: testSpecs[0],
|
||||
BundleDir: bundleDir,
|
||||
}
|
||||
rootCont, err := New(conf, args)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating container: %v", err)
|
||||
}
|
||||
defer rootCont.Destroy()
|
||||
if err := rootCont.Start(conf); err != nil {
|
||||
t.Fatalf("error starting container: %v", err)
|
||||
}
|
||||
|
||||
bundleDir, cleanup, err = testutil.SetupBundleDir(testSpecs[0])
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up container: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
sock, err := socketPath(bundleDir)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting socket path: %v", err)
|
||||
}
|
||||
srv, cleanup := createConsoleSocket(t, sock)
|
||||
defer cleanup()
|
||||
|
||||
// Create the container and pass the socket name.
|
||||
args = Args{
|
||||
ID: ids[1],
|
||||
Spec: testSpecs[1],
|
||||
BundleDir: bundleDir,
|
||||
ConsoleSocket: sock,
|
||||
}
|
||||
cont, err := New(conf, args)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating container: %v", err)
|
||||
}
|
||||
defer cont.Destroy()
|
||||
if err := cont.Start(conf); err != nil {
|
||||
t.Fatalf("error starting container: %v", err)
|
||||
}
|
||||
|
||||
// Make sure we get a console PTY.
|
||||
ptyMaster, err := receiveConsolePTY(srv)
|
||||
if err != nil {
|
||||
t.Fatalf("error receiving console FD: %v", err)
|
||||
}
|
||||
ptyMaster.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that job control signals work on a console created with "exec -ti".
|
||||
func TestJobControlSignalExec(t *testing.T) {
|
||||
spec := testutil.NewSpecWithArgs("/bin/sleep", "10000")
|
||||
@@ -221,9 +300,9 @@ func TestJobControlSignalExec(t *testing.T) {
|
||||
// Make sure all the processes are running.
|
||||
expectedPL := []*control.Process{
|
||||
// Root container process.
|
||||
{PID: 1, Cmd: "sleep", Threads: []kernel.ThreadID{1}},
|
||||
newProcessBuilder().Cmd("sleep").Process(),
|
||||
// Bash from exec process.
|
||||
{PID: 2, Cmd: "bash", Threads: []kernel.ThreadID{2}},
|
||||
newProcessBuilder().PID(2).Cmd("bash").Process(),
|
||||
}
|
||||
if err := waitForProcessList(c, expectedPL); err != nil {
|
||||
t.Error(err)
|
||||
@@ -233,7 +312,7 @@ func TestJobControlSignalExec(t *testing.T) {
|
||||
ptyMaster.Write([]byte("sleep 100\n"))
|
||||
|
||||
// Wait for it to start. Sleep's PPID is bash's PID.
|
||||
expectedPL = append(expectedPL, &control.Process{PID: 3, PPID: 2, Cmd: "sleep", Threads: []kernel.ThreadID{3}})
|
||||
expectedPL = append(expectedPL, newProcessBuilder().PID(3).PPID(2).Cmd("sleep").Process())
|
||||
if err := waitForProcessList(c, expectedPL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -254,7 +333,7 @@ func TestJobControlSignalExec(t *testing.T) {
|
||||
// Sleep is dead, but it may take more time for bash to notice and
|
||||
// change the foreground process back to itself. We know it is done
|
||||
// when bash writes "Terminated" to the pty.
|
||||
if err := testutil.WaitUntilRead(ptyMaster, "Terminated", nil, 5*time.Second); err != nil {
|
||||
if err := testutil.WaitUntilRead(ptyMaster, "Terminated", 5*time.Second); err != nil {
|
||||
t.Fatalf("bash did not take over pty: %v", err)
|
||||
}
|
||||
|
||||
@@ -359,7 +438,7 @@ func TestJobControlSignalRootContainer(t *testing.T) {
|
||||
|
||||
// Wait for bash to start.
|
||||
expectedPL := []*control.Process{
|
||||
{PID: 1, Cmd: "bash", Threads: []kernel.ThreadID{1}},
|
||||
newProcessBuilder().PID(1).Cmd("bash").Process(),
|
||||
}
|
||||
if err := waitForProcessList(c, expectedPL); err != nil {
|
||||
t.Fatalf("error waiting for processes: %v", err)
|
||||
@@ -369,7 +448,7 @@ func TestJobControlSignalRootContainer(t *testing.T) {
|
||||
ptyMaster.Write([]byte("sleep 100\n"))
|
||||
|
||||
// Wait for sleep to start.
|
||||
expectedPL = append(expectedPL, &control.Process{PID: 2, PPID: 1, Cmd: "sleep", Threads: []kernel.ThreadID{2}})
|
||||
expectedPL = append(expectedPL, newProcessBuilder().PID(2).PPID(1).Cmd("sleep").Process())
|
||||
if err := waitForProcessList(c, expectedPL); err != nil {
|
||||
t.Fatalf("error waiting for processes: %v", err)
|
||||
}
|
||||
@@ -393,7 +472,7 @@ func TestJobControlSignalRootContainer(t *testing.T) {
|
||||
// Sleep is dead, but it may take more time for bash to notice and
|
||||
// change the foreground process back to itself. We know it is done
|
||||
// when bash writes "Terminated" to the pty.
|
||||
if err := testutil.WaitUntilRead(ptyBuf, "Terminated", nil, 5*time.Second); err != nil {
|
||||
if err := testutil.WaitUntilRead(ptyBuf, "Terminated", 5*time.Second); err != nil {
|
||||
t.Fatalf("bash did not take over pty: %v", err)
|
||||
}
|
||||
|
||||
@@ -414,6 +493,104 @@ func TestJobControlSignalRootContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that terminal works with root and sub-containers.
|
||||
func TestMultiContainerTerminal(t *testing.T) {
|
||||
for name, conf := range configsWithVFS2(t, all...) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rootDir, cleanup, err := testutil.SetupRootDir()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating root dir: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
conf.RootDir = rootDir
|
||||
|
||||
// Don't let bash execute from profile or rc files, otherwise our PID
|
||||
// counts get messed up.
|
||||
bash := []string{"/bin/bash", "--noprofile", "--norc"}
|
||||
testSpecs, ids := createSpecs(bash, bash)
|
||||
|
||||
type termContainer struct {
|
||||
container *Container
|
||||
master *os.File
|
||||
}
|
||||
var containers []termContainer
|
||||
for i, spec := range testSpecs {
|
||||
bundleDir, cleanup, err := testutil.SetupBundleDir(spec)
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up container: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
spec.Process.Terminal = true
|
||||
sock, err := socketPath(bundleDir)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting socket path: %v", err)
|
||||
}
|
||||
srv, cleanup := createConsoleSocket(t, sock)
|
||||
defer cleanup()
|
||||
|
||||
// Create the container and pass the socket name.
|
||||
args := Args{
|
||||
ID: ids[i],
|
||||
Spec: spec,
|
||||
BundleDir: bundleDir,
|
||||
ConsoleSocket: sock,
|
||||
}
|
||||
cont, err := New(conf, args)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating container: %v", err)
|
||||
}
|
||||
defer cont.Destroy()
|
||||
|
||||
if err := cont.Start(conf); err != nil {
|
||||
t.Fatalf("error starting container: %v", err)
|
||||
}
|
||||
|
||||
// Make sure we get a console PTY.
|
||||
ptyMaster, err := receiveConsolePTY(srv)
|
||||
if err != nil {
|
||||
t.Fatalf("error receiving console FD: %v", err)
|
||||
}
|
||||
defer ptyMaster.Close()
|
||||
|
||||
containers = append(containers, termContainer{
|
||||
container: cont,
|
||||
master: ptyMaster,
|
||||
})
|
||||
}
|
||||
|
||||
for _, tc := range containers {
|
||||
// Bash output as well as sandbox output will be written to the PTY
|
||||
// file. Writes after a certain point will block unless we drain the
|
||||
// PTY, so we must continually copy from it.
|
||||
//
|
||||
// We log the output to stderr for debugabilitly, and also to a buffer,
|
||||
// since we wait on particular output from bash below. We use a custom
|
||||
// blockingBuffer which is thread-safe and also blocks on Read calls,
|
||||
// which makes this a suitable Reader for WaitUntilRead.
|
||||
ptyBuf := newBlockingBuffer()
|
||||
tee := io.TeeReader(tc.master, ptyBuf)
|
||||
go io.Copy(os.Stderr, tee)
|
||||
|
||||
// Wait for bash to start.
|
||||
expectedPL := []*control.Process{
|
||||
newProcessBuilder().Cmd("bash").Process(),
|
||||
}
|
||||
if err := waitForProcessList(tc.container, expectedPL); err != nil {
|
||||
t.Fatalf("error waiting for processes: %v", err)
|
||||
}
|
||||
|
||||
// Execute echo command and check that it was executed correctly. Use
|
||||
// a variable to ensure it's not matching against command echo.
|
||||
tc.master.Write([]byte("echo foo-${PWD}-123\n"))
|
||||
if err := testutil.WaitUntilRead(ptyBuf, "foo-/-123", 5*time.Second); err != nil {
|
||||
t.Fatalf("echo didn't execute: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// blockingBuffer is a thread-safe buffer that blocks when reading if the
|
||||
// buffer is empty. It implements io.ReadWriter.
|
||||
type blockingBuffer struct {
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"gvisor.dev/gvisor/runsc/boot"
|
||||
"gvisor.dev/gvisor/runsc/cgroup"
|
||||
"gvisor.dev/gvisor/runsc/config"
|
||||
"gvisor.dev/gvisor/runsc/console"
|
||||
"gvisor.dev/gvisor/runsc/sandbox"
|
||||
"gvisor.dev/gvisor/runsc/specutils"
|
||||
)
|
||||
@@ -397,7 +398,22 @@ func New(conf *config.Config, args Args) (*Container, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.Sandbox = sb.Sandbox
|
||||
if err := c.Sandbox.CreateContainer(c.ID); err != nil {
|
||||
|
||||
// If the console control socket file is provided, then create a new
|
||||
// pty master/slave pair and send the TTY to the sandbox process.
|
||||
var tty *os.File
|
||||
if c.ConsoleSocket != "" {
|
||||
// Create a new TTY pair and send the master on the provided socket.
|
||||
var err error
|
||||
tty, err = console.NewWithSocket(c.ConsoleSocket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up console with socket %q: %w", c.ConsoleSocket, err)
|
||||
}
|
||||
// tty file is transferred to the sandbox, then it can be closed here.
|
||||
defer tty.Close()
|
||||
}
|
||||
|
||||
if err := c.Sandbox.CreateContainer(c.ID, tty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -451,11 +467,16 @@ func (c *Container) Start(conf *config.Config) error {
|
||||
// the start (and all their children processes).
|
||||
if err := runInCgroup(c.Sandbox.Cgroup, func() error {
|
||||
// Create the gofer process.
|
||||
ioFiles, mountsFile, err := c.createGoferProcess(c.Spec, conf, c.BundleDir, false)
|
||||
goferFiles, mountsFile, err := c.createGoferProcess(c.Spec, conf, c.BundleDir, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mountsFile.Close()
|
||||
defer func() {
|
||||
_ = mountsFile.Close()
|
||||
for _, f := range goferFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
cleanMounts, err := specutils.ReadMounts(mountsFile)
|
||||
if err != nil {
|
||||
@@ -463,7 +484,14 @@ func (c *Container) Start(conf *config.Config) error {
|
||||
}
|
||||
c.Spec.Mounts = cleanMounts
|
||||
|
||||
return c.Sandbox.StartContainer(c.Spec, conf, c.ID, ioFiles)
|
||||
// Setup stdios if the container is not using terminal. Otherwise TTY was
|
||||
// already setup in create.
|
||||
var stdios []*os.File
|
||||
if !c.Spec.Process.Terminal {
|
||||
stdios = []*os.File{os.Stdin, os.Stdout, os.Stderr}
|
||||
}
|
||||
|
||||
return c.Sandbox.StartContainer(c.Spec, conf, c.ID, stdios, goferFiles)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+19
-11
@@ -173,7 +173,7 @@ func New(conf *config.Config, args *Args) (*Sandbox, error) {
|
||||
}
|
||||
|
||||
// CreateContainer creates a non-root container inside the sandbox.
|
||||
func (s *Sandbox) CreateContainer(cid string) error {
|
||||
func (s *Sandbox) CreateContainer(cid string, tty *os.File) error {
|
||||
log.Debugf("Create non-root container %q in sandbox %q, PID: %d", cid, s.ID, s.Pid)
|
||||
sandboxConn, err := s.sandboxConnect()
|
||||
if err != nil {
|
||||
@@ -181,7 +181,16 @@ func (s *Sandbox) CreateContainer(cid string) error {
|
||||
}
|
||||
defer sandboxConn.Close()
|
||||
|
||||
if err := sandboxConn.Call(boot.ContainerCreate, &cid, nil); err != nil {
|
||||
var files []*os.File
|
||||
if tty != nil {
|
||||
files = []*os.File{tty}
|
||||
}
|
||||
|
||||
args := boot.CreateArgs{
|
||||
CID: cid,
|
||||
FilePayload: urpc.FilePayload{Files: files},
|
||||
}
|
||||
if err := sandboxConn.Call(boot.ContainerCreate, &args, nil); err != nil {
|
||||
return fmt.Errorf("creating non-root container %q: %v", cid, err)
|
||||
}
|
||||
return nil
|
||||
@@ -211,11 +220,7 @@ func (s *Sandbox) StartRoot(spec *specs.Spec, conf *config.Config) error {
|
||||
}
|
||||
|
||||
// StartContainer starts running a non-root container inside the sandbox.
|
||||
func (s *Sandbox) StartContainer(spec *specs.Spec, conf *config.Config, cid string, goferFiles []*os.File) error {
|
||||
for _, f := range goferFiles {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
func (s *Sandbox) StartContainer(spec *specs.Spec, conf *config.Config, cid string, stdios, goferFiles []*os.File) error {
|
||||
log.Debugf("Start non-root container %q in sandbox %q, PID: %d", cid, s.ID, s.Pid)
|
||||
sandboxConn, err := s.sandboxConnect()
|
||||
if err != nil {
|
||||
@@ -223,15 +228,18 @@ func (s *Sandbox) StartContainer(spec *specs.Spec, conf *config.Config, cid stri
|
||||
}
|
||||
defer sandboxConn.Close()
|
||||
|
||||
// The payload must container stdin/stdout/stderr followed by gofer
|
||||
// files.
|
||||
files := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, goferFiles...)
|
||||
// The payload must contain stdin/stdout/stderr (which may be empty if using
|
||||
// TTY) followed by gofer files.
|
||||
payload := urpc.FilePayload{}
|
||||
payload.Files = append(payload.Files, stdios...)
|
||||
payload.Files = append(payload.Files, goferFiles...)
|
||||
|
||||
// Start running the container.
|
||||
args := boot.StartArgs{
|
||||
Spec: spec,
|
||||
Conf: conf,
|
||||
CID: cid,
|
||||
FilePayload: urpc.FilePayload{Files: files},
|
||||
FilePayload: payload,
|
||||
}
|
||||
if err := sandboxConn.Call(boot.ContainerStart, &args, nil); err != nil {
|
||||
return fmt.Errorf("starting non-root container %v: %v", spec.Process.Args, err)
|
||||
|
||||
@@ -480,7 +480,7 @@ func setup(t *testing.T, version string) (*criutil.Crictl, func(), error) {
|
||||
}
|
||||
|
||||
// Wait for containerd to boot.
|
||||
if err := testutil.WaitUntilRead(startupR, "Start streaming server", nil, 10*time.Second); err != nil {
|
||||
if err := testutil.WaitUntilRead(startupR, "Start streaming server", 10*time.Second); err != nil {
|
||||
t.Fatalf("failed to start containerd: %v", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user