Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update container inspect with size option #157

Merged
merged 3 commits into from
Feb 18, 2025
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: 1 addition & 1 deletion api/handlers/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Service interface {
Stop(ctx context.Context, cid string, timeout *time.Duration) error
Restart(ctx context.Context, cid string, timeout time.Duration) error
Create(ctx context.Context, image string, cmd []string, createOpt ncTypes.ContainerCreateOptions, netOpt ncTypes.NetworkOptions) (string, error)
Inspect(ctx context.Context, cid string) (*types.Container, error)
Inspect(ctx context.Context, cid string, size bool) (*types.Container, error)
WriteFilesAsTarArchive(filePath string, writer io.Writer, slashDot bool) error
Attach(ctx context.Context, cid string, opts *types.AttachOptions) error
List(ctx context.Context, listOpts ncTypes.ContainerListOptions) ([]types.ContainerListItem, error)
Expand Down
2 changes: 1 addition & 1 deletion api/handlers/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ var _ = Describe("Container API", func() {
})
It("should call container inspect method", func() {
// setup mocks
service.EXPECT().Inspect(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error from inspect api"))
service.EXPECT().Inspect(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error from inspect api"))
req, _ = http.NewRequest(http.MethodGet, "/containers/123/json", nil)
// call the API to check if it returns the error generated from inspect method
router.ServeHTTP(rr, req)
Expand Down
7 changes: 6 additions & 1 deletion api/handlers/container/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package container

import (
"net/http"
"strconv"

"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/gorilla/mux"
Expand All @@ -16,8 +17,12 @@ import (

func (h *handler) inspect(w http.ResponseWriter, r *http.Request) {
cid := mux.Vars(r)["id"]
sizeflag, err := strconv.ParseBool(r.URL.Query().Get("size"))
if err != nil {
sizeflag = false
}
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)
c, err := h.service.Inspect(ctx, cid)
c, err := h.service.Inspect(ctx, cid, sizeflag)
// map the error into http status code and send response.
if err != nil {
var code int
Expand Down
53 changes: 43 additions & 10 deletions api/handlers/container/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var _ = Describe("Container Inspect API", func() {
req *http.Request
resp types.Container
respJSON []byte
req2 *http.Request
err error
)
BeforeEach(func() {
mockCtrl = gomock.NewController(GinkgoT())
Expand All @@ -42,29 +44,60 @@ var _ = Describe("Container Inspect API", func() {
h = newHandler(service, &c, logger)
rr = httptest.NewRecorder()
cid = "123"
var err error
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/containers/%s/json", cid), nil)
Expect(err).Should(BeNil())
req = mux.SetURLVars(req, map[string]string{"id": "123"})

// Create a helper function to create and set up requests
createRequest := func(sizeParam bool) *http.Request {
url := fmt.Sprintf("/containers/%s/json", cid)
if sizeParam {
url += "?size=true"
}
req, err := http.NewRequest(http.MethodGet, url, nil)
Expect(err).Should(BeNil())
return mux.SetURLVars(req, map[string]string{"id": cid})
}

// Create both requests using the helper function
req = createRequest(false)
req2 = createRequest(true)

sizeRw := int64(1000)
sizeRootFs := int64(5000)
resp = types.Container{
ID: cid,
Image: "test-image",
Name: "/test-container",
ID: cid,
Image: "test-image",
Name: "/test-container",
SizeRw: &sizeRw,
SizeRootFs: &sizeRootFs,
}
respJSON, err = json.Marshal(resp)
Expect(err).Should(BeNil())
})
Context("handler", func() {
It("should return inspect object and 200 status code upon success", func() {
service.EXPECT().Inspect(gomock.Any(), cid).Return(&resp, nil)
service.EXPECT().Inspect(gomock.Any(), cid, false).Return(&resp, nil)

// handler should return response object with 200 status code
h.inspect(rr, req)
Expect(rr.Body).Should(MatchJSON(respJSON))
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
})
It("should return inspect object with size information and 200 status code upon success", func() {
service.EXPECT().Inspect(gomock.Any(), cid, true).Return(&resp, nil)

h.inspect(rr, req2)
Expect(rr.Body).Should(MatchJSON(respJSON))
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))

var returnedResp types.Container
err := json.Unmarshal(rr.Body.Bytes(), &returnedResp)
Expect(err).Should(BeNil())
Expect(returnedResp.SizeRw).ShouldNot(BeNil())
Expect(*returnedResp.SizeRw).Should(Equal(int64(1000)))
Expect(returnedResp.SizeRootFs).ShouldNot(BeNil())
Expect(*returnedResp.SizeRootFs).Should(Equal(int64(5000)))
})
It("should return 404 status code if container was not found", func() {
service.EXPECT().Inspect(gomock.Any(), cid).Return(nil, errdefs.NewNotFound(fmt.Errorf("no such container")))
service.EXPECT().Inspect(gomock.Any(), cid, false).Return(nil, errdefs.NewNotFound(fmt.Errorf("no such container")))
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())

// handler should return error message with 404 status code
Expand All @@ -73,7 +106,7 @@ var _ = Describe("Container Inspect API", func() {
Expect(rr).Should(HaveHTTPStatus(http.StatusNotFound))
})
It("should return 500 status code if service returns an error message", func() {
service.EXPECT().Inspect(gomock.Any(), cid).Return(nil, fmt.Errorf("error"))
service.EXPECT().Inspect(gomock.Any(), cid, false).Return(nil, fmt.Errorf("error"))
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())

// handler should return error message
Expand Down
4 changes: 2 additions & 2 deletions api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ type Container struct {
// TODO: ExecIDs []string
// TODO: HostConfig *container.HostConfig
// TODO: GraphDriver GraphDriverData
// TODO: SizeRw *int64 `json:",omitempty"`
// TODO: SizeRootFs *int64 `json:",omitempty"`
SizeRw *int64 `json:",omitempty"`
SizeRootFs *int64 `json:",omitempty"`

Mounts []dockercompat.MountPoint
Config *ContainerConfig
Expand Down
1 change: 1 addition & 0 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestRun(t *testing.T) {
tests.ContainerAttach(opt)
tests.ContainerLogs(opt)
tests.ContainerKill(opt)
tests.ContainerInspect(opt)

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

package tests

import (
"encoding/json"
"fmt"
"io"
"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/types"
"github.com/runfinch/finch-daemon/e2e/client"
)

// ContainerInspect tests the `GET containers/{id}/json` API.
func ContainerInspect(opt *option.Option) {
Describe("inspect container", func() {
var (
uClient *http.Client
version string
containerId string
containerName string
wantContainerName string
)
BeforeEach(func() {
uClient = client.NewClient(GetDockerHostUrl())
version = GetDockerApiVersion()
containerName = testContainerName
wantContainerName = fmt.Sprintf("/%s", containerName)
containerId = command.StdoutStr(opt, "run", "-d", "--name", containerName, defaultImage, "sleep", "infinity")
})
AfterEach(func() {
command.RemoveAll(opt)
})

It("should inspect the container by ID", func() {
res, err := uClient.Get(client.ConvertToFinchUrl(version, fmt.Sprintf("/containers/%s/json", containerId)))
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusOK))
var got types.Container
err = json.NewDecoder(res.Body).Decode(&got)
Expect(err).Should(BeNil())
Expect(got.ID).Should(Equal(containerId))
Expect(got.Name).Should(Equal(wantContainerName))
Expect(got.State.Status).Should(Equal("running"))
})

It("should inspect the container by name", func() {
res, err := uClient.Get(client.ConvertToFinchUrl(version, fmt.Sprintf("/containers/%s/json", containerName)))
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusOK))
var got types.Container
err = json.NewDecoder(res.Body).Decode(&got)
Expect(err).Should(BeNil())
Expect(got.ID).Should(Equal(containerId))
Expect(got.Name).Should(Equal(wantContainerName))
Expect(got.State.Status).Should(Equal("running"))
})

It("should return size information when size parameter is true", func() {
res, err := uClient.Get(client.ConvertToFinchUrl(version, fmt.Sprintf("/containers/%s/json?size=1", containerId)))
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusOK))
var got types.Container
err = json.NewDecoder(res.Body).Decode(&got)
Expect(err).Should(BeNil())
Expect(got.SizeRw).ShouldNot(BeNil())
Expect(got.SizeRootFs).ShouldNot(BeNil())
})

It("should return 404 error when container does not exist", func() {
res, err := uClient.Get(client.ConvertToFinchUrl(version, "/containers/nonexistent/json"))
Expect(err).Should(BeNil())
Expect(res.StatusCode).Should(Equal(http.StatusNotFound))
body, err := io.ReadAll(res.Body)
Expect(err).Should(BeNil())
defer res.Body.Close()
Expect(body).Should(MatchJSON(`{"message": "no such container: nonexistent"}`))
})
})
}
32 changes: 28 additions & 4 deletions internal/backend/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
package backend

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
Expand All @@ -27,7 +30,7 @@ type NerdctlContainerSvc interface {
StartContainer(ctx context.Context, container containerd.Container) error
StopContainer(ctx context.Context, container containerd.Container, timeout *time.Duration) error
CreateContainer(ctx context.Context, args []string, netManager containerutil.NetworkOptionsManager, options types.ContainerCreateOptions) (containerd.Container, func(), error)
InspectContainer(ctx context.Context, c containerd.Container) (*dockercompat.Container, error)
InspectContainer(ctx context.Context, c containerd.Container, size bool) (*dockercompat.Container, error)
InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error)
NewNetworkingOptionsManager(types.NetworkOptions) (containerutil.NetworkOptionsManager, error)
ListContainers(ctx context.Context, options types.ContainerListOptions) ([]container.ListItem, error)
Expand Down Expand Up @@ -61,12 +64,33 @@ func (w *NerdctlWrapper) CreateContainer(ctx context.Context, args []string, net
return container.Create(ctx, w.clientWrapper.client, args, netManager, options)
}

func (w *NerdctlWrapper) InspectContainer(ctx context.Context, c containerd.Container) (*dockercompat.Container, error) {
n, err := containerinspector.Inspect(ctx, c)
func (w *NerdctlWrapper) InspectContainer(ctx context.Context, c containerd.Container, sizeFlag bool) (*dockercompat.Container, error) {
var buf bytes.Buffer
options := types.ContainerInspectOptions{
Mode: "dockercompat",
Stdout: &buf,
Size: sizeFlag,
GOptions: types.GlobalCommandOptions{
Snapshotter: w.globalOptions.Snapshotter,
},
}

err := container.Inspect(ctx, w.clientWrapper.client, []string{c.ID()}, options)
if err != nil {
return nil, err
}
return dockercompat.ContainerFromNative(n)

// Parse the JSON response
var containers []*dockercompat.Container
if err := json.Unmarshal(buf.Bytes(), &containers); err != nil {
return nil, err
}

if len(containers) != 1 {
return nil, fmt.Errorf("expected 1 container, got %d", len(containers))
}

return containers[0], nil
}

func (w *NerdctlWrapper) InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error) {
Expand Down
6 changes: 4 additions & 2 deletions internal/service/container/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ import (

const networkPrefix = "unknown-eth"

func (s *service) Inspect(ctx context.Context, cid string) (*types.Container, error) {
func (s *service) Inspect(ctx context.Context, cid string, sizeFlag bool) (*types.Container, error) {
c, err := s.getContainer(ctx, cid)
if err != nil {
return nil, err
}

inspect, err := s.nctlContainerSvc.InspectContainer(ctx, c)
inspect, err := s.nctlContainerSvc.InspectContainer(ctx, c, sizeFlag)
if err != nil {
return nil, err
}
Expand All @@ -47,6 +47,8 @@ func (s *service) Inspect(ctx context.Context, cid string) (*types.Container, er
AppArmorProfile: inspect.AppArmorProfile,
Mounts: inspect.Mounts,
NetworkSettings: inspect.NetworkSettings,
SizeRw: inspect.SizeRw,
SizeRootFs: inspect.SizeRootFs,
}

cont.Config = &types.ContainerConfig{
Expand Down
Loading
Loading