Skip to content

Commit

Permalink
feat: Add container kill API
Browse files Browse the repository at this point in the history
Signed-off-by: Shubharanshu Mahapatra <[email protected]>
  • Loading branch information
Shubhranshu153 committed Jan 21, 2025
1 parent 94aa369 commit 60af05c
Show file tree
Hide file tree
Showing 13 changed files with 442 additions and 6 deletions.
2 changes: 2 additions & 0 deletions api/handlers/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Service interface {
ExtractArchiveInContainer(ctx context.Context, putArchiveOpt *types.PutArchiveOptions, body io.ReadCloser) error
Stats(ctx context.Context, cid string) (<-chan *types.StatsJSON, error)
ExecCreate(ctx context.Context, cid string, config types.ExecConfig) (string, error)
Kill(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error
}

// RegisterHandlers register all the supported endpoints related to the container APIs.
Expand All @@ -58,6 +59,7 @@ func RegisterHandlers(r types.VersionedRouter, service Service, conf *config.Con
r.HandleFunc("/{id:.*}/archive", h.putArchive, http.MethodPut)
r.HandleFunc("/{id:.*}/stats", h.stats, http.MethodGet)
r.HandleFunc("/{id:.*}/exec", h.exec, http.MethodPost)
r.HandleFunc("/{id:.*}/kill", h.kill, http.MethodPost)
}

// newHandler creates the handler that serves all the container related APIs.
Expand Down
64 changes: 64 additions & 0 deletions api/handlers/container/kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package container

import (
"net/http"
"os"

"github.com/containerd/containerd/namespaces"
ncTypes "github.com/containerd/nerdctl/pkg/api/types"
"github.com/gorilla/mux"

"github.com/runfinch/finch-daemon/api/response"
"github.com/runfinch/finch-daemon/pkg/errdefs"
)

// kill creates a new kill instance.
func (h *handler) kill(w http.ResponseWriter, r *http.Request) {
cid, ok := mux.Vars(r)["id"]
if !ok || cid == "" {
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("must specify a container ID"))
return
}

ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)

signal := r.URL.Query().Get("signal")
if signal == "" {
signal = "SIGKILL"
}

devNull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0644)
if err != nil {
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("failed to open /dev/null"))
return
}
defer devNull.Close()

globalOpt := ncTypes.GlobalCommandOptions(*h.Config)
options := ncTypes.ContainerKillOptions{
GOptions: globalOpt,
KillSignal: signal,
Stdout: devNull,
Stderr: devNull,
}

err = h.service.Kill(ctx, cid, options)
if err != nil {
var code int
switch {
case errdefs.IsNotFound(err):
code = http.StatusNotFound
case errdefs.IsConflict(err):
code = http.StatusConflict
default:
code = http.StatusInternalServerError
}
response.JSON(w, code, response.NewError(err))
return
}

response.Status(w, http.StatusNoContent)
}
107 changes: 107 additions & 0 deletions api/handlers/container/kill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package container

import (
"context"
"fmt"
"net/http"
"net/http/httptest"

ncTypes "github.com/containerd/nerdctl/pkg/api/types"
"github.com/containerd/nerdctl/pkg/config"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/runfinch/finch-daemon/pkg/errdefs"

"github.com/runfinch/finch-daemon/mocks/mocks_container"
"github.com/runfinch/finch-daemon/mocks/mocks_logger"
)

var _ = Describe("Container Kill API", func() {
var (
mockCtrl *gomock.Controller
logger *mocks_logger.Logger
service *mocks_container.MockService
h *handler
rr *httptest.ResponseRecorder
_ ncTypes.GlobalCommandOptions
_ error
)

BeforeEach(func() {
mockCtrl = gomock.NewController(GinkgoT())
defer mockCtrl.Finish()
logger = mocks_logger.NewLogger(mockCtrl)
service = mocks_container.NewMockService(mockCtrl)
c := config.Config{}
h = newHandler(service, &c, logger)
rr = httptest.NewRecorder()
})

Context("kill handler", func() {
It("should return 204 No Content on successful kill", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/kill", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})
req.URL.RawQuery = "signal=SIGTERM"

service.EXPECT().Kill(gomock.Any(), "id1", gomock.Any()).DoAndReturn(func(ctx context.Context, cid string, opts ncTypes.ContainerKillOptions) error {
Expect(opts.KillSignal).Should(Equal("SIGTERM"))
return nil
})

h.kill(rr, req)
Expect(rr.Body.String()).Should(BeEmpty())
Expect(rr).Should(HaveHTTPStatus(http.StatusNoContent))
})

It("should return 400 when container ID is missing", func() {
req, err := http.NewRequest(http.MethodPost, "/containers//kill", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": ""})

h.kill(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "must specify a container ID"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusBadRequest))
})
It("should return 404 when service returns a not found error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/kill", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Kill(gomock.Any(), "id1", gomock.Any()).Return(errdefs.NewNotFound(fmt.Errorf("not found")))

h.kill(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "not found"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusNotFound))
})

It("should return 409 when service returns a conflict error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/kill", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Kill(gomock.Any(), "id1", gomock.Any()).Return(errdefs.NewConflict(fmt.Errorf("conflict")))

h.kill(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "conflict"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusConflict))
})

It("should return 500 when service returns an internal error", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/kill", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Kill(gomock.Any(), "id1", gomock.Any()).Return(fmt.Errorf("unexpected error"))

h.kill(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "unexpected error"}`))
Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError))
})
})
})
1 change: 1 addition & 0 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestRun(t *testing.T) {
tests.ContainerStats(opt)
tests.ContainerAttach(opt)
tests.ContainerLogs(opt)
tests.ContainerKill(opt)

// functional test for volume APIs
tests.VolumeList(opt)
Expand Down
86 changes: 86 additions & 0 deletions e2e/tests/container_kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package tests

import (
"encoding/json"
"fmt"
"net/http"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/runfinch/common-tests/command"
"github.com/runfinch/common-tests/option"

"github.com/runfinch/finch-daemon/api/response"
"github.com/runfinch/finch-daemon/e2e/client"
)

func ContainerKill(opt *option.Option) {
Describe("kill a container", func() {
var (
uClient *http.Client
version string
apiUrl string
)
BeforeEach(func() {
uClient = client.NewClient(GetDockerHostUrl())
version = GetDockerApiVersion()
relativeUrl := fmt.Sprintf("/containers/%s/kill", testContainerName)
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
})
AfterEach(func() {
command.RemoveAll(opt)
})
It("should kill the container with default SIGKILL", func() {
// start a container that keeps running
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")
res, err := uClient.Post(apiUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))
containerShouldNotBeRunning(opt, testContainerName)
})
It("should fail to kill a non-existent container", func() {
res, err := uClient.Post(apiUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNotFound))
var body response.Error
err = json.NewDecoder(res.Body).Decode(&body)
Expect(err).Should(BeNil())
})
It("should fail to kill a non running container", func() {
command.Run(opt, "create", "--name", testContainerName, defaultImage, "sleep", "infinity")
res, err := uClient.Post(apiUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusConflict))
var body response.Error
err = json.NewDecoder(res.Body).Decode(&body)
Expect(err).Should(BeNil())
containerShouldExist(opt, testContainerName)
})
It("should kill the container with SIGINT", func() {
relativeUrl := fmt.Sprintf("/containers/%s/kill?signal=SIGINT", testContainerName)
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
// sleep infinity doesnot respond to SIGINT
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "/bin/sh", "-c", "trap 'exit 0' SIGINT; while true; do sleep 1; done")
res, err := uClient.Post(apiUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))
// This is an async operation as a result we need to wait for the container to exit gracefully before checking the status
time.Sleep(1 * time.Second)
containerShouldNotBeRunning(opt, testContainerName)
})
It("should not kill the container and throw error on unrecognized signal", func() {
relativeUrl := fmt.Sprintf("/containers/%s/kill?signal=SIGRAND", testContainerName)
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")
res, err := uClient.Post(apiUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusInternalServerError))
containerShouldExist(opt, testContainerName)
containerShouldBeRunning(opt, testContainerName)
})
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ require (
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/getlantern/httptest v0.0.0-20161025015934-4b40f4c7e590
github.com/gofrs/flock v0.12.1
github.com/golang/mock v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
Expand Down Expand Up @@ -146,6 +145,7 @@ require (
go.opentelemetry.io/otel/trace v1.30.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down Expand Up @@ -373,6 +371,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
6 changes: 6 additions & 0 deletions internal/backend/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/containerd/containerd"
"github.com/containerd/nerdctl/pkg/api/types"
ncTypes "github.com/containerd/nerdctl/pkg/api/types"
"github.com/containerd/nerdctl/pkg/clientutil"
"github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/containerd/nerdctl/pkg/containerinspector"
Expand All @@ -32,6 +33,7 @@ type NerdctlContainerSvc interface {
NewNetworkingOptionsManager(types.NetworkOptions) (containerutil.NetworkOptionsManager, error)
ListContainers(ctx context.Context, options types.ContainerListOptions) ([]container.ListItem, error)
RenameContainer(ctx context.Context, container containerd.Container, newName string, options types.ContainerRenameOptions) error
KillContainer(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error

// Mocked functions for container attach
GetDataStore() (string, error)
Expand Down Expand Up @@ -96,6 +98,10 @@ func (*NerdctlWrapper) LoggingPrintLogsTo(stdout, stderr io.Writer, clv *logging
return clv.PrintLogsTo(stdout, stderr)
}

func (w *NerdctlWrapper) KillContainer(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error {
return container.Kill(ctx, w.clientWrapper.client, []string{cid}, options)
}

func (w *NerdctlWrapper) GetNerdctlExe() (string, error) {
if w.nerdctlExe != "" {
return w.nerdctlExe, nil
Expand Down
37 changes: 37 additions & 0 deletions internal/service/container/kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package container

import (
"context"
"fmt"

"github.com/containerd/containerd"
cerrdefs "github.com/containerd/errdefs"
ncTypes "github.com/containerd/nerdctl/pkg/api/types"

"github.com/runfinch/finch-daemon/pkg/errdefs"
)

func (s *service) Kill(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error {

cont, err := s.getContainer(ctx, cid)
if err != nil {
if cerrdefs.IsNotFound(err) {
return errdefs.NewNotFound(err)
}
return err
}
status := s.client.GetContainerStatus(ctx, cont)
if status != containerd.Running {
return errdefs.NewConflict(fmt.Errorf("Container is not running id: ", cid))
}

err = s.nctlContainerSvc.KillContainer(ctx, cid, options)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit 60af05c

Please sign in to comment.