Skip to content

Commit 08878dc

Browse files
authored
fix: refactor wait API (runfinch#177)
Signed-off-by: Arjun Raja Yogidas <[email protected]>
1 parent 1bcfebd commit 08878dc

File tree

9 files changed

+256
-38
lines changed

9 files changed

+256
-38
lines changed

api/handlers/container/container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
type Service interface {
2222
GetPathToFilesInContainer(ctx context.Context, cid string, path string) (string, func(), error)
2323
Remove(ctx context.Context, cid string, force, removeVolumes bool) error
24-
Wait(ctx context.Context, cid string, condition string) (code int64, err error)
24+
Wait(ctx context.Context, cid string, options ncTypes.ContainerWaitOptions) error
2525
Start(ctx context.Context, cid string, options ncTypes.ContainerStartOptions) error
2626
Stop(ctx context.Context, cid string, timeout *time.Duration) error
2727
Restart(ctx context.Context, cid string, options ncTypes.ContainerRestartOptions) error

api/handlers/container/wait.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
package container
55

66
import (
7+
"fmt"
78
"net/http"
9+
"strconv"
10+
"strings"
811

912
"github.com/containerd/containerd/v2/pkg/namespaces"
13+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
1014
"github.com/gorilla/mux"
1115
"github.com/sirupsen/logrus"
1216

@@ -19,19 +23,45 @@ type waitError struct {
1923
}
2024

2125
type waitResp struct {
22-
StatusCode int64
26+
StatusCode int
2327
Error *waitError `json:",omitempty"`
2428
}
2529

30+
type codeCapturingWriter struct {
31+
code int
32+
}
33+
34+
func (w *codeCapturingWriter) Write(p []byte) (n int, err error) {
35+
w.code, _ = strconv.Atoi(strings.TrimSpace(string(p)))
36+
return len(p), nil
37+
}
38+
2639
func (h *handler) wait(w http.ResponseWriter, r *http.Request) {
2740
cid := mux.Vars(r)["id"]
2841

2942
// TODO: condition is not used because nerdctl doesn't support it
3043
condition := r.URL.Query().Get("condition")
44+
if condition != "" {
45+
logrus.Debugf("Wait container API called with condition options - not supported.")
46+
response.SendErrorResponse(w, http.StatusBadRequest, fmt.Errorf("wait condition is not supported"))
47+
return
48+
}
3149
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)
32-
code, err := h.service.Wait(ctx, cid, condition)
3350

34-
if code == -1 {
51+
// Create the custom writer
52+
codeWriter := &codeCapturingWriter{}
53+
54+
globalOpt := ncTypes.GlobalCommandOptions(*h.Config)
55+
options := ncTypes.ContainerWaitOptions{
56+
GOptions: globalOpt,
57+
Stdout: codeWriter,
58+
}
59+
60+
err := h.service.Wait(ctx, cid, options)
61+
62+
code := codeWriter.code
63+
64+
if err != nil {
3565
var errorCode int
3666
switch {
3767
case errdefs.IsNotFound(err):
@@ -47,13 +77,10 @@ func (h *handler) wait(w http.ResponseWriter, r *http.Request) {
4777
return
4878
}
4979

50-
waitResponse := waitResp{
51-
StatusCode: code,
52-
}
5380
// if there is no err then don't need to set the error msg. e.g. when container is stopped the wait should return
5481
// {"Error":null,"StatusCode":0}
55-
if err != nil {
56-
waitResponse.Error = &waitError{Message: err.Error()}
82+
waitResponse := waitResp{
83+
StatusCode: code,
5784
}
5885

5986
response.JSON(w, http.StatusOK, waitResponse)

e2e/e2e_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func TestRun(t *testing.T) {
4949
tests.ContainerLogs(opt)
5050
tests.ContainerKill(opt)
5151
tests.ContainerInspect(opt)
52+
tests.ContainerWait(opt)
5253

5354
// functional test for volume APIs
5455
tests.VolumeList(opt)

e2e/tests/container_wait.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
// ContainerWait tests the `POST containers/{id}/wait` API.
21+
func ContainerWait(opt *option.Option) {
22+
Describe("wait for a container", func() {
23+
var (
24+
uClient *http.Client
25+
version string
26+
apiUrl string
27+
)
28+
29+
BeforeEach(func() {
30+
// create a custom client to use http over unix sockets
31+
uClient = client.NewClient(GetDockerHostUrl())
32+
// get the docker api version that will be tested
33+
version = GetDockerApiVersion()
34+
relativeUrl := fmt.Sprintf("/containers/%s/wait", testContainerName)
35+
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
36+
})
37+
38+
AfterEach(func() {
39+
command.RemoveAll(opt)
40+
})
41+
42+
It("should wait for the container to exit and return status code", func() {
43+
// start a container and exit after 3s
44+
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "3")
45+
46+
// call wait on this container
47+
relativeUrl := fmt.Sprintf("/containers/%s/wait", testContainerName)
48+
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
49+
50+
res, err := uClient.Post(apiUrl, "application/json", nil)
51+
Expect(err).Should(BeNil())
52+
Expect(res.StatusCode).Should(Equal(http.StatusOK))
53+
54+
var waitResponse struct {
55+
StatusCode int `json:"StatusCode"`
56+
Error string `json:"Error"`
57+
}
58+
err = json.NewDecoder(res.Body).Decode(&waitResponse)
59+
Expect(err).Should(BeNil())
60+
Expect(waitResponse.StatusCode).Should(Equal(0))
61+
Expect(waitResponse.Error).Should(BeEmpty())
62+
})
63+
64+
It("should fail when container does not exist", func() {
65+
// don't create the container and call wait api
66+
res, err := uClient.Post(apiUrl, "application/json", nil)
67+
Expect(err).Should(BeNil())
68+
Expect(res.StatusCode).Should(Equal(http.StatusNotFound))
69+
70+
var errResponse response.Error
71+
err = json.NewDecoder(res.Body).Decode(&errResponse)
72+
Expect(err).Should(BeNil())
73+
Expect(errResponse.Message).Should(Not(BeEmpty()))
74+
})
75+
76+
It("should reject wait condition parameter", func() {
77+
// start a container
78+
command.Run(opt, "run", "-d", "--name", testContainerName, defaultImage, "sleep", "5")
79+
80+
// try to wait with a condition
81+
relativeUrl := fmt.Sprintf("/containers/%s/wait?condition=not-running", testContainerName)
82+
apiUrl = client.ConvertToFinchUrl(version, relativeUrl)
83+
84+
res, err := uClient.Post(apiUrl, "application/json", nil)
85+
Expect(err).Should(BeNil())
86+
Expect(res.StatusCode).Should(Equal(http.StatusBadRequest))
87+
88+
var errResponse response.Error
89+
err = json.NewDecoder(res.Body).Decode(&errResponse)
90+
Expect(err).Should(BeNil())
91+
Expect(errResponse.Message).Should(ContainSubstring("condition"))
92+
})
93+
})
94+
}

internal/backend/container.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type NerdctlContainerSvc interface {
3636
ListContainers(ctx context.Context, options types.ContainerListOptions) ([]container.ListItem, error)
3737
RenameContainer(ctx context.Context, container containerd.Container, newName string, options types.ContainerRenameOptions) error
3838
KillContainer(ctx context.Context, cid string, options types.ContainerKillOptions) error
39+
ContainerWait(ctx context.Context, cid string, options types.ContainerWaitOptions) error
3940

4041
// Mocked functions for container attach
4142
GetDataStore() (string, error)
@@ -125,6 +126,10 @@ func (w *NerdctlWrapper) KillContainer(ctx context.Context, cid string, options
125126
return container.Kill(ctx, w.clientWrapper.client, []string{cid}, options)
126127
}
127128

129+
func (w *NerdctlWrapper) ContainerWait(ctx context.Context, cid string, options types.ContainerWaitOptions) error {
130+
return container.Wait(ctx, w.clientWrapper.client, []string{cid}, options)
131+
}
132+
128133
func (w *NerdctlWrapper) GetNerdctlExe() (string, error) {
129134
if w.nerdctlExe != "" {
130135
return w.nerdctlExe, nil

internal/service/container/wait.go

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,20 @@ package container
66
import (
77
"context"
88

9-
containerd "github.com/containerd/containerd/v2/client"
9+
cerrdefs "github.com/containerd/errdefs"
10+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
11+
"github.com/runfinch/finch-daemon/pkg/errdefs"
1012
)
1113

12-
func (s *service) Wait(ctx context.Context, cid string, condition string) (code int64, err error) {
13-
con, err := s.getContainer(ctx, cid)
14-
// container wait status code is uint32, use -1 to indicate container search error
14+
func (s *service) Wait(ctx context.Context, cid string, options ncTypes.ContainerWaitOptions) error {
15+
cont, err := s.getContainer(ctx, cid)
1516
if err != nil {
16-
return -1, err
17+
if cerrdefs.IsNotFound(err) {
18+
return errdefs.NewNotFound(err)
19+
}
20+
return err
1721
}
18-
s.logger.Debugf("wait container: %s", con.ID())
19-
rawcode, err := waitContainer(ctx, con)
20-
return int64(rawcode), err
21-
}
22-
23-
// TODO: contribute to nerdctl to make this function public.
24-
func waitContainer(ctx context.Context, container containerd.Container) (code uint32, err error) {
25-
task, err := container.Task(ctx, nil)
26-
if err != nil {
27-
return 0, err
28-
}
29-
30-
statusC, err := task.Wait(ctx)
31-
if err != nil {
32-
return 0, err
33-
}
34-
35-
status := <-statusC
36-
code, _, err = status.Result()
3722

38-
return code, err
23+
s.logger.Debugf("wait container: %s", cont.ID())
24+
return s.nctlContainerSvc.ContainerWait(ctx, cont.ID(), options)
3925
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
"errors"
9+
"fmt"
10+
11+
"github.com/golang/mock/gomock"
12+
. "github.com/onsi/ginkgo/v2"
13+
. "github.com/onsi/gomega"
14+
15+
containerd "github.com/containerd/containerd/v2/client"
16+
cerrdefs "github.com/containerd/errdefs"
17+
ncTypes "github.com/containerd/nerdctl/v2/pkg/api/types"
18+
"github.com/runfinch/finch-daemon/mocks/mocks_backend"
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 Wait API", func() {
25+
var (
26+
ctx context.Context
27+
mockCtrl *gomock.Controller
28+
logger *mocks_logger.Logger
29+
cdClient *mocks_backend.MockContainerdClient
30+
ncContainerSvc *mocks_backend.MockNerdctlContainerSvc
31+
svc *service
32+
cid string
33+
waitOptions ncTypes.ContainerWaitOptions
34+
con *mocks_container.MockContainer
35+
)
36+
37+
BeforeEach(func() {
38+
ctx = context.Background()
39+
mockCtrl = gomock.NewController(GinkgoT())
40+
logger = mocks_logger.NewLogger(mockCtrl)
41+
cdClient = mocks_backend.NewMockContainerdClient(mockCtrl)
42+
ncContainerSvc = mocks_backend.NewMockNerdctlContainerSvc(mockCtrl)
43+
44+
cid = "test-container-id"
45+
waitOptions = ncTypes.ContainerWaitOptions{}
46+
con = mocks_container.NewMockContainer(mockCtrl)
47+
con.EXPECT().ID().Return(cid).AnyTimes()
48+
49+
svc = &service{
50+
client: cdClient,
51+
nctlContainerSvc: mockNerdctlService{ncContainerSvc, nil},
52+
logger: logger,
53+
}
54+
})
55+
56+
AfterEach(func() {
57+
mockCtrl.Finish()
58+
})
59+
60+
Context("Wait API", func() {
61+
It("should successfully wait for a container", func() {
62+
cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return(
63+
[]containerd.Container{con}, nil)
64+
logger.EXPECT().Debugf("wait container: %s", cid)
65+
ncContainerSvc.EXPECT().ContainerWait(ctx, cid, waitOptions).Return(nil)
66+
67+
err := svc.Wait(ctx, cid, waitOptions)
68+
Expect(err).Should(BeNil())
69+
})
70+
71+
It("should return NotFound error if container is not found", func() {
72+
mockErr := cerrdefs.ErrNotFound.WithMessage(fmt.Sprintf("no such container: %s", cid))
73+
cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return(nil, mockErr)
74+
logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any())
75+
76+
err := svc.Wait(ctx, cid, waitOptions)
77+
Expect(err.Error()).Should(Equal(errdefs.NewNotFound(fmt.Errorf("no such container: %s", cid)).Error()))
78+
})
79+
80+
It("should return an error if ContainerWait fails", func() {
81+
cdClient.EXPECT().SearchContainer(gomock.Any(), cid).Return(
82+
[]containerd.Container{con}, nil)
83+
logger.EXPECT().Debugf("wait container: %s", cid)
84+
mockErr := errors.New("error waiting for container")
85+
ncContainerSvc.EXPECT().ContainerWait(ctx, cid, waitOptions).Return(mockErr)
86+
87+
err := svc.Wait(ctx, cid, waitOptions)
88+
Expect(err).Should(Equal(mockErr))
89+
})
90+
91+
})
92+
})

mocks/mocks_backend/nerdctlcontainersvc.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mocks/mocks_container/containersvc.go

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)