From 8abfc31e53324a907452dc2f703831da0f3c8b73 Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Sat, 17 Oct 2020 05:30:55 +0000 Subject: [PATCH] allow for channel to receive runc pid Signed-off-by: Cory Bennett --- runc.go | 35 +++++++++++---- runc_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/runc.go b/runc.go index 2a515d2..f5f03ae 100644 --- a/runc.go +++ b/runc.go @@ -56,7 +56,7 @@ const ( // List returns all containers created inside the provided runc root directory func (r *Runc) List(context context.Context) ([]*Container, error) { - data, err := cmdOutput(r.command(context, "list", "--format=json"), false) + data, err := cmdOutput(r.command(context, "list", "--format=json"), false, nil) defer putBuf(data) if err != nil { return nil, err @@ -70,7 +70,7 @@ func (r *Runc) List(context context.Context) ([]*Container, error) { // State returns the state for the container provided by id func (r *Runc) State(context context.Context, id string) (*Container, error) { - data, err := cmdOutput(r.command(context, "state", id), true) + data, err := cmdOutput(r.command(context, "state", id), true, nil) defer putBuf(data) if err != nil { return nil, fmt.Errorf("%s: %s", err, data.String()) @@ -95,6 +95,7 @@ type CreateOpts struct { NoPivot bool NoNewKeyring bool ExtraFiles []*os.File + Started chan<- int } func (o *CreateOpts) args() (out []string, err error) { @@ -140,7 +141,7 @@ func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOp cmd.ExtraFiles = opts.ExtraFiles if cmd.Stdout == nil && cmd.Stderr == nil { - data, err := cmdOutput(cmd, true) + data, err := cmdOutput(cmd, true, nil) defer putBuf(data) if err != nil { return fmt.Errorf("%s: %s", err, data.String()) @@ -175,6 +176,7 @@ type ExecOpts struct { PidFile string ConsoleSocket ConsoleSocket Detach bool + Started chan<- int } func (o *ExecOpts) args() (out []string, err error) { @@ -197,6 +199,9 @@ func (o *ExecOpts) args() (out []string, err error) { // Exec executes an additional process inside the container based on a full // OCI Process specification func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts *ExecOpts) error { + if opts.Started != nil { + defer close(opts.Started) + } f, err := ioutil.TempFile(os.Getenv("XDG_RUNTIME_DIR"), "runc-process") if err != nil { return err @@ -220,7 +225,7 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts opts.Set(cmd) } if cmd.Stdout == nil && cmd.Stderr == nil { - data, err := cmdOutput(cmd, true) + data, err := cmdOutput(cmd, true, opts.Started) defer putBuf(data) if err != nil { return fmt.Errorf("%w: %s", err, data.String()) @@ -231,6 +236,9 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts if err != nil { return err } + if opts.Started != nil { + opts.Started <- cmd.Process.Pid + } if opts != nil && opts.IO != nil { if c, ok := opts.IO.(StartCloser); ok { if err := c.CloseAfterStart(); err != nil { @@ -248,6 +256,9 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts // Run runs the create, start, delete lifecycle of the container // and returns its exit status after it has exited func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts) (int, error) { + if opts.Started != nil { + defer close(opts.Started) + } args := []string{"run", "--bundle", bundle} if opts != nil { oargs, err := opts.args() @@ -264,6 +275,9 @@ func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts) if err != nil { return -1, err } + if opts.Started != nil { + opts.Started <- cmd.Process.Pid + } status, err := Monitor.Wait(cmd, ec) if err == nil && status != 0 { err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) @@ -387,7 +401,7 @@ func (r *Runc) Resume(context context.Context, id string) error { // Ps lists all the processes inside the container returning their pids func (r *Runc) Ps(context context.Context, id string) ([]int, error) { - data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true) + data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true, nil) defer putBuf(data) if err != nil { return nil, fmt.Errorf("%s: %s", err, data.String()) @@ -401,7 +415,7 @@ func (r *Runc) Ps(context context.Context, id string) ([]int, error) { // Top lists all the processes inside the container returning the full ps data func (r *Runc) Top(context context.Context, id string, psOptions string) (*TopResults, error) { - data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true) + data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true, nil) defer putBuf(data) if err != nil { return nil, fmt.Errorf("%s: %s", err, data.String()) @@ -613,7 +627,7 @@ type Version struct { // Version returns the runc and runtime-spec versions func (r *Runc) Version(context context.Context) (Version, error) { - data, err := cmdOutput(r.command(context, "--version"), false) + data, err := cmdOutput(r.command(context, "--version"), false, nil) defer putBuf(data) if err != nil { return Version{}, err @@ -685,7 +699,7 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error { } return err } - data, err := cmdOutput(cmd, true) + data, err := cmdOutput(cmd, true, nil) defer putBuf(data) if err != nil { return fmt.Errorf("%s: %s", err, data.String()) @@ -695,7 +709,7 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error { // callers of cmdOutput are expected to call putBuf on the returned Buffer // to ensure it is released back to the shared pool after use. -func cmdOutput(cmd *exec.Cmd, combined bool) (*bytes.Buffer, error) { +func cmdOutput(cmd *exec.Cmd, combined bool, started chan<- int) (*bytes.Buffer, error) { b := getBuf() cmd.Stdout = b @@ -706,6 +720,9 @@ func cmdOutput(cmd *exec.Cmd, combined bool) (*bytes.Buffer, error) { if err != nil { return nil, err } + if started != nil { + started <- cmd.Process.Pid + } status, err := Monitor.Wait(cmd, ec) if err == nil && status != 0 { diff --git a/runc_test.go b/runc_test.go index f24e531..f7cf3c4 100644 --- a/runc_test.go +++ b/runc_test.go @@ -19,8 +19,12 @@ package runc import ( "context" "errors" + "io/ioutil" + "os" "sync" + "syscall" "testing" + "time" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -182,6 +186,78 @@ func TestRuncExecExit(t *testing.T) { } } +func TestRuncStarted(t *testing.T) { + ctx, timeout := context.WithTimeout(context.Background(), 10*time.Second) + defer timeout() + + dummyCommand, err := dummySleepRunc() + if err != nil { + t.Fatalf("Failed to create dummy sleep runc: %s", err) + } + defer os.Remove(dummyCommand) + sleepRunc := &Runc{ + Command: dummyCommand, + } + + var wg sync.WaitGroup + defer wg.Wait() + + started := make(chan int) + wg.Add(1) + go func() { + defer wg.Done() + interrupt(ctx, t, started) + }() + status, err := sleepRunc.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{ + Started: started, + }) + if err == nil { + t.Fatal("Expected error from Run, but got nil") + } + if status != -1 { + t.Fatalf("Expected exit status 0 from Run, got %d", status) + } + + started = make(chan int) + wg.Add(1) + go func() { + defer wg.Done() + interrupt(ctx, t, started) + }() + err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{ + Started: started, + }) + if err == nil { + t.Fatal("Expected error from Exec, but got nil") + } + status = extractStatus(err) + if status != -1 { + t.Fatalf("Expected exit status -1 from Exec, got %d", status) + } + + started = make(chan int) + wg.Add(1) + go func() { + defer wg.Done() + interrupt(ctx, t, started) + }() + io, err := NewSTDIO() + if err != nil { + t.Fatalf("Unexpected error from NewSTDIO: %s", err) + } + err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{ + IO: io, + Started: started, + }) + if err == nil { + t.Fatal("Expected error from Exec, but got nil") + } + status = extractStatus(err) + if status != -1 { + t.Fatalf("Expected exit status 1 from Exec, got %d", status) + } +} + func extractStatus(err error) int { if err == nil { return 0 @@ -192,3 +268,49 @@ func extractStatus(err error) int { } return -1 } + +// interrupt waits for the pid over the started channel then sends a +// SIGINT to the process. +func interrupt(ctx context.Context, t *testing.T, started <-chan int) { + select { + case <-ctx.Done(): + t.Fatal("Timed out waiting for started message") + case pid, ok := <-started: + if !ok { + t.Fatal("Started channel closed without sending pid") + } + process, _ := os.FindProcess(pid) + defer process.Release() + err := process.Signal(syscall.SIGINT) + if err != nil { + t.Fatalf("Failed to send SIGINT to %d: %s", pid, err) + } + } +} + +// dummySleepRunc creates s simple script that just runs `sleep 10` to replace +// runc for testing process that are longer running. +func dummySleepRunc() (_ string, err error) { + fh, err := ioutil.TempFile("", "*.sh") + if err != nil { + return "", err + } + defer func() { + if err != nil { + os.Remove(fh.Name()) + } + }() + _, err = fh.Write([]byte("#!/bin/sh\nexec /bin/sleep 10")) + if err != nil { + return "", err + } + err = fh.Close() + if err != nil { + return "", err + } + err = os.Chmod(fh.Name(), 0755) + if err != nil { + return "", err + } + return fh.Name(), nil +}