Skip to content

Commit

Permalink
feat: support kill command
Browse files Browse the repository at this point in the history
In fact, we consider carefully about add a kill command for pouch, and finally we think it's a good idea, because much time when we want to stop a container immediately, we can use kill command instead of stop command(which have default timeout 10s).
On the other hand, docker already support kill command!
  • Loading branch information
cxz66666 authored and rudyfly committed Jul 20, 2022
1 parent 6f79625 commit 080b9e0
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 2 deletions.
32 changes: 32 additions & 0 deletions apis/server/container_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"strconv"
"strings"
"syscall"
"time"

"github.com/alibaba/pouch/apis/metrics"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/alibaba/pouch/pkg/utils"
"github.com/alibaba/pouch/pkg/utils/filters"
util_metrics "github.com/alibaba/pouch/pkg/utils/metrics"
"github.com/alibaba/pouch/pkg/utils/signal"

"github.com/go-openapi/strfmt"
"github.com/gorilla/mux"
Expand Down Expand Up @@ -270,6 +272,36 @@ func (s *Server) stopContainer(ctx context.Context, rw http.ResponseWriter, req
return nil
}

func (s *Server) killContainer(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {

var sig syscall.Signal

label := util_metrics.ActionKillLabel
defer func(start time.Time) {
metrics.ContainerActionsCounter.WithLabelValues(label).Inc()
metrics.ContainerActionsTimer.WithLabelValues(label).Observe(time.Since(start).Seconds())
}(time.Now())

name := mux.Vars(req)["name"]

// If we have a signal, look at it. Otherwise, do nothing
if sigStr := req.FormValue("signal"); sigStr != "" {
var err error
if sig, err = signal.ParseSignal(sigStr); err != nil {
return err
}
}

if err := s.ContainerMgr.Kill(ctx, name, int(sig)); err != nil {
return err
}

metrics.ContainerSuccessActionsCounter.WithLabelValues(label).Inc()

rw.WriteHeader(http.StatusNoContent)
return nil
}

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

Expand Down
1 change: 1 addition & 0 deletions apis/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func initRoute(s *Server) *mux.Router {
{Method: http.MethodPost, Path: "/containers/create", HandlerFunc: s.createContainer},
{Method: http.MethodPost, Path: "/containers/{name:.*}/start", HandlerFunc: s.startContainer},
{Method: http.MethodPost, Path: "/containers/{name:.*}/stop", HandlerFunc: s.stopContainer},
{Method: http.MethodPost, Path: "/containers/{name:.*}/kill", HandlerFunc: s.killContainer},
{Method: http.MethodPost, Path: "/containers/{name:.*}/attach", HandlerFunc: s.attachContainer},
{Method: http.MethodGet, Path: "/containers/json", HandlerFunc: s.getContainers},
{Method: http.MethodGet, Path: "/containers/{name:.*}/json", HandlerFunc: s.getContainer},
Expand Down
19 changes: 19 additions & 0 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,25 @@ 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"
responses:
204:
description: "no error"
404:
$ref: "#/responses/404ErrorResponse"
500:
$ref: "#/responses/500ErrorResponse"
tags: ["Container"]

/containers/{id}/pause:
post:
summary: "Pause 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 container objects in Pouchd. " +
"You can kill a container using the container’s ID, ID-prefix, or name. " +
"This is useful when you wish to kill a container which is running."

// KillCommand use to implement 'kill' command, it kills a container.
type KillCommand struct {
baseCommand
signal string
}

// Init initialize kill command.
func (kc *KillCommand) Init(c *Cli) {
kc.cli = c
kc.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 kc.runKill(args)
},
Example: killExample(),
}
kc.addFlags()
}

// addFlags adds flags for specific command.
func (kc *KillCommand) addFlags() {
flagSet := kc.cmd.Flags()
flagSet.StringVarP(&kc.signal, "signal", "s", "KILL", "Signal to send to the container (default \"KILL\")")
}

func (kc *KillCommand) runKill(args []string) error {
ctx := context.Background()
apiClient := kc.cli.Client()

var errs []string
for _, name := range args {
if err := apiClient.ContainerKill(ctx, name, kc.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 documentation.
func killExample() string {
return `$ pouch ps -a
Name ID Status Created Image Runtime
foo2 5a0ede Up 2 seconds 3 second ago registry.hub.docker.com/library/busybox:latest runc
foo1 e05637 Up 6 seconds 7 seconds ago registry.hub.docker.com/library/busybox:latest runc
$ pouch kill foo1
foo1
$ pouch ps
Name ID Status Created Image Runtime
foo2 5a0ede Up 11 seconds 12 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 @@ -20,6 +20,7 @@ func main() {
cli.AddCommand(base, &CreateCommand{})
cli.AddCommand(base, &StartCommand{})
cli.AddCommand(base, &StopCommand{})
cli.AddCommand(base, &KillCommand{})
cli.AddCommand(base, &PsCommand{})
cli.AddCommand(base, &RmCommand{})
cli.AddCommand(base, &RestartCommand{})
Expand Down
17 changes: 17 additions & 0 deletions client/container_kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package client

import (
"context"
"net/url"
)

// ContainerKill kill a container.
func (client *APIClient) ContainerKill(ctx context.Context, name string, signal string) error {
q := url.Values{}
q.Add("signal", signal)

resp, err := client.post(ctx, "/containers/"+name+"/kill", q, nil, nil)
ensureCloseReader(resp)

return err
}
46 changes: 46 additions & 0 deletions client/container_kill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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.ContainerGet(context.Background(), "nothing")
if err == nil || !strings.Contains(err.Error(), "Server error") {
t.Fatalf("expected a Server 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 != "KILL" {
return nil, fmt.Errorf("signal not set in URL query properly. Expected 'KILL', 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", "KILL")
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 @@ -24,6 +24,7 @@ type ContainerAPIClient interface {
ContainerCreate(ctx context.Context, config types.ContainerConfig, hostConfig *types.HostConfig, networkConfig *types.NetworkingConfig, containerName string) (*types.ContainerCreateResp, error)
ContainerStart(ctx context.Context, name string, options types.ContainerStartOptions) error
ContainerStop(ctx context.Context, name, timeout string) error
ContainerKill(ctx context.Context, name, signal string) error
ContainerRemove(ctx context.Context, name string, options *types.ContainerRemoveOptions) error
ContainerList(ctx context.Context, option types.ContainerListOptions) ([]*types.Container, error)
ContainerAttach(ctx context.Context, name string, stdin bool) (net.Conn, *bufio.Reader, error)
Expand Down
36 changes: 35 additions & 1 deletion ctrd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import (
"syscall"
"time"

"github.com/sirupsen/logrus"

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/daemon/containerio"
"github.com/alibaba/pouch/pkg/errtypes"
"github.com/alibaba/pouch/pkg/ioutils"
"github.com/alibaba/pouch/pkg/log"
"github.com/sirupsen/logrus"

"github.com/containerd/containerd"
containerdtypes "github.com/containerd/containerd/api/types"
Expand Down Expand Up @@ -394,6 +395,39 @@ func (c *Client) recoverContainer(ctx context.Context, id string, io *containeri
return nil
}

// KillContainer kills a container's all processes by signal.
func (c *Client) KillContainer(ctx context.Context, id string, signal int) error {
if err := c.killContainer(ctx, id, signal); err != nil {
return convertCtrdErr(err)
}
return nil
}

// killContainer is the real process of killing a container
func (c *Client) killContainer(ctx context.Context, id string, signal int) error {
wrapperCli, err := c.Get(ctx)
if err != nil {
return fmt.Errorf("failed to get a containerd grpc client: %v", err)
}

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

if !c.lock.TrylockWithRetry(ctx, id) {
return errtypes.ErrLockfailed
}
defer c.lock.Unlock(id)

pack, err := c.watch.get(id)
if err != nil {
return err
}

//don't need to skip hooks!!!

// TODO: need we add WithKillAll to kill all processes in the container?
return pack.task.Kill(ctx, syscall.Signal(signal), containerd.WithKillAll)
}

// DestroyContainer kill container and delete it.
func (c *Client) DestroyContainer(ctx context.Context, id string, timeout int64) (*Message, error) {
msg, err := c.destroyContainer(ctx, id, timeout)
Expand Down
2 changes: 2 additions & 0 deletions ctrd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type APIClient interface {
type ContainerAPIClient interface {
// CreateContainer creates a containerd container and start process.
CreateContainer(ctx context.Context, container *Container, checkpointDir string) error
// KillContainer kills a container's all processes by signal.
KillContainer(ctx context.Context, id string, signal int) error
// DestroyContainer kill container and delete it.
DestroyContainer(ctx context.Context, id string, timeout int64) (*Message, error)
// ProbeContainer probe the container's status, if timeout <= 0, will block to receive message.
Expand Down
55 changes: 54 additions & 1 deletion daemon/mgr/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"time"

"github.com/sirupsen/logrus"

"github.com/alibaba/pouch/apis/opts"
"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/ctrd"
Expand All @@ -30,7 +32,6 @@ import (
"github.com/alibaba/pouch/pkg/streams"
"github.com/alibaba/pouch/pkg/utils"
volumetypes "github.com/alibaba/pouch/storage/volume/types"
"github.com/sirupsen/logrus"

"github.com/containerd/cgroups"
containerdtypes "github.com/containerd/containerd/api/types"
Expand Down Expand Up @@ -75,6 +76,9 @@ type ContainerMgr interface {
// Stop a container.
Stop(ctx context.Context, name string, timeout int64) error

// Kill a container
Kill(ctx context.Context, name string, signal int) error

// Restart restart a running container.
Restart(ctx context.Context, name string, timeout int64) error

Expand Down Expand Up @@ -968,6 +972,55 @@ func (mgr *ContainerManager) stop(ctx context.Context, c *Container, timeout int
return mgr.markStoppedAndRelease(ctx, c, msg)
}

// Kill kills a running container.
func (mgr *ContainerManager) Kill(ctx context.Context, name string, signal int) (err error) {
c, err := mgr.container(name)
if err != nil {
return err
}

ctx = log.AddFields(ctx, map[string]interface{}{"ContainerID": c.ID})

// NOTE: choose snapshotter, snapshotter can only be set
// through containerPlugin in Create function
ctx = ctrd.WithSnapshotter(ctx, c.Config.Snapshotter)

err = mgr.kill(ctx, c, signal)
if err != nil {
return err
}
attributes := map[string]string{
"signal": fmt.Sprintf("%d", signal),
}
mgr.LogContainerEventWithAttributes(ctx, c, "kill", attributes)
return nil
}

func (mgr *ContainerManager) kill(ctx context.Context, c *Container, signal int) error {
logrus.Debugf("Sending signal %d to %s", signal, c.ID)
c.Lock()
defer c.Unlock()

if c.State.Paused {
return fmt.Errorf("Container %s is paused. Unpause the container before stopping", c.ID)
}

if !c.State.Running {
return fmt.Errorf("Container %s is not running", c.ID)
}

if err := mgr.Client.KillContainer(ctx, c.ID, signal); err != nil {
// if container or process not exists, ignore the error
if strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "no such process") {
logrus.Warnf("container kill failed because of 'container not found' or 'no such process': %s", err.Error())
} else {
return fmt.Errorf("Cannot kill container %s: %s", c.ID, err)
}
}
return nil
}

// Restart restarts a running container.
func (mgr *ContainerManager) Restart(ctx context.Context, name string, timeout int64) error {
c, err := mgr.container(name)
Expand Down
1 change: 1 addition & 0 deletions docs/commandline/pouch.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pouch is a client side tool pouch to interact with daemon side process pouchd. F
* [pouch images](pouch_images.md) - List all images
* [pouch info](pouch_info.md) - Display system-wide information
* [pouch inspect](pouch_inspect.md) - Get the detailed information of container
* [pouch kill](pouch_kill.md) - kill one or more running containers
* [pouch load](pouch_load.md) - load a set of images from a tar archive or STDIN
* [pouch login](pouch_login.md) - Login to a registry
* [pouch logout](pouch_logout.md) - Logout from a registry
Expand Down
Loading

0 comments on commit 080b9e0

Please sign in to comment.