diff --git a/cmd/client/container.go b/cmd/client/container.go index ecca0d5f..112407d0 100644 --- a/cmd/client/container.go +++ b/cmd/client/container.go @@ -21,6 +21,7 @@ var containerCommand = cli.Command{ removeContainerCommand, containerStatusCommand, listContainersCommand, + execSyncCommand, }, } @@ -203,6 +204,38 @@ var containerStatusCommand = cli.Command{ }, } +var execSyncCommand = cli.Command{ + Name: "execsync", + Usage: "exec a command synchronously in a container", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Value: "", + Usage: "id of the container", + }, + cli.Int64Flag{ + Name: "timeout", + Value: 0, + Usage: "timeout for the command", + }, + }, + Action: func(context *cli.Context) error { + // Set up a connection to the server. + conn, err := getClientConnection(context) + if err != nil { + return fmt.Errorf("failed to connect: %v", err) + } + defer conn.Close() + client := pb.NewRuntimeServiceClient(conn) + + err = ExecSync(client, context.String("id"), context.Args(), context.Int64("timeout")) + if err != nil { + return fmt.Errorf("execing command in container failed: %v", err) + } + return nil + }, +} + type listOptions struct { // id of the container id string @@ -394,6 +427,29 @@ func ContainerStatus(client pb.RuntimeServiceClient, ID string) error { return nil } +// ExecSync sends an ExecSyncRequest to the server, and parses +// the returned ExecSyncResponse. +func ExecSync(client pb.RuntimeServiceClient, ID string, cmd []string, timeout int64) error { + if ID == "" { + return fmt.Errorf("ID cannot be empty") + } + r, err := client.ExecSync(context.Background(), &pb.ExecSyncRequest{ + ContainerId: &ID, + Cmd: cmd, + Timeout: &timeout, + }) + if err != nil { + return err + } + fmt.Println("Stdout:") + fmt.Println(string(r.Stdout)) + fmt.Println("Stderr:") + fmt.Println(string(r.Stderr)) + fmt.Printf("Exit code: %v\n", *r.ExitCode) + + return nil +} + // ListContainers sends a ListContainerRequest to the server, and parses // the returned ListContainerResponse. func ListContainers(client pb.RuntimeServiceClient, opts listOptions) error { diff --git a/oci/oci.go b/oci/oci.go index e71cce48..8e0fb8ad 100644 --- a/oci/oci.go +++ b/oci/oci.go @@ -148,6 +148,100 @@ func (r *Runtime) StartContainer(c *Container) error { return nil } +// ExecSyncResponse is returned from ExecSync. +type ExecSyncResponse struct { + Stdout []byte + Stderr []byte + ExitCode int32 +} + +// ExecSync execs a command in a container and returns it's stdout, stderr and return code. +func (r *Runtime) ExecSync(c *Container, command []string, timeout int64) (resp *ExecSyncResponse, err error) { + args := []string{"exec", c.name} + args = append(args, command...) + cmd := exec.Command(r.Path(), args...) + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + err = cmd.Start() + if err != nil { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: -1, + }, err + } + + if timeout > 0 { + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + select { + case <-time.After(time.Duration(timeout) * time.Second): + err = unix.Kill(cmd.Process.Pid, syscall.SIGKILL) + if err != nil && err != syscall.ESRCH { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: -1, + }, fmt.Errorf("failed to kill process on timeout: %v", err) + } + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: -1, + }, fmt.Errorf("command timed out") + case err = <-done: + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: int32(status.ExitStatus()), + }, err + } + } else { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: -1, + }, err + } + } + + } + } else { + err = cmd.Wait() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: int32(status.ExitStatus()), + }, err + } + } else { + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: -1, + }, err + } + + } + } + + return &ExecSyncResponse{ + Stdout: stdoutBuf.Bytes(), + Stderr: stderrBuf.Bytes(), + ExitCode: 0, + }, nil +} + // StopContainer stops a container. func (r *Runtime) StopContainer(c *Container) error { if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "kill", c.name); err != nil { diff --git a/server/container.go b/server/container.go index eadf9284..c57bf1a9 100644 --- a/server/container.go +++ b/server/container.go @@ -577,7 +577,35 @@ func (s *Server) UpdateRuntimeConfig(ctx context.Context, req *pb.UpdateRuntimeC // ExecSync runs a command in a container synchronously. func (s *Server) ExecSync(ctx context.Context, req *pb.ExecSyncRequest) (*pb.ExecSyncResponse, error) { - return nil, nil + logrus.Debugf("ExecSyncRequest %+v", req) + c, err := s.getContainerFromRequest(req) + if err != nil { + return nil, err + } + + if err := s.runtime.UpdateStatus(c); err != nil { + return nil, err + } + + cState := s.runtime.ContainerStatus(c) + if !(cState.Status == oci.ContainerStateRunning || cState.Status == oci.ContainerStateCreated) { + return nil, fmt.Errorf("container is not created or running") + } + + cmd := req.GetCmd() + if cmd == nil { + return nil, fmt.Errorf("exec command cannot be empty") + } + + execResp, err := s.runtime.ExecSync(c, cmd, req.GetTimeout()) + resp := &pb.ExecSyncResponse{ + Stdout: execResp.Stdout, + Stderr: execResp.Stderr, + ExitCode: &execResp.ExitCode, + } + + logrus.Debugf("ExecSyncResponse: %+v", resp) + return resp, err } // Exec prepares a streaming endpoint to execute a command in the container. diff --git a/test/ctr.bats b/test/ctr.bats index 4b435934..4ace03f3 100644 --- a/test/ctr.bats +++ b/test/ctr.bats @@ -327,3 +327,36 @@ function teardown() { cleanup_pods stop_ocid } + +@test "ctr execsync" { + # this test requires docker, thus it can't yet be run in a container + if [ "$TRAVIS" = "true" ]; then # instead of $TRAVIS, add a function is_containerized to skip here + skip "cannot yet run this test in a container, use sudo make localintegration" + fi + + start_ocid + run ocic pod create --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run ocic ctr create --config "$TESTDATA"/container_redis.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ocic ctr start --id "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + run ocic ctr execsync --id "$ctr_id" echo HELLO + echo "$output" + [ "$status" -eq 0 ] + [[ "$output" =~ "HELLO" ]] + run ocic ctr execsync --id "$ctr_id" --timeout 1 sleep 10 + echo "$output" + [[ "$output" =~ "command timed out" ]] + run ocic pod remove --id "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_ocid +}