Skip to content

Commit 0f9194a

Browse files
authored
Merge pull request #82 from coderbirju/unpause-container
feat: Add unpause container support
2 parents d8a6c4a + 6348542 commit 0f9194a

File tree

10 files changed

+469
-0
lines changed

10 files changed

+469
-0
lines changed

api/handlers/container/container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Service interface {
3737
ExecCreate(ctx context.Context, cid string, config types.ExecConfig) (string, error)
3838
Kill(ctx context.Context, cid string, options ncTypes.ContainerKillOptions) error
3939
Pause(ctx context.Context, cid string, options ncTypes.ContainerPauseOptions) error
40+
Unpause(ctx context.Context, cid string, options ncTypes.ContainerUnpauseOptions) error
4041
}
4142

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

6769
// newHandler creates the handler that serves all the container related APIs.

api/handlers/container/unpause.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package container
5+
6+
import (
7+
"net/http"
8+
"os"
9+
10+
"github.com/containerd/containerd/v2/pkg/namespaces"
11+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
12+
"github.com/gorilla/mux"
13+
14+
"github.com/runfinch/finch-daemon/api/response"
15+
"github.com/runfinch/finch-daemon/pkg/errdefs"
16+
)
17+
18+
// unpause resumes a paused container.
19+
func (h *handler) unpause(w http.ResponseWriter, r *http.Request) {
20+
cid, ok := mux.Vars(r)["id"]
21+
if !ok || cid == "" {
22+
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("must specify a container ID"))
23+
return
24+
}
25+
26+
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)
27+
28+
devNull, err := os.OpenFile("/dev/null", os.O_WRONLY, 0600)
29+
if err != nil {
30+
response.JSON(w, http.StatusBadRequest, response.NewErrorFromMsg("failed to open /dev/null"))
31+
return
32+
}
33+
defer devNull.Close()
34+
35+
globalOpt := ncTypes.GlobalCommandOptions(*h.Config)
36+
options := ncTypes.ContainerUnpauseOptions{
37+
GOptions: globalOpt,
38+
Stdout: devNull,
39+
}
40+
41+
err = h.service.Unpause(ctx, cid, options)
42+
if err != nil {
43+
var code int
44+
switch {
45+
case errdefs.IsNotFound(err):
46+
code = http.StatusNotFound
47+
case errdefs.IsConflict(err):
48+
code = http.StatusConflict
49+
default:
50+
code = http.StatusInternalServerError
51+
}
52+
response.JSON(w, code, response.NewError(err))
53+
return
54+
}
55+
56+
response.Status(w, http.StatusNoContent)
57+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package container
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
"net/http/httptest"
11+
12+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
13+
"github.com/containerd/nerdctl/v2/pkg/config"
14+
"github.com/golang/mock/gomock"
15+
"github.com/gorilla/mux"
16+
. "github.com/onsi/ginkgo/v2"
17+
. "github.com/onsi/gomega"
18+
19+
"github.com/runfinch/finch-daemon/mocks/mocks_container"
20+
"github.com/runfinch/finch-daemon/mocks/mocks_logger"
21+
"github.com/runfinch/finch-daemon/pkg/errdefs"
22+
)
23+
24+
var _ = Describe("Container Unpause API", func() {
25+
var (
26+
mockCtrl *gomock.Controller
27+
logger *mocks_logger.Logger
28+
service *mocks_container.MockService
29+
h *handler
30+
rr *httptest.ResponseRecorder
31+
_ ncTypes.GlobalCommandOptions
32+
_ error
33+
)
34+
35+
BeforeEach(func() {
36+
mockCtrl = gomock.NewController(GinkgoT())
37+
defer mockCtrl.Finish()
38+
logger = mocks_logger.NewLogger(mockCtrl)
39+
service = mocks_container.NewMockService(mockCtrl)
40+
c := config.Config{}
41+
h = newHandler(service, &c, logger)
42+
rr = httptest.NewRecorder()
43+
})
44+
45+
Context("unpause handler", func() {
46+
It("should return 204 No Content on successful unpause", func() {
47+
req, err := http.NewRequest(http.MethodPost, "/containers/id1/unpause", nil)
48+
Expect(err).Should(BeNil())
49+
req = mux.SetURLVars(req, map[string]string{"id": "id1"})
50+
51+
service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).DoAndReturn(
52+
func(ctx context.Context, cid string, opts ncTypes.ContainerUnpauseOptions) error {
53+
return nil
54+
})
55+
56+
h.unpause(rr, req)
57+
Expect(rr.Body.String()).Should(BeEmpty())
58+
Expect(rr).Should(HaveHTTPStatus(http.StatusNoContent))
59+
})
60+
61+
It("should return 400 when container ID is missing", func() {
62+
req, err := http.NewRequest(http.MethodPost, "/containers//pause", nil)
63+
Expect(err).Should(BeNil())
64+
req = mux.SetURLVars(req, map[string]string{"id": ""})
65+
66+
h.unpause(rr, req)
67+
Expect(rr.Body).Should(MatchJSON(`{"message": "must specify a container ID"}`))
68+
Expect(rr).Should(HaveHTTPStatus(http.StatusBadRequest))
69+
})
70+
71+
It("should return 404 when service returns a not found error", func() {
72+
req, err := http.NewRequest(http.MethodPost, "/containers/id1/unpause", nil)
73+
Expect(err).Should(BeNil())
74+
req = mux.SetURLVars(req, map[string]string{"id": "id1"})
75+
76+
service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).Return(
77+
errdefs.NewNotFound(fmt.Errorf("container not found")))
78+
79+
h.unpause(rr, req)
80+
Expect(rr.Body).Should(MatchJSON(`{"message": "container not found"}`))
81+
Expect(rr).Should(HaveHTTPStatus(http.StatusNotFound))
82+
})
83+
84+
It("should return 409 when service returns a conflict error", func() {
85+
req, err := http.NewRequest(http.MethodPost, "/containers/id1/unpause", nil)
86+
Expect(err).Should(BeNil())
87+
req = mux.SetURLVars(req, map[string]string{"id": "id1"})
88+
89+
service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).Return(
90+
errdefs.NewConflict(fmt.Errorf("container not paused")))
91+
92+
h.unpause(rr, req)
93+
Expect(rr.Body).Should(MatchJSON(`{"message": "container not paused"}`))
94+
Expect(rr).Should(HaveHTTPStatus(http.StatusConflict))
95+
})
96+
97+
It("should return 500 when service returns an internal error", func() {
98+
req, err := http.NewRequest(http.MethodPost, "/containers/id1/unpause", nil)
99+
Expect(err).Should(BeNil())
100+
req = mux.SetURLVars(req, map[string]string{"id": "id1"})
101+
102+
service.EXPECT().Unpause(gomock.Any(), "id1", gomock.Any()).Return(
103+
fmt.Errorf("unexpected internal error"))
104+
105+
h.unpause(rr, req)
106+
Expect(rr.Body).Should(MatchJSON(`{"message": "unexpected internal error"}`))
107+
Expect(rr).Should(HaveHTTPStatus(http.StatusInternalServerError))
108+
})
109+
})
110+
})

e2e/e2e_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func TestRun(t *testing.T) {
7373
tests.ContainerInspect(opt)
7474
tests.ContainerWait(opt)
7575
tests.ContainerPause(opt)
76+
tests.ContainerUnpause(opt)
7677

7778
// functional test for volume APIs
7879
tests.VolumeList(opt)

e2e/tests/container_unpause.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package tests
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
"github.com/runfinch/common-tests/command"
14+
"github.com/runfinch/common-tests/option"
15+
16+
"github.com/runfinch/finch-daemon/api/response"
17+
"github.com/runfinch/finch-daemon/e2e/client"
18+
)
19+
20+
func ContainerUnpause(opt *option.Option) {
21+
Describe("unpause a container", func() {
22+
var (
23+
uClient *http.Client
24+
version string
25+
unpauseUrl string
26+
pauseUrl string
27+
)
28+
29+
BeforeEach(func() {
30+
uClient = client.NewClient(GetDockerHostUrl())
31+
version = GetDockerApiVersion()
32+
relativeUrl_pause := fmt.Sprintf("/containers/%s/pause", testContainerName)
33+
relativeUrl_unpause := fmt.Sprintf("/containers/%s/unpause", testContainerName)
34+
unpauseUrl = client.ConvertToFinchUrl(version, relativeUrl_unpause)
35+
pauseUrl = client.ConvertToFinchUrl(version, relativeUrl_pause)
36+
})
37+
38+
AfterEach(func() {
39+
command.RemoveAll(opt)
40+
})
41+
42+
It("should unpause a paused container", func() {
43+
// Start a container that keeps running
44+
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")
45+
46+
// Pause this running container
47+
res, err := uClient.Post(pauseUrl, "application/json", nil)
48+
Expect(err).Should(BeNil())
49+
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))
50+
51+
// Verify container is paused
52+
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
53+
Expect(output).Should(Equal("paused"))
54+
55+
// Unpause the paused container
56+
res, err = uClient.Post(unpauseUrl, "application/json", nil)
57+
Expect(err).Should(BeNil())
58+
Expect(res.StatusCode).Should(Equal(http.StatusNoContent))
59+
60+
// Verify container is running again
61+
output = command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
62+
Expect(output).Should(Equal("running"))
63+
})
64+
65+
It("should fail to unpause a non-existent container", func() {
66+
res, err := uClient.Post(unpauseUrl, "application/json", nil)
67+
Expect(err).Should(BeNil())
68+
Expect(res.StatusCode).Should(Equal(http.StatusNotFound))
69+
70+
var body response.Error
71+
err = json.NewDecoder(res.Body).Decode(&body)
72+
Expect(err).Should(BeNil())
73+
})
74+
75+
It("should fail to unpause a running container", func() {
76+
// Start a container that keeps running
77+
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")
78+
79+
// Try to unpause the running container
80+
res, err := uClient.Post(unpauseUrl, "application/json", nil)
81+
Expect(err).Should(BeNil())
82+
Expect(res.StatusCode).Should(Equal(http.StatusConflict))
83+
84+
var body response.Error
85+
err = json.NewDecoder(res.Body).Decode(&body)
86+
Expect(err).Should(BeNil())
87+
Expect(body.Message).Should(Equal(fmt.Sprintf("container %s is already running", testContainerName)))
88+
89+
// Verify container is still running
90+
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
91+
Expect(output).Should(Equal("running"))
92+
})
93+
94+
It("should fail to unpause a stopped container", func() {
95+
// Create and start a container
96+
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "infinity")
97+
98+
// Verify container is running
99+
output := command.StdoutStr(opt, "inspect", "--format", "{{.State.Status}}", testContainerName)
100+
Expect(output).Should(Equal("running"))
101+
102+
// Stop the container with a timeout to ensure it stops
103+
command.Run(opt, "stop", "-t", "1", testContainerName)
104+
105+
// Try to unpause the stopped container
106+
res, err := uClient.Post(unpauseUrl, "application/json", nil)
107+
Expect(err).Should(BeNil())
108+
Expect(res.StatusCode).Should(Equal(http.StatusConflict))
109+
110+
var body response.Error
111+
err = json.NewDecoder(res.Body).Decode(&body)
112+
Expect(err).Should(BeNil())
113+
Expect(body.Message).Should(Equal(fmt.Sprintf("container %s is not paused", testContainerName)))
114+
})
115+
})
116+
}

internal/backend/container.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type NerdctlContainerSvc interface {
3838
KillContainer(ctx context.Context, cid string, options types.ContainerKillOptions) error
3939
ContainerWait(ctx context.Context, cid string, options types.ContainerWaitOptions) error
4040
PauseContainer(ctx context.Context, cid string, options types.ContainerPauseOptions) error
41+
UnpauseContainer(ctx context.Context, cid string, options types.ContainerUnpauseOptions) error
4142

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

139+
func (w *NerdctlWrapper) UnpauseContainer(ctx context.Context, cid string, options types.ContainerUnpauseOptions) error {
140+
return container.Unpause(ctx, w.clientWrapper.client, []string{cid}, options)
141+
}
142+
138143
func (w *NerdctlWrapper) GetNerdctlExe() (string, error) {
139144
if w.nerdctlExe != "" {
140145
return w.nerdctlExe, nil

internal/service/container/unpause.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package container
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
containerd "github.com/containerd/containerd/v2/client"
11+
cerrdefs "github.com/containerd/errdefs"
12+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
13+
14+
"github.com/runfinch/finch-daemon/pkg/errdefs"
15+
)
16+
17+
func (s *service) Unpause(ctx context.Context, cid string, options ncTypes.ContainerUnpauseOptions) error {
18+
cont, err := s.getContainer(ctx, cid)
19+
if err != nil {
20+
if cerrdefs.IsNotFound(err) {
21+
return errdefs.NewNotFound(err)
22+
}
23+
return err
24+
}
25+
status := s.client.GetContainerStatus(ctx, cont)
26+
if status != containerd.Paused {
27+
if status == containerd.Running {
28+
return errdefs.NewConflict(fmt.Errorf("container %s is already running", cid))
29+
}
30+
return errdefs.NewConflict(fmt.Errorf("container %s is not paused", cid))
31+
}
32+
33+
err = s.nctlContainerSvc.UnpauseContainer(ctx, cid, options)
34+
if err != nil {
35+
return err
36+
}
37+
38+
return nil
39+
}

0 commit comments

Comments
 (0)