diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index ce6ef9e43..3ab130a76 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "syscall" "time" "github.com/alibaba/pouch/apis/types" @@ -18,6 +19,8 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/sirupsen/logrus" + + "github.com/docker/docker/pkg/signal" ) func (s *Server) createContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { @@ -397,3 +400,24 @@ func (s *Server) waitContainer(ctx context.Context, rw http.ResponseWriter, req return EncodeResponse(rw, http.StatusOK, &waitStatus) } + +func (s *Server) killContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { + name := mux.Vars(req)["name"] + + var sig syscall.Signal + + // parse client signal + if sigStr := req.FormValue("signal"); sigStr != "" { + var err error + if sig, err = signal.ParseSignal(sigStr); err != nil { + return httputils.NewHTTPError(err, http.StatusBadRequest) + } + } + + if err := s.ContainerMgr.Kill(ctx, name, int(sig)); err != nil { + return err + } + + rw.WriteHeader(http.StatusNoContent) + return nil +} diff --git a/apis/server/router.go b/apis/server/router.go index 8e1759ad5..b8aa98680 100644 --- a/apis/server/router.go +++ b/apis/server/router.go @@ -54,6 +54,7 @@ func initRoute(s *Server) http.Handler { 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)) + s.addRoute(r, http.MethodPost, "/containers/{name:.*}/kill", withCancelHandler(s.killContainer)) // image s.addRoute(r, http.MethodPost, "/images/create", s.pullImage) diff --git a/apis/swagger.yml b/apis/swagger.yml index 651ccb9ba..b60d27fb3 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -551,6 +551,30 @@ paths: $ref: "#/responses/500ErrorResponse" tags: ["Container"] + /containers/{id}/kill: + post: + summary: "Kill a container" + operationId: "ContainerKill" + parameters: + - $ref: "#/parameters/id" + - name: "signal" + in: "query" + description: "signal to send to the container" + type: "string" + default: "SIGKILL" + responses: + 204: + description: "no error" + 400: + description: "bad parameter" + schema: + $ref: "#/definitions/Error" + 404: + $ref: "#/responses/404ErrorResponse" + 500: + $ref: "#/responses/500ErrorResponse" + tags: ["Container"] + /containers/{id}/top: post: summary: "Display the running processes of a container" diff --git a/cli/kill.go b/cli/kill.go new file mode 100644 index 000000000..131cb9383 --- /dev/null +++ b/cli/kill.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +// killDescription is used to describe kill command in detail and auto generate command doc. +var killDescription = "Kill one or more running containers, the container will receive SIGKILL by default, " + + "or the signal which is specified with the --signal option." + +// KillCommand use to implement 'kill' command +type KillCommand struct { + baseCommand + signal string +} + +// Init initialize kill command. +func (kill *KillCommand) Init(c *Cli) { + kill.cli = c + kill.cmd = &cobra.Command{ + Use: "kill [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Kill one or more running containers", + Long: killDescription, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return kill.runKill(args) + }, + Example: killExample(), + } + kill.addFlags() +} + +// addFlags adds flags for specific command. +func (kill *KillCommand) addFlags() { + flagSet := kill.cmd.Flags() + flagSet.StringVarP(&kill.signal, "signal", "s", "SIGKILL", "Signal to send to the container") +} + +// runKill is the entry of kill command. +func (kill *KillCommand) runKill(args []string) error { + ctx := context.Background() + apiClient := kill.cli.Client() + + var errs []string + for _, name := range args { + if err := apiClient.ContainerKill(ctx, name, kill.signal); err != nil { + errs = append(errs, err.Error()) + continue + } + fmt.Printf("%s\n", name) + } + + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +// killExample shows examples in kill command, and is used in auto-generated cli docs. +func killExample() string { + return `$ pouch ps +Name ID Status Created Image Runtime +foo c926cf Up 5 seconds 6 seconds ago registry.hub.docker.com/library/busybox:latest runc +$ pouch kill foo +foo +$ pouch ps -a +Name ID Status Created Image Runtime +foo c926cf Exited (137) 9 seconds 25 seconds ago registry.hub.docker.com/library/busybox:latest runc` +} diff --git a/cli/main.go b/cli/main.go index bcb0b41f9..ca75c12d4 100644 --- a/cli/main.go +++ b/cli/main.go @@ -21,6 +21,7 @@ func main() { cli.AddCommand(base, &PsCommand{}) cli.AddCommand(base, &RmCommand{}) cli.AddCommand(base, &RestartCommand{}) + cli.AddCommand(base, &KillCommand{}) cli.AddCommand(base, &ExecCommand{}) cli.AddCommand(base, &VersionCommand{}) cli.AddCommand(base, &InfoCommand{}) diff --git a/client/container_kill.go b/client/container_kill.go new file mode 100644 index 000000000..1e8eb5b17 --- /dev/null +++ b/client/container_kill.go @@ -0,0 +1,20 @@ +package client + +import ( + "context" + "net/url" +) + +// ContainerKill sends signal to a container. +func (client *APIClient) ContainerKill(ctx context.Context, name, signal string) error { + q := url.Values{} + q.Set("signal", signal) + + resp, err := client.post(ctx, "/containers/"+name+"/kill", q, nil, nil) + if err != nil { + return err + } + ensureCloseReader(resp) + + return nil +} diff --git a/client/container_kill_test.go b/client/container_kill_test.go new file mode 100644 index 000000000..edc0b60d2 --- /dev/null +++ b/client/container_kill_test.go @@ -0,0 +1,58 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" +) + +func TestContainerKillError(t *testing.T) { + client := &APIClient{ + HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")), + } + err := client.ContainerKill(context.Background(), "nothing", "SIGKILL") + if err == nil || !strings.Contains(err.Error(), "Server error") { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestContainerKillNotFoundError(t *testing.T) { + client := &APIClient{ + HTTPCli: newMockClient(errorMockResponse(http.StatusNotFound, "Not Found")), + } + err := client.ContainerKill(context.Background(), "no container", "SIGKILL") + if err == nil || !strings.Contains(err.Error(), "Not Found") { + t.Fatalf("expected a Not Found Error, got %v", err) + } +} + +func TestContainerKill(t *testing.T) { + expectedURL := "/containers/container_id/kill" + + 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) + } + signal := req.URL.Query().Get("signal") + if signal != "SIGKILL" { + return nil, fmt.Errorf("signal not set in URL query properly. Expected 'SIGKILL', got %s", signal) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }) + + client := &APIClient{ + HTTPCli: httpClient, + } + + err := client.ContainerKill(context.Background(), "container_id", "SIGKILL") + if err != nil { + t.Fatal(err) + } +} diff --git a/client/interface.go b/client/interface.go index a56059dd6..e50c6030e 100644 --- a/client/interface.go +++ b/client/interface.go @@ -34,6 +34,7 @@ type ContainerAPIClient interface { ContainerRestart(ctx context.Context, name string, timeout string) error ContainerPause(ctx context.Context, name string) error ContainerUnpause(ctx context.Context, name string) error + ContainerKill(ctx context.Context, name, signal string) error ContainerUpdate(ctx context.Context, name string, config *types.UpdateConfig) error ContainerUpgrade(ctx context.Context, name string, config types.ContainerConfig, hostConfig *types.HostConfig) error ContainerTop(ctx context.Context, name string, arguments []string) (types.ContainerProcessList, error) diff --git a/ctrd/container.go b/ctrd/container.go index f98df48d0..b4eb6bfb5 100644 --- a/ctrd/container.go +++ b/ctrd/container.go @@ -532,3 +532,77 @@ func (c *Client) WaitContainer(ctx context.Context, id string) (types.ContainerW StatusCode: int64(msg.ExitCode()), }, nil } + +// KillContainer sends signal to a container. +func (c *Client) KillContainer(ctx context.Context, id string, sig int) (*Message, error) { + wrapperCli, err := c.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get a containerd grpc client: %v", err) + } + + ctx = leases.WithLease(ctx, wrapperCli.lease.ID()) + + if !c.lock.Trylock(id) { + return nil, errtypes.ErrLockfailed + } + defer c.lock.Unlock(id) + + pack, err := c.watch.get(id) + if err != nil { + return nil, err + } + + pack.skipStopHooks = true + defer func() { + pack.skipStopHooks = false + }() + + // send signal to container + if err := pack.task.Kill(ctx, syscall.Signal(sig), containerd.WithKillAll); err != nil { + return nil, fmt.Errorf("failed to send signal %d to container %s: %v", sig, id, err) + } + + // if container task has been stopped, we should wait container and task to be totally deleted + time.Sleep(10 * time.Millisecond) + + lc, err := wrapperCli.client.LoadContainer(ctx, id) + if err != nil { + return nil, err + } + + var ( + status containerd.Status + deleted bool + ) + + task, err := lc.Task(ctx, nil) + // the container task has been deleted + if err != nil { + if errdefs.IsNotFound(err) { + deleted = true + } else { + return nil, err + } + } else { + // the container task isn't deleted, we could get task's status + status, err = task.Status(ctx) + if err != nil { + return nil, err + } + } + + waitExit := func() *Message { + return c.ProbeContainer(ctx, id, time.Duration(10)*time.Second) + } + + // if the container task has been deleted or container status is stopped, we could clean up this container + if deleted || status.Status == containerd.Stopped { + msg := waitExit() + if err := msg.RawError(); err != nil && errtypes.IsTimeout(err) { + return nil, err + } + return msg, c.watch.remove(ctx, id) + } + // the container is still running after receiving signal + return nil, nil +} diff --git a/ctrd/interface.go b/ctrd/interface.go index fc8ba54f6..e3b29033c 100644 --- a/ctrd/interface.go +++ b/ctrd/interface.go @@ -39,10 +39,12 @@ type ContainerAPIClient interface { ContainerPID(ctx context.Context, id string) (int, error) // ExecContainer executes a process in container. ExecContainer(ctx context.Context, process *Process) error - // RecoverContainer reload the container from metadata and watch it, if program be restarted. + // RecoverContainer reloads the container from metadata and watch it, if program be restarted. RecoverContainer(ctx context.Context, id string, io *containerio.IO) error - // PauseContainer pause container. + // PauseContainer pauses container. PauseContainer(ctx context.Context, id string) error + // KillContainer sends signal to container. + KillContainer(ctx context.Context, id string, sig int) (*Message, error) // UnpauseContainer unpauses a container. UnpauseContainer(ctx context.Context, id string) error // ResizeContainer changes the size of the TTY of the init process running diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index 0124e5ed3..280ff6436 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "syscall" "time" "github.com/alibaba/pouch/apis/opts" @@ -39,6 +40,8 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" + + "github.com/docker/docker/pkg/signal" ) // ContainerMgr as an interface defines all operations against container. @@ -100,6 +103,9 @@ type ContainerMgr interface { // Wait stops processing until the given container is stopped. Wait(ctx context.Context, name string) (types.ContainerWaitOKBody, error) + // Kill sends signal to a container. + Kill(ctx context.Context, name string, sig int) error + // 2. The following five functions is related to container exec. // CreateExec creates exec process's environment. @@ -1326,6 +1332,33 @@ func (mgr *ContainerManager) Top(ctx context.Context, name string, psArgs string return procList, nil } +// Kill sends signal to a container. +func (mgr *ContainerManager) Kill(ctx context.Context, name string, sig int) error { + c, err := mgr.container(name) + if err != nil { + return err + } + + if sig != 0 && !signal.ValidSignalForPlatform(syscall.Signal(sig)) { + return fmt.Errorf("signal %d is not suppoerted", sig) + } + + if !c.IsRunning() { + return fmt.Errorf("container %s is not running, can not execute kill command", c.ID) + } + + msg, err := mgr.Client.KillContainer(ctx, c.ID, sig) + if err != nil { + return err + } + + // the container has been stopped by client signal, we should mark and clean up this container + if msg != nil { + return mgr.exitedAndRelease(c.ID, msg) + } + return nil +} + // Resize resizes the size of a container tty. func (mgr *ContainerManager) Resize(ctx context.Context, name string, opts types.ResizeOptions) error { c, err := mgr.container(name) diff --git a/test/api_container_kill_test.go b/test/api_container_kill_test.go new file mode 100644 index 000000000..aafa2da9b --- /dev/null +++ b/test/api_container_kill_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "github.com/alibaba/pouch/apis/types" + "github.com/alibaba/pouch/test/environment" + "github.com/alibaba/pouch/test/request" + + "github.com/go-check/check" + + "net/url" +) + +// APIContainerKillSuite is the test suite for container kill API. +type APIContainerKillSuite struct{} + +func init() { + check.Suite(&APIContainerKillSuite{}) +} + +// SetUpTest does common setup in the beginning of each test. +func (suite *APIContainerKillSuite) SetUpTest(c *check.C) { + SkipIfFalse(c, environment.IsLinux) + PullImage(c, busyboxImage) +} + +// TestKillOk tests a running container could be killed. +func (suite *APIContainerKillSuite) TestKillOk(c *check.C) { + cname := "TestKillOk" + CreateBusyboxContainerOk(c, cname) + + StartContainerOk(c, cname) + + q := url.Values{} + q.Add("signal", "SIGKILL") + query := request.WithQuery(q) + + resp, err := request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 204) + + // add state check here + resp, err = request.Get("/containers/" + cname + "/json") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + defer resp.Body.Close() + + got := types.ContainerJSON{} + err = request.DecodeBody(&got, resp.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(got.State.Status), check.Equals, "exited") + + DelContainerForceMultyTime(c, cname) +} + +// TestKillNonExistingContainer tests killing a non-existing container return 404. +func (suite *APIContainerKillSuite) TestKillNonExistingContainer(c *check.C) { + cname := "TestKillNonExistingContainer" + + q := url.Values{} + q.Add("signal", "SIGKILL") + query := request.WithQuery(q) + + resp, err := request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 404) +} + +// TestKillNotRunningContainer tests killing a non-running container will return error. +func (suite *APIContainerKillSuite) TestKillNotRunningContainer(c *check.C) { + cname := "TestKillNotRunningContainer" + CreateBusyboxContainerOk(c, cname) + + q := url.Values{} + q.Add("signal", "SIGKILL") + query := request.WithQuery(q) + + resp, err := request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 500) + + StartContainerOk(c, cname) + + resp, err = request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 204) + + DelContainerForceMultyTime(c, cname) +} + +// TestKillContainerWithInvalidSignal tests killing a container with invalid signal will return error. +func (suite *APIContainerKillSuite) TestKillContainerWithInvalidSignal(c *check.C) { + cname := "TestKillContainerWithInvalidSignal" + CreateBusyboxContainerOk(c, cname) + + q := url.Values{} + q.Add("signal", "0") + query := request.WithQuery(q) + + resp, err := request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 400) + + StartContainerOk(c, cname) + + q = url.Values{} + q.Add("signal", "SIGKILL") + query = request.WithQuery(q) + + resp, err = request.Post("/containers/"+cname+"/kill", query) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 204) + + DelContainerForceMultyTime(c, cname) +} diff --git a/test/api_container_pause_test.go b/test/api_container_pause_test.go index 96d8ae403..cbb2316d1 100644 --- a/test/api_container_pause_test.go +++ b/test/api_container_pause_test.go @@ -1,6 +1,7 @@ package main import ( + "github.com/alibaba/pouch/apis/types" "github.com/alibaba/pouch/test/environment" "github.com/alibaba/pouch/test/request" @@ -32,26 +33,48 @@ func (suite *APIContainerPauseSuite) TestPauseUnpauseOk(c *check.C) { c.Assert(err, check.IsNil) CheckRespStatus(c, resp, 204) - // TODO: Add state check + // add state check + resp, err = request.Get("/containers/" + cname + "/json") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + + got := types.ContainerJSON{} + err = request.DecodeBody(&got, resp.Body) + c.Assert(err, check.IsNil) + defer resp.Body.Close() + + c.Assert(string(got.State.Status), check.Equals, "paused") resp, err = request.Post("/containers/" + cname + "/unpause") c.Assert(err, check.IsNil) CheckRespStatus(c, resp, 204) + // add state check + resp, err = request.Get("/containers/" + cname + "/json") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + + got = types.ContainerJSON{} + err = request.DecodeBody(&got, resp.Body) + c.Assert(err, check.IsNil) + defer resp.Body.Close() + + c.Assert(string(got.State.Status), check.Equals, "running") + DelContainerForceMultyTime(c, cname) } -// TestNonExistingContainer tests pause a non-existing container return 404. -func (suite *APIContainerPauseSuite) TestNonExistingContainer(c *check.C) { - cname := "TestNonExistingContainer" +// TestPauseNonExistingContainer tests pause a non-existing container return 404. +func (suite *APIContainerPauseSuite) TestPauseNonExistingContainer(c *check.C) { + cname := "TestPauseNonExistingContainer" resp, err := request.Post("/containers/" + cname + "/pause") c.Assert(err, check.IsNil) CheckRespStatus(c, resp, 404) } -// TestNotRunningContainer tests pausing a non-running container will return error. -func (suite *APIContainerPauseSuite) TestNotRunningContainer(c *check.C) { - cname := "TestNotRunningContainer" +// TestPauseUnpauseNotRunningContainer tests pausing/unpausing a non-running container will return error. +func (suite *APIContainerPauseSuite) TestPauseUnpauseNotRunningContainer(c *check.C) { + cname := "TestPauseUnpauseNotRunningContainer" CreateBusyboxContainerOk(c, cname) resp, err := request.Post("/containers/" + cname + "/pause") diff --git a/test/cli_kill_test.go b/test/cli_kill_test.go new file mode 100644 index 000000000..32e422f30 --- /dev/null +++ b/test/cli_kill_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "strings" + "time" + + "github.com/alibaba/pouch/apis/types" + "github.com/alibaba/pouch/test/command" + "github.com/alibaba/pouch/test/environment" + + "github.com/go-check/check" + "github.com/gotestyourself/gotestyourself/icmd" +) + +// PouchKillSuite is the test suite for kill CLI. +type PouchKillSuite struct{} + +func init() { + check.Suite(&PouchKillSuite{}) +} + +// SetUpSuite does common setup in the beginning of each test suite. +func (suite *PouchKillSuite) 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 *PouchKillSuite) TeadDownTest(c *check.C) { +} + +// TestKillWorks tests "pouch kill" work. +func (suite *PouchKillSuite) TestKillWorks(c *check.C) { + name := "TestKillWorks" + res := command.PouchRun("run", "-d", "--name", name, busyboxImage, "top") + defer DelContainerForceMultyTime(c, name) + res.Assert(c, icmd.Success) + + res = command.PouchRun("kill", name) + res.Assert(c, icmd.Success) + time.Sleep(250 * time.Millisecond) + + res = command.PouchRun("inspect", name) + res.Assert(c, icmd.Success) + + result := []types.ContainerJSON{} + if err := json.Unmarshal([]byte(res.Stdout()), &result); err != nil { + c.Errorf("failed to decode inspect output: %v", err) + } + + c.Assert(string(result[0].State.Status), check.Equals, "exited") +} + +// TestKillContainerWithSignal is to verify the correctness of sending signal to a container. +func (suite *PouchKillSuite) TestKillContainerWithSignal(c *check.C) { + name := "TestKillContainerWithSignal" + res := command.PouchRun("run", "-d", "--name", name, busyboxImage, "top") + defer DelContainerForceMultyTime(c, name) + res.Assert(c, icmd.Success) + + res = command.PouchRun("kill", "-s", "SIGWINCH", name) + res.Assert(c, icmd.Success) + time.Sleep(250 * time.Millisecond) + + res = command.PouchRun("inspect", name) + res.Assert(c, icmd.Success) + + result := []types.ContainerJSON{} + if err := json.Unmarshal([]byte(res.Stdout()), &result); err != nil { + c.Errorf("failed to decode inspect output: %v", err) + } + + c.Assert(string(result[0].State.Status), check.Equals, "running") +} + +// TestKillContainerWithInvalidSignal is to verify the correctness of sending invalid signal to a container. +func (suite *PouchKillSuite) TestKillContainerWithInvalidSignal(c *check.C) { + name := "TestKillContainerWithInvalidSignal" + res := command.PouchRun("run", "-d", "--name", name, busyboxImage, "top") + defer DelContainerForceMultyTime(c, name) + res.Assert(c, icmd.Success) + + res = command.PouchRun("kill", "-s", "0", name) + + expectedError := "Invalid signal: 0" + if out := res.Combined(); !strings.Contains(out, expectedError) { + c.Fatalf("unexpected output %s expected %s", res.Stderr(), expectedError) + } + + res = command.PouchRun("inspect", name) + res.Assert(c, icmd.Success) + + result := []types.ContainerJSON{} + if err := json.Unmarshal([]byte(res.Stdout()), &result); err != nil { + c.Errorf("failed to decode inspect output: %v", err) + } + + c.Assert(string(result[0].State.Status), check.Equals, "running") +}