Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/handlers/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Service interface {
ExecCreate(ctx context.Context, cid string, config types.ExecConfig) (string, error)
Kill(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error
Pause(ctx context.Context, cid string, options ncTypes.ContainerPauseOptions) error
Unpause(ctx context.Context, cid string, options ncTypes.ContainerUnpauseOptions) error
}

// RegisterHandlers register all the supported endpoints related to the container APIs.
Expand All @@ -62,6 +63,7 @@ func RegisterHandlers(r types.VersionedRouter, service Service, conf *config.Con
r.HandleFunc("/{id:.*}/exec", h.exec, http.MethodPost)
r.HandleFunc("/{id:.*}/kill", h.kill, http.MethodPost)
r.HandleFunc("/{id:.*}/pause", h.pause, http.MethodPost)
r.HandleFunc("/{id:.*}/unpause", h.unpause, http.MethodPost)
}

// newHandler creates the handler that serves all the container related APIs.
Expand Down
57 changes: 57 additions & 0 deletions api/handlers/container/unpause.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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/v2/pkg/namespaces"
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/gorilla/mux"

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

// unpause resumes a paused container.
func (h *handler) unpause(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)

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.ContainerUnpauseOptions{
GOptions: globalOpt,
Stdout: devNull,
}

err = h.service.Unpause(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)
}
110 changes: 110 additions & 0 deletions api/handlers/container/unpause_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/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/mocks/mocks_container"
"github.com/runfinch/finch-daemon/mocks/mocks_logger"
"github.com/runfinch/finch-daemon/pkg/errdefs"
)

var _ = Describe("Container Unpause 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("unpause handler", func() {
It("should return 204 No Content on successful unpause", func() {
req, err := http.NewRequest(http.MethodPost, "/containers/id1/unpause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).DoAndReturn(
func(ctx context.Context, cid string, opts ncTypes.ContainerUnpauseOptions) error {
return nil
})

h.unpause(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//pause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": ""})

h.unpause(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/unpause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

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

h.unpause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "container 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/unpause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).Return(
errdefs.NewConflict(fmt.Errorf("container not paused")))

h.unpause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "container not paused"}`))
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/unpause", nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "id1"})

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

h.unpause(rr, req)
Expect(rr.Body).Should(MatchJSON(`{"message": "unexpected internal 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 @@ -53,6 +53,7 @@ func TestRun(t *testing.T) {
tests.ContainerInspect(opt)
tests.ContainerWait(opt)
tests.ContainerPause(opt)
tests.ContainerUnpause(opt)

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

package tests

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

. "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 ContainerUnpause(opt *option.Option) {
Describe("unpause a container", func() {
var (
uClient *http.Client
version string
unpauseUrl string
pauseUrl string
)

BeforeEach(func() {
uClient = client.NewClient(GetDockerHostUrl())
version = GetDockerApiVersion()
relativeUrl_pause := fmt.Sprintf("/containers/%s/pause", testContainerName)
relativeUrl_unpause := fmt.Sprintf("/containers/%s/unpause", testContainerName)
unpauseUrl = client.ConvertToFinchUrl(version, relativeUrl_unpause)
pauseUrl = client.ConvertToFinchUrl(version, relativeUrl_pause)
})

AfterEach(func() {
command.RemoveAll(opt)
})

It("should unpause a paused container", func() {
// Start a container that keeps running
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")

// Pause this running container
res, err := uClient.Post(pauseUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))

// Verify container is paused
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
Expect(output).Should(Equal("paused"))

// Unpause the paused container
res, err = uClient.Post(unpauseUrl, "application/json", nil)
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))

// Verify container is running again
output = command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
Expect(output).Should(Equal("running"))
})

It("should fail to unpause a non-existent container", func() {
res, err := uClient.Post(unpauseUrl, "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 unpause a running container", func() {
// Start a container that keeps running
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")

// Try to unpause the running container
res, err := uClient.Post(unpauseUrl, "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())
Expect(body.Message).Should(Equal(fmt.Sprintf("container %s is already running", testContainerName)))

// Verify container is still running
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
Expect(output).Should(Equal("running"))
})

It("should fail to unpause a stopped container", func() {
// Create and start a container
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")

// Verify container is running
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
Expect(output).Should(Equal("running"))

// Stop the container with a timeout to ensure it stops
command.Run(opt, "stop", "-t", "1", testContainerName)

// Try to unpause the stopped container
res, err := uClient.Post(unpauseUrl, "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())
Expect(body.Message).Should(Equal(fmt.Sprintf("container %s is not paused", testContainerName)))
})
})
}
5 changes: 5 additions & 0 deletions internal/backend/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type NerdctlContainerSvc interface {
KillContainer(ctx context.Context, cid string, options types.ContainerKillOptions) error
ContainerWait(ctx context.Context, cid string, options types.ContainerWaitOptions) error
PauseContainer(ctx context.Context, cid string, options types.ContainerPauseOptions) error
UnpauseContainer(ctx context.Context, cid string, options types.ContainerUnpauseOptions) error

// Mocked functions for container attach
GetDataStore() (string, error)
Expand Down Expand Up @@ -135,6 +136,10 @@ func (w *NerdctlWrapper) PauseContainer(ctx context.Context, cid string, options
return container.Pause(ctx, w.clientWrapper.client, []string{cid}, options)
}

func (w *NerdctlWrapper) UnpauseContainer(ctx context.Context, cid string, options types.ContainerUnpauseOptions) error {
return container.Unpause(ctx, w.clientWrapper.client, []string{cid}, options)
}

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

package container

import (
"context"
"fmt"

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

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

func (s *service) Unpause(ctx context.Context, cid string, options ncTypes.ContainerUnpauseOptions) 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.Paused {
if status == containerd.Running {
return errdefs.NewConflict(fmt.Errorf("container %s is already running", cid))
}
return errdefs.NewConflict(fmt.Errorf("container %s is not paused", cid))
}

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

return nil
}
Loading
Loading