From 6fbcf9cc1f2177b84513c3d82068ea56292e4091 Mon Sep 17 00:00:00 2001 From: xiechengsheng Date: Mon, 28 May 2018 10:22:38 +0800 Subject: [PATCH] feature: add wait client command for pouch Signed-off-by: xiechengsheng --- apis/server/container_bridge.go | 12 +++ apis/server/router.go | 1 + apis/swagger.yml | 26 +++++++ apis/types/container_wait_okbody.go | 68 +++++++++++++++++ cli/main.go | 1 + cli/wait.go | 70 ++++++++++++++++++ client/container_wait.go | 22 ++++++ client/container_wait_test.go | 65 ++++++++++++++++ client/interface.go | 1 + ctrd/container.go | 27 +++++++ ctrd/interface.go | 2 + daemon/mgr/container.go | 23 ++++++ daemon/mgr/container_types.go | 5 ++ hack/make.sh | 2 +- test/api_container_wait_test.go | 78 ++++++++++++++++++++ test/cli_wait_test.go | 110 ++++++++++++++++++++++++++++ 16 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 apis/types/container_wait_okbody.go create mode 100644 cli/wait.go create mode 100644 client/container_wait.go create mode 100644 client/container_wait_test.go create mode 100644 test/api_container_wait_test.go create mode 100644 test/cli_wait_test.go diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index dbe8faa102..7333c79c32 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -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) +} diff --git a/apis/server/router.go b/apis/server/router.go index f786b0b6c2..2f9706b628 100644 --- a/apis/server/router.go +++ b/apis/server/router.go @@ -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) diff --git a/apis/swagger.yml b/apis/swagger.yml index bf06681f7c..ef035b9ea4 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -546,6 +546,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" diff --git a/apis/types/container_wait_okbody.go b/apis/types/container_wait_okbody.go new file mode 100644 index 0000000000..f96014e833 --- /dev/null +++ b/apis/types/container_wait_okbody.go @@ -0,0 +1,68 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package types + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ContainerWaitOKBody container wait o k body +// swagger:model containerWaitOKBody +type ContainerWaitOKBody struct { + + // The error message of waiting container + Error string `json:"Error,omitempty"` + + // Exit code of the container + // Required: true + StatusCode int64 `json:"StatusCode"` +} + +// Validate validates this container wait o k body +func (m *ContainerWaitOKBody) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateStatusCode(formats); err != nil { + // prop + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerWaitOKBody) validateStatusCode(formats strfmt.Registry) error { + + if err := validate.Required("StatusCode", "body", int64(m.StatusCode)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerWaitOKBody) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerWaitOKBody) UnmarshalBinary(b []byte) error { + var res ContainerWaitOKBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/cli/main.go b/cli/main.go index 54035df819..3b265e384f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -43,6 +43,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{}) diff --git a/cli/wait.go b/cli/wait.go new file mode 100644 index 0000000000..34d896f238 --- /dev/null +++ b/cli/wait.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "errors" + "fmt" + "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, 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` +} diff --git a/client/container_wait.go b/client/container_wait.go new file mode 100644 index 0000000000..b75b9322cb --- /dev/null +++ b/client/container_wait.go @@ -0,0 +1,22 @@ +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{}, err + } + + var response types.ContainerWaitOKBody + err = decodeBody(&response, resp.Body) + ensureCloseReader(resp) + return response, err +} diff --git a/client/container_wait_test.go b/client/container_wait_test.go new file mode 100644 index 0000000000..bd2bff1986 --- /dev/null +++ b/client/container_wait_test.go @@ -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) + } +} diff --git a/client/interface.go b/client/interface.go index 78e193c147..7d5f646a73 100644 --- a/client/interface.go +++ b/client/interface.go @@ -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. diff --git a/ctrd/container.go b/ctrd/container.go index d3c8fb3209..8633ff1519 100644 --- a/ctrd/container.go +++ b/ctrd/container.go @@ -500,3 +500,30 @@ 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{}, 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{}, err + } + + return types.ContainerWaitOKBody{ + Error: msg.RawError().Error(), + StatusCode: int64(msg.ExitCode()), + }, nil +} diff --git a/ctrd/interface.go b/ctrd/interface.go index c8c8f70dfa..8f2a4db4a2 100644 --- a/ctrd/interface.go +++ b/ctrd/interface.go @@ -47,6 +47,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. diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index dbba680313..b4ec1825fa 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -97,6 +97,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. @@ -1302,6 +1305,26 @@ 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{}, 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: c.State.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) diff --git a/daemon/mgr/container_types.go b/daemon/mgr/container_types.go index 6976961ae1..7c42d6bbf2 100644 --- a/daemon/mgr/container_types.go +++ b/daemon/mgr/container_types.go @@ -187,6 +187,11 @@ func (c *Container) Key() string { return c.ID } +// ExitCode returns container's ExitCode. +func (c *Container) ExitCode() int64 { + return c.State.ExitCode +} + // IsRunning returns container is running or not. func (c *Container) IsRunning() bool { return c.State.Status == types.StatusRunning diff --git a/hack/make.sh b/hack/make.sh index c1e3514297..cbd391e063 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -283,7 +283,7 @@ function target # If test is failed, print pouch daemon log. set +e - $DIR/test/integration-test -test.v -check.v + $DIR/test/integration-test -test.v -check.vv if (( $? != 0 )); then echo "pouch daemon log:" diff --git a/test/api_container_wait_test.go b/test/api_container_wait_test.go new file mode 100644 index 0000000000..c2de13e196 --- /dev/null +++ b/test/api_container_wait_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "net/http" + "time" + + "github.com/alibaba/pouch/test/environment" + "github.com/alibaba/pouch/test/request" + + "github.com/go-check/check" +) + +// APIContainerWaitSuite is the test suite for container wait API. +type APIContainerWaitSuite struct{} + +func init() { + check.Suite(&APIContainerWaitSuite{}) +} + +// SetUpTest does common setup in the beginning of each test. +func (suite *APIContainerWaitSuite) SetUpTest(c *check.C) { + SkipIfFalse(c, environment.IsLinux) + PullImage(c, busyboxImage) +} + +// TestWaitOk tests waiting a stopped container return 200. +func (suite *APIContainerWaitSuite) TestWaitOk(c *check.C) { + cname := "TestWaitOk" + + CreateBusyboxContainerOk(c, cname) + StartContainerOk(c, cname) + StopContainerOk(c, cname) + + resp, err := request.Post("/containers/" + cname + "/wait") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + + DelContainerForceOk(c, cname) +} + +// TestRunningContainer tests waiting a running container to stop, then returns 200. +func (suite *APIContainerWaitSuite) TestRunningContainer(c *check.C) { + cname := "TestRunningContainer" + + CreateBusyboxContainerOk(c, cname) + StartContainerOk(c, cname) + + var ( + err error + resp *http.Response + ) + + chWait := make(chan struct{}) + go func() { + resp, err = request.Post("/containers/" + cname + "/wait") + close(chWait) + }() + time.Sleep(100 * time.Millisecond) + StopContainerOk(c, cname) + <-chWait + + select { + case <-chWait: + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + case <-time.After(2 * time.Second): + c.Errorf("timeout waiting for `pouch wait` API to exit") + } + DelContainerForceOk(c, cname) +} + +// TestNonExistingContainer tests waiting a non-existing container return 404. +func (suite *APIContainerWaitSuite) TestNonExistingContainer(c *check.C) { + cname := "TestNonExistingContainer" + resp, err := request.Post("/containers/" + cname + "/wait") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 404) +} diff --git a/test/cli_wait_test.go b/test/cli_wait_test.go new file mode 100644 index 0000000000..622d144eed --- /dev/null +++ b/test/cli_wait_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "time" + + "github.com/alibaba/pouch/test/command" + "github.com/alibaba/pouch/test/environment" + + "github.com/go-check/check" + "github.com/gotestyourself/gotestyourself/icmd" +) + +// PouchWaitSuite is the test suite for wait CLI. +type PouchWaitSuite struct{} + +func init() { + check.Suite(&PouchWaitSuite{}) +} + +// SetUpSuite does common setup in the beginning of each test suite. +func (suite *PouchWaitSuite) SetUpSuite(c *check.C) { + SkipIfFalse(c, environment.IsLinux) + + environment.PruneAllContainers(apiClient) + + PullImage(c, busyboxImage) +} + +// TearDownTest does cleanup work in the end of each test. +func (suite *PouchWaitSuite) TearDownTest(c *check.C) { +} + +// TestWaitNonBlockedExitZero is to verify the correctness of waiting a non-blocking container with 0 exit code +func (suite *PouchWaitSuite) TestWaitNonBlockedExitZero(c *check.C) { + name := "TestWaitNonBlockedExitZero" + command.PouchRun("run", "-d", "--name", name, busyboxImage, "sh", "-c", "true").Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, name) + + res := command.PouchRun("wait", name) + res.Assert(c, icmd.Success) + output := res.Stdout() + c.Assert(output, check.Equals, fmt.Sprintf("%s\n", "0")) +} + +// TestWaitBlockedExitZero is to verify the correctness of waiting a blocking container with 0 exit code +func (suite *PouchWaitSuite) TestWaitBlockedExitZero(c *check.C) { + name := "TestWaitBlockedExitZero" + command.PouchRun("run", "-d", "--name", name, busyboxImage, "/bin/sh", "-c", "trap 'exit 0' TERM; "+ + "while true; do usleep 10; done").Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, name) + + var output string + chWait := make(chan struct{}) + go func() { + res := command.PouchRun("wait", name) + res.Assert(c, icmd.Success) + output = res.Stdout() + close(chWait) + }() + time.Sleep(100 * time.Millisecond) + command.PouchRun("stop", name).Assert(c, icmd.Success) + <-chWait + + select { + case <-chWait: + c.Assert(output, check.Equals, fmt.Sprintf("%s\n", "0")) + case <-time.After(2 * time.Second): + c.Errorf("timeout waiting for `pouch wait` to exit") + } +} + +// TestWaitNonBlockedExitRandom is to verify the correctness of waiting a non-blocking container with random exit code +func (suite *PouchWaitSuite) TestWaitNonBlockedExitRandom(c *check.C) { + name := "TestWaitNonBlockedExitRandom" + command.PouchRun("run", "-d", "--name", name, busyboxImage, "sh", "-c", "exit 99").Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, name) + + res := command.PouchRun("wait", name) + res.Assert(c, icmd.Success) + output := res.Stdout() + c.Assert(output, check.Equals, fmt.Sprintf("%s\n", "99")) +} + +// TestWaitBlockedExitRandom is to verify the correctness of waiting a blocking container with random exit code +func (suite *PouchWaitSuite) TestWaitBlockedExitRandom(c *check.C) { + name := "TestWaitBlockedExitRandom" + command.PouchRun("run", "-d", "--name", name, busyboxImage, "/bin/sh", "-c", "trap 'exit 99' TERM; "+ + "while true; do usleep 10; done").Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, name) + + var output string + chWait := make(chan struct{}) + go func() { + res := command.PouchRun("wait", name) + res.Assert(c, icmd.Success) + output = res.Stdout() + close(chWait) + }() + time.Sleep(100 * time.Millisecond) + command.PouchRun("stop", name).Assert(c, icmd.Success) + <-chWait + + select { + case <-chWait: + c.Assert(output, check.Equals, fmt.Sprintf("%s\n", "99")) + case <-time.After(2 * time.Second): + c.Errorf("timeout waiting for `pouch wait` to exit") + } +}