Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/go_modules/github.com/spf13/afero…
Browse files Browse the repository at this point in the history
…-1.12.0
  • Loading branch information
swagatbora90 authored Jan 24, 2025
2 parents 79c0efc + 8a40617 commit a62f539
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 3 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, 0600)
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)
})
})
}
5 changes: 5 additions & 0 deletions internal/backend/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 types.ContainerKillOptions) error

// Mocked functions for container attach
GetDataStore() (string, error)
Expand Down Expand Up @@ -96,6 +97,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 types.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
36 changes: 36 additions & 0 deletions internal/service/container/kill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 %s is not running", cid))
}

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

return nil
}
Loading

0 comments on commit a62f539

Please sign in to comment.