Skip to content

Commit

Permalink
feature: add pouch kill functionality
Browse files Browse the repository at this point in the history
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
  • Loading branch information
xiechengsheng committed Jun 13, 2018
1 parent 3f0c62f commit b50b084
Show file tree
Hide file tree
Showing 14 changed files with 559 additions and 9 deletions.
24 changes: 24 additions & 0 deletions apis/server/container_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"strconv"
"strings"
"syscall"
"time"

"github.com/alibaba/pouch/apis/types"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions apis/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,26 @@ 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"
404:
$ref: "#/responses/404ErrorResponse"
500:
$ref: "#/responses/500ErrorResponse"
tags: ["Container"]

/containers/{id}/top:
post:
summary: "Display the running processes of a container"
Expand Down
75 changes: 75 additions & 0 deletions cli/kill.go
Original file line number Diff line number Diff line change
@@ -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`
}
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
20 changes: 20 additions & 0 deletions client/container_kill.go
Original file line number Diff line number Diff line change
@@ -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
}
58 changes: 58 additions & 0 deletions client/container_kill_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 74 additions & 0 deletions ctrd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(50 * 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
}
6 changes: 4 additions & 2 deletions ctrd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit b50b084

Please sign in to comment.