wrap errors from executor Run/Exec to allow access to exit code

Signed-off-by: Cory Bennett <cbennett@netflix.com>
v0.8
Cory Bennett 2020-07-30 23:44:42 +00:00
parent 3f3957d16c
commit 4b456f17f4
3 changed files with 118 additions and 49 deletions

View File

@ -172,6 +172,7 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root cache.Moun
return err
}
defer cleanup()
spec.Process.Terminal = meta.Tty
container, err := w.client.NewContainer(ctx, id,
containerd.WithSpec(spec),
@ -195,59 +196,24 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root cache.Moun
if err != nil {
return err
}
defer func() {
if _, err1 := task.Delete(context.TODO()); err == nil && err1 != nil {
err = errors.Wrapf(err1, "failed to delete task %s", id)
}
}()
if err := task.Start(ctx); err != nil {
return err
}
if started != nil {
err = w.runProcess(ctx, task, process.Resize, func() {
startedOnce.Do(func() {
close(started)
if started != nil {
close(started)
}
})
}
statusCh, err := task.Wait(context.Background())
if err != nil {
return err
}
var cancel func()
ctxDone := ctx.Done()
for {
select {
case <-ctxDone:
ctxDone = nil
var killCtx context.Context
killCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
task.Kill(killCtx, syscall.SIGKILL)
case status := <-statusCh:
if cancel != nil {
cancel()
}
if status.ExitCode() != 0 {
var err error
if status.ExitCode() == containerd.UnknownExitStatus && status.Error() != nil {
err = errors.Wrap(status.Error(), "failure waiting for process")
} else {
err = errors.Errorf("process returned non-zero exit code: %d", status.ExitCode())
}
select {
case <-ctx.Done():
err = errors.Wrap(ctx.Err(), err.Error())
default:
}
return err
}
return nil
}
}
})
return err
}
func (w *containerdExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) error {
func (w *containerdExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) (err error) {
meta := process.Meta
// first verify the container is running, if we get an error assume the container
@ -329,5 +295,62 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut
if err != nil {
return errors.WithStack(err)
}
return taskProcess.Start(ctx)
return w.runProcess(ctx, taskProcess, process.Resize, nil)
}
func (w *containerdExecutor) runProcess(ctx context.Context, p containerd.Process, resize <-chan executor.WinSize, started func()) (err error) {
err = p.Start(ctx)
if err != nil {
return err
}
if started != nil {
started()
}
statusCh, err := p.Wait(context.Background())
if err != nil {
return err
}
var cancel func()
ctxDone := ctx.Done()
for {
select {
case <-ctxDone:
ctxDone = nil
var killCtx context.Context
killCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
p.Kill(killCtx, syscall.SIGKILL)
case size := <-resize:
err := p.Resize(ctx, size.Cols, size.Rows)
if err != nil {
cancel()
return err
}
case status := <-statusCh:
if cancel != nil {
cancel()
}
if status.ExitCode() != 0 {
var err error
if status.ExitCode() == containerd.UnknownExitStatus && status.Error() != nil {
err = errors.Wrap(status.Error(), "failure waiting for process")
} else {
err = &executor.ExitError{
ExitCode: status.ExitCode(),
Err: status.Error(),
}
}
select {
case <-ctx.Done():
err = errors.Wrap(ctx.Err(), err.Error())
default:
}
return err
}
return nil
}
}
}

View File

@ -2,6 +2,7 @@ package executor
import (
"context"
"fmt"
"io"
"net"
@ -28,10 +29,18 @@ type Mount struct {
Readonly bool
}
type WinSize struct {
Rows uint32
Cols uint32
Xpixel uint32
Ypixel uint32
}
type ProcessInfo struct {
Meta Meta
Stdin io.ReadCloser
Stdout, Stderr io.WriteCloser
Resize <-chan WinSize
}
type Executor interface {
@ -48,3 +57,24 @@ type HostIP struct {
Host string
IP net.IP
}
// ExitError will be returned from Run and Exec when the container process exits with
// a non-zero exit code.
type ExitError struct {
ExitCode uint32
Err error
}
func (err *ExitError) Error() string {
if err.Err != nil {
return err.Err.Error()
}
return fmt.Sprintf("exit code: %d", err.ExitCode)
}
func (err *ExitError) Unwrap() error {
if err.Err == nil {
return fmt.Errorf("exit code: %d", err.ExitCode)
}
return err.Err
}

View File

@ -271,7 +271,10 @@ func (w *runcExecutor) Run(ctx context.Context, id string, root cache.Mountable,
}
}
spec.Process.Terminal = meta.Tty
if meta.Tty {
return errors.New("tty with runc not implemented")
}
spec.Process.OOMScoreAdj = w.oomScoreAdj
if w.rootless {
if err := rootlessspecconv.ToRootless(spec); err != nil {
@ -329,8 +332,9 @@ func (w *runcExecutor) Run(ctx context.Context, id string, root cache.Mountable,
close(ended)
if status != 0 || err != nil {
if err == nil {
err = errors.Errorf("exit code: %d", status)
err = &executor.ExitError{
ExitCode: uint32(status),
Err: err,
}
select {
case <-ctx.Done():
@ -343,7 +347,7 @@ func (w *runcExecutor) Run(ctx context.Context, id string, root cache.Mountable,
return nil
}
func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) error {
func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) (err error) {
// first verify the container is running, if we get an error assume the container
// is in the process of being created and check again every 100ms or until
// context is canceled.
@ -406,9 +410,21 @@ func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.Pro
spec.Process.Env = process.Meta.Env
}
return w.runc.Exec(ctx, id, *spec.Process, &runc.ExecOpts{
err = w.runc.Exec(ctx, id, *spec.Process, &runc.ExecOpts{
IO: &forwardIO{stdin: process.Stdin, stdout: process.Stdout, stderr: process.Stderr},
})
var exitError *exec.ExitError
if errors.As(err, &exitError) {
err = &executor.ExitError{
ExitCode: uint32(exitError.ExitCode()),
Err: err,
}
return err
} else if err != nil {
return err
}
return nil
}
type forwardIO struct {