Skip to content

Commit

Permalink
feature: add wait client command for pouch
Browse files Browse the repository at this point in the history
Signed-off-by: xiechengsheng <[email protected]>
  • Loading branch information
xiechengsheng committed May 24, 2018
1 parent 30a8520 commit d53c13b
Show file tree
Hide file tree
Showing 15 changed files with 531 additions and 0 deletions.
12 changes: 12 additions & 0 deletions apis/server/container_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,15 @@ func (s *Server) removeContainers(ctx context.Context, rw http.ResponseWriter, r
rw.WriteHeader(http.StatusNoContent)
return nil
}

func (s *Server) waitContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
name := mux.Vars(req)["name"]

waitStatus, err := s.ContainerMgr.Wait(ctx, name)

if err != nil {
return err
}

return EncodeResponse(rw, http.StatusOK, &waitStatus)
}
1 change: 1 addition & 0 deletions apis/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func initRoute(s *Server) http.Handler {
s.addRoute(r, http.MethodGet, "/containers/{name:.*}/logs", withCancelHandler(s.logsContainer))
s.addRoute(r, http.MethodPost, "/containers/{name:.*}/resize", s.resizeContainer)
s.addRoute(r, http.MethodPost, "/containers/{name:.*}/restart", s.restartContainer)
s.addRoute(r, http.MethodPost, "/containers/{name:.*}/wait", withCancelHandler(s.waitContainer))

// image
s.addRoute(r, http.MethodPost, "/images/create", s.pullImage)
Expand Down
26 changes: 26 additions & 0 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,32 @@ paths:
$ref: "#/responses/500ErrorResponse"
tags: ["Container"]

/containers/{id}/wait:
post:
summary: "Block until a container stops, then returns the exit code."
operationId: "ContainerWait"
parameters:
- $ref: "#/parameters/id"
responses:
200:
description: "The container has exited."
schema:
type: "object"
required: [StatusCode]
properties:
StatusCode:
description: "Exit code of the container"
type: "integer"
x-nullable: false
Error:
description: "The error message of waiting container"
type: "string"
404:
$ref: "#/responses/404ErrorResponse"
500:
$ref: "#/responses/500ErrorResponse"
tags: ["Container"]

/containers/{id}:
delete:
summary: "Remove one container"
Expand Down
68 changes: 68 additions & 0 deletions apis/types/container_wait_okbody.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
cli.AddCommand(base, &TopCommand{})
cli.AddCommand(base, &LogsCommand{})
cli.AddCommand(base, &RemountLxcfsCommand{})
cli.AddCommand(base, &WaitCommand{})

// add generate doc command
cli.AddCommand(base, &GenDocCommand{})
Expand Down
72 changes: 72 additions & 0 deletions cli/wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"context"
"errors"
"fmt"
"strconv"
"strings"

"github.com/spf13/cobra"
)

// waitDescription is used to describe wait command in detail and auto generate command doc.
var waitDescription = "Block until one or more containers stop, then print their exit codes. " +
"If container state is already stopped, the command will return exit code immediately. " +
"On a successful stop, the exit code of the container is returned. "

// WaitCommand is used to implement 'wait' command.
type WaitCommand struct {
baseCommand
}

// Init initializes wait command.
func (wait *WaitCommand) Init(c *Cli) {
wait.cli = c
wait.cmd = &cobra.Command{
Use: "wait CONTAINER [CONTAINER...]",
Short: "Block until one or more containers stop, then print their exit codes",
Long: waitDescription,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return wait.runWait(args)
},
Example: waitExamples(),
}
}

// runWait is the entry of wait command.
func (wait *WaitCommand) runWait(args []string) error {
ctx := context.Background()
apiClient := wait.cli.Client()

var errs []string
for _, name := range args {
response, err := apiClient.ContainerWait(ctx, name)
if err != nil {
errs = append(errs, "container exitcode: "+strconv.FormatInt(response.StatusCode, 10)+
", err message: "+response.Error+", "+err.Error())
continue
}
fmt.Printf("%d\n", response.StatusCode)
}

if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}

return nil
}

// waitExamples shows examples in wait command, and is used in auto-generated cli docs.
func waitExamples() string {
return `$ pouch ps
Name ID Status Created Image Runtime
foo f6717e Up 2 seconds 3 seconds ago registry.hub.docker.com/library/busybox:latest runc
$ pouch stop foo
$ pouch ps -a
Name ID Status Created Image Runtime
foo f6717e Stopped (0) 1 minute 2 minutes ago registry.hub.docker.com/library/busybox:latest runc
$ pouch wait foo
0`
}
25 changes: 25 additions & 0 deletions client/container_wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package client

import (
"context"

"github.com/alibaba/pouch/apis/types"
)

// ContainerWait pauses execution until a container exits.
// It returns the API status code as response of its readiness.
func (client *APIClient) ContainerWait(ctx context.Context, name string) (types.ContainerWaitOKBody, error) {
resp, err := client.post(ctx, "/containers/"+name+"/wait", nil, nil, nil)

if err != nil {
return types.ContainerWaitOKBody{
Error: err.Error(),
StatusCode: -1,
}, err
}

var response types.ContainerWaitOKBody
err = decodeBody(&response, resp.Body)
ensureCloseReader(resp)
return response, err
}
65 changes: 65 additions & 0 deletions client/container_wait_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"

"github.com/alibaba/pouch/apis/types"
)

func TestContainerWaitError(t *testing.T) {
client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerWait(context.Background(), "nothing")
if err == nil || !strings.Contains(err.Error(), "Server error") {
t.Fatalf("expected a Server Error, got %v", err)
}
}

func TestContainerWaitNotFoundError(t *testing.T) {
client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusNotFound, "Not Found")),
}
_, err := client.ContainerWait(context.Background(), "no container")
if err == nil || !strings.Contains(err.Error(), "Not Found") {
t.Fatalf("expected a Not Found Error, got %v", err)
}
}

func TestContainerWait(t *testing.T) {
expectedURL := "/containers/container_id"

httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}
waitJSON := types.ContainerWaitOKBody{
Error: "",
StatusCode: 0,
}
b, err := json.Marshal(waitJSON)
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(b))),
}, nil
})

client := &APIClient{
HTTPCli: httpClient,
}

_, err := client.ContainerWait(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type ContainerAPIClient interface {
ContainerTop(ctx context.Context, name string, arguments []string) (types.ContainerProcessList, error)
ContainerLogs(ctx context.Context, name string, options types.ContainerLogsOptions) (io.ReadCloser, error)
ContainerResize(ctx context.Context, name, height, width string) error
ContainerWait(ctx context.Context, name string) (types.ContainerWaitOKBody, error)
}

// ImageAPIClient defines methods of Image client.
Expand Down
33 changes: 33 additions & 0 deletions ctrd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,3 +500,36 @@ func (c *Client) ResizeContainer(ctx context.Context, id string, opts types.Resi

return pack.task.Resize(ctx, uint32(opts.Height), uint32(opts.Width))
}

// WaitContainer waits until container's status is stopped.
func (c *Client) WaitContainer(ctx context.Context, id string) (types.ContainerWaitOKBody, error) {
wrapperCli, err := c.Get(ctx)
if err != nil {
return types.ContainerWaitOKBody{
Error: err.Error(),
StatusCode: -1,
}, fmt.Errorf("failed to get a containerd grpc client: %v", err)
}

ctx = leases.WithLease(ctx, wrapperCli.lease.ID())

waitExit := func() *Message {
return c.ProbeContainer(ctx, id, -1*time.Second)
}

var msg *Message
// wait for the task to exit.
msg = waitExit()

if err := msg.RawError(); err != nil && errtypes.IsTimeout(err) {
return types.ContainerWaitOKBody{
Error: err.Error(),
StatusCode: -1,
}, err
}

return types.ContainerWaitOKBody{
Error: "",
StatusCode: int64(msg.ExitCode()),
}, nil
}
2 changes: 2 additions & 0 deletions ctrd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type ContainerAPIClient interface {
// ResizeContainer changes the size of the TTY of the init process running
// in the container to the given height and width.
ResizeContainer(ctx context.Context, id string, opts types.ResizeOptions) error
// WaitContainer waits until container's status is stopped.
WaitContainer(ctx context.Context, id string) (types.ContainerWaitOKBody, error)
// UpdateResources updates the configurations of a container.
UpdateResources(ctx context.Context, id string, resources types.Resources) error
// SetExitHooks specified the handlers of container exit.
Expand Down
26 changes: 26 additions & 0 deletions daemon/mgr/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ type ContainerMgr interface {
// Remove removes a container, it may be running or stopped and so on.
Remove(ctx context.Context, name string, option *types.ContainerRemoveOptions) error

// Wait stops processing until the given container is stopped.
Wait(ctx context.Context, name string) (types.ContainerWaitOKBody, error)

// 2. The following five functions is related to container exec.

// CreateExec creates exec process's environment.
Expand Down Expand Up @@ -1299,6 +1302,29 @@ func (mgr *ContainerManager) Resize(ctx context.Context, name string, opts types
return mgr.Client.ResizeContainer(ctx, c.ID, opts)
}

// Wait stops processing until the given container is stopped.
func (mgr *ContainerManager) Wait(ctx context.Context, name string) (types.ContainerWaitOKBody, error) {
c, err := mgr.container(name)
if err != nil {
return types.ContainerWaitOKBody{
Error: err.Error(),
StatusCode: -1,
}, err
}

// We should notice that container's meta data shouldn't be locked in wait process, otherwise waiting for
// a running container to stop would make other client commands which manage this container are blocked.
// If a container status is exited or stopped, return exit code immediately.
if c.IsExited() || c.IsStopped() {
return types.ContainerWaitOKBody{
Error: "",
StatusCode: c.ExitCode(),
}, nil
}

return mgr.Client.WaitContainer(ctx, c.ID)
}

// Connect is used to connect a container to a network.
func (mgr *ContainerManager) Connect(ctx context.Context, name string, networkIDOrName string, epConfig *types.EndpointSettings) error {
c, err := mgr.container(name)
Expand Down
Loading

0 comments on commit d53c13b

Please sign in to comment.