Skip to content

Commit 7400fda

Browse files
authored
feat: container listing with pagination, search, and sort (#367)
1 parent 5912ce5 commit 7400fda

File tree

9 files changed

+507
-115
lines changed

9 files changed

+507
-115
lines changed

api/api/versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"version": "v1",
55
"status": "active",
6-
"release_date": "2025-08-28T18:36:20.486085+05:30",
6+
"release_date": "2025-09-03T02:32:19.06053+05:30",
77
"end_of_life": "0001-01-01T00:00:00Z",
88
"changes": [
99
"Initial API version"

api/internal/features/container/controller/list_containers.go

Lines changed: 209 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,247 @@ package controller
22

33
import (
44
"net/http"
5+
"sort"
6+
"strconv"
7+
"strings"
58

9+
"github.com/docker/docker/api/types/container"
10+
"github.com/docker/docker/api/types/filters"
611
"github.com/go-fuego/fuego"
7-
"github.com/raghavyuva/nixopus-api/internal/features/container/types"
12+
containertypes "github.com/raghavyuva/nixopus-api/internal/features/container/types"
813
"github.com/raghavyuva/nixopus-api/internal/features/logger"
914
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
1015
)
1116

12-
func (c *ContainerController) ListContainers(f fuego.ContextNoBody) (*shared_types.Response, error) {
13-
containers, err := c.dockerService.ListAllContainers()
17+
func (c *ContainerController) ListContainers(fuegoCtx fuego.ContextNoBody) (*shared_types.Response, error) {
18+
// normalize query params
19+
params := parseContainerListParams(fuegoCtx.Request())
20+
21+
// Get pre-filtered summaries from Docker
22+
containers, err := c.dockerService.ListContainers(container.ListOptions{
23+
All: true,
24+
Filters: buildDockerFilters(params),
25+
})
1426
if err != nil {
1527
c.logger.Log(logger.Error, err.Error(), "")
1628
return nil, fuego.HTTPError{
1729
Err: err,
1830
Status: http.StatusInternalServerError,
1931
}
2032
}
33+
// Build summaries, then search/sort/paginate
34+
rows := summarizeContainers(containers)
35+
pageRows, totalCount := applySearchSortPaginate(rows, params)
2136

22-
var result []types.Container
23-
for _, container := range containers {
24-
containerInfo, err := c.dockerService.GetContainerById(container.ID)
25-
if err != nil {
26-
c.logger.Log(logger.Error, "Error inspecting container", container.ID)
27-
continue
28-
}
37+
result := c.appendContainerInfo(pageRows, containers)
2938

30-
containerData := types.Container{
31-
ID: container.ID,
32-
Name: "",
33-
Image: container.Image,
34-
Status: container.Status,
35-
State: container.State,
36-
Created: containerInfo.Created,
37-
Labels: container.Labels,
38-
Command: "",
39-
IPAddress: containerInfo.NetworkSettings.IPAddress,
40-
HostConfig: types.HostConfig{
41-
Memory: containerInfo.HostConfig.Memory,
42-
MemorySwap: containerInfo.HostConfig.MemorySwap,
43-
CPUShares: containerInfo.HostConfig.CPUShares,
44-
},
45-
}
39+
return &shared_types.Response{
40+
Status: "success",
41+
Message: "Containers fetched successfully",
42+
Data: map[string]interface{}{
43+
"containers": result,
44+
"total_count": totalCount,
45+
"page": params.Page,
46+
"page_size": params.PageSize,
47+
"sort_by": params.SortBy,
48+
"sort_order": params.SortOrder,
49+
"search": params.Search,
50+
"status": params.Status,
51+
"name": params.Name,
52+
"image": params.Image,
53+
},
54+
}, nil
55+
}
56+
57+
func parseContainerListParams(r *http.Request) containertypes.ContainerListParams {
58+
q := r.URL.Query()
59+
pageStr := q.Get("page")
60+
pageSizeStr := q.Get("page_size")
61+
sortBy := strings.ToLower(strings.TrimSpace(q.Get("sort_by")))
62+
sortOrder := strings.ToLower(strings.TrimSpace(q.Get("sort_order")))
63+
64+
if pageStr == "" {
65+
pageStr = "1"
66+
}
67+
if pageSizeStr == "" {
68+
pageSizeStr = "10"
69+
}
70+
if sortBy == "" {
71+
sortBy = "name"
72+
}
73+
if sortOrder == "" {
74+
sortOrder = "asc"
75+
}
76+
77+
page, _ := strconv.Atoi(pageStr)
78+
if page < 1 {
79+
page = 1
80+
}
81+
pageSize, _ := strconv.Atoi(pageSizeStr)
82+
if pageSize < 1 {
83+
pageSize = 10
84+
}
85+
86+
return containertypes.ContainerListParams{
87+
Page: page,
88+
PageSize: pageSize,
89+
Search: strings.TrimSpace(q.Get("search")),
90+
SortBy: sortBy,
91+
SortOrder: sortOrder,
92+
Status: strings.TrimSpace(q.Get("status")),
93+
Name: strings.TrimSpace(q.Get("name")),
94+
Image: strings.TrimSpace(q.Get("image")),
95+
}
96+
}
97+
98+
func buildDockerFilters(p containertypes.ContainerListParams) filters.Args {
99+
f := filters.NewArgs()
100+
if p.Status != "" {
101+
f.Add("status", p.Status)
102+
}
103+
if p.Name != "" {
104+
f.Add("name", p.Name)
105+
}
106+
if p.Image != "" {
107+
f.Add("ancestor", p.Image)
108+
}
109+
return f
110+
}
46111

47-
if container.Names != nil && len(container.Names) > 0 {
48-
name := container.Names[0]
49-
if len(name) > 1 {
50-
containerData.Name = name[1:]
112+
func summarizeContainers(summaries []container.Summary) []containertypes.ContainerListRow {
113+
rows := make([]containertypes.ContainerListRow, 0, len(summaries))
114+
for _, csum := range summaries {
115+
name := ""
116+
if len(csum.Names) > 0 {
117+
n := csum.Names[0]
118+
if len(n) > 1 {
119+
name = n[1:]
51120
} else {
52-
containerData.Name = name
121+
name = n
53122
}
54123
}
124+
rows = append(rows, containertypes.ContainerListRow{
125+
ID: csum.ID,
126+
Name: name,
127+
Image: csum.Image,
128+
Status: csum.Status,
129+
State: csum.State,
130+
Created: csum.Created,
131+
Labels: csum.Labels,
132+
})
133+
}
134+
return rows
135+
}
55136

56-
if containerInfo.Config != nil && containerInfo.Config.Cmd != nil && len(containerInfo.Config.Cmd) > 0 {
57-
containerData.Command = containerInfo.Config.Cmd[0]
137+
func applySearchSortPaginate(rows []containertypes.ContainerListRow, p containertypes.ContainerListParams) ([]containertypes.ContainerListRow, int) {
138+
if p.Search != "" {
139+
lower := strings.ToLower(p.Search)
140+
filtered := make([]containertypes.ContainerListRow, 0, len(rows))
141+
for _, r := range rows {
142+
if strings.Contains(strings.ToLower(r.Name), lower) ||
143+
strings.Contains(strings.ToLower(r.Image), lower) ||
144+
strings.Contains(strings.ToLower(r.Status), lower) {
145+
filtered = append(filtered, r)
146+
}
58147
}
148+
rows = filtered
149+
}
59150

60-
for _, port := range container.Ports {
61-
containerData.Ports = append(containerData.Ports, types.Port{
62-
PrivatePort: int(port.PrivatePort),
63-
PublicPort: int(port.PublicPort),
64-
Type: port.Type,
65-
})
151+
sort.SliceStable(rows, func(i, j int) bool {
152+
switch p.SortBy {
153+
case "status":
154+
a := strings.ToLower(rows[i].Status)
155+
b := strings.ToLower(rows[j].Status)
156+
if p.SortOrder == "desc" {
157+
return a > b
158+
}
159+
return a < b
160+
case "name":
161+
a := strings.ToLower(rows[i].Name)
162+
b := strings.ToLower(rows[j].Name)
163+
if p.SortOrder == "desc" {
164+
return a > b
165+
}
166+
return a < b
167+
default:
168+
ai := rows[i].Created
169+
aj := rows[j].Created
170+
if p.SortOrder == "desc" {
171+
return ai > aj
172+
}
173+
return ai < aj
66174
}
175+
})
67176

68-
for _, mount := range containerInfo.Mounts {
69-
containerData.Mounts = append(containerData.Mounts, types.Mount{
70-
Type: string(mount.Type),
71-
Source: mount.Source,
72-
Destination: mount.Destination,
73-
Mode: mount.Mode,
177+
totalCount := len(rows)
178+
start := (p.Page - 1) * p.PageSize
179+
if start > totalCount {
180+
start = totalCount
181+
}
182+
end := start + p.PageSize
183+
if end > totalCount {
184+
end = totalCount
185+
}
186+
return rows[start:end], totalCount
187+
}
188+
189+
func (c *ContainerController) appendContainerInfo(pageRows []containertypes.ContainerListRow, summaries []container.Summary) []containertypes.Container {
190+
result := make([]containertypes.Container, 0, len(pageRows))
191+
for _, r := range pageRows {
192+
info, err := c.dockerService.GetContainerById(r.ID)
193+
if err != nil {
194+
c.logger.Log(logger.Error, "Error inspecting container", r.ID)
195+
continue
196+
}
197+
cd := containertypes.Container{
198+
ID: r.ID,
199+
Name: r.Name,
200+
Image: r.Image,
201+
Status: r.Status,
202+
State: r.State,
203+
Created: info.Created,
204+
Labels: r.Labels,
205+
Command: "",
206+
IPAddress: info.NetworkSettings.IPAddress,
207+
HostConfig: containertypes.HostConfig{
208+
Memory: info.HostConfig.Memory,
209+
MemorySwap: info.HostConfig.MemorySwap,
210+
CPUShares: info.HostConfig.CPUShares,
211+
},
212+
}
213+
if info.Config != nil && info.Config.Cmd != nil && len(info.Config.Cmd) > 0 {
214+
cd.Command = info.Config.Cmd[0]
215+
}
216+
for _, s := range summaries {
217+
if s.ID == r.ID {
218+
for _, p := range s.Ports {
219+
cd.Ports = append(cd.Ports, containertypes.Port{
220+
PrivatePort: int(p.PrivatePort),
221+
PublicPort: int(p.PublicPort),
222+
Type: p.Type,
223+
})
224+
}
225+
break
226+
}
227+
}
228+
for _, m := range info.Mounts {
229+
cd.Mounts = append(cd.Mounts, containertypes.Mount{
230+
Type: string(m.Type),
231+
Source: m.Source,
232+
Destination: m.Destination,
233+
Mode: m.Mode,
74234
})
75235
}
76-
77-
for name, network := range containerInfo.NetworkSettings.Networks {
78-
containerData.Networks = append(containerData.Networks, types.Network{
236+
for name, network := range info.NetworkSettings.Networks {
237+
cd.Networks = append(cd.Networks, containertypes.Network{
79238
Name: name,
80239
IPAddress: network.IPAddress,
81240
Gateway: network.Gateway,
82241
MacAddress: network.MacAddress,
83242
Aliases: network.Aliases,
84243
})
85244
}
86-
87-
result = append(result, containerData)
245+
result = append(result, cd)
88246
}
89-
90-
return &shared_types.Response{
91-
Status: "success",
92-
Message: "Containers fetched successfully",
93-
Data: result,
94-
}, nil
247+
return result
95248
}

api/internal/features/container/types/container_types.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,24 @@ type VolumeCreateRequest struct {
108108
type VolumeListOptions struct {
109109
Filters map[string][]string `json:"filters"`
110110
}
111+
112+
type ContainerListParams struct {
113+
Page int `json:"page"`
114+
PageSize int `json:"page_size"`
115+
Search string `json:"search"`
116+
SortBy string `json:"sort_by"`
117+
SortOrder string `json:"sort_order"`
118+
Status string `json:"status"`
119+
Name string `json:"name"`
120+
Image string `json:"image"`
121+
}
122+
123+
type ContainerListRow struct {
124+
ID string `json:"id"`
125+
Name string `json:"name"`
126+
Image string `json:"image"`
127+
Status string `json:"status"`
128+
State string `json:"state"`
129+
Created int64 `json:"created"`
130+
Labels map[string]string `json:"labels"`
131+
}

api/internal/features/deploy/docker/init.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ type DockerService struct {
2323
}
2424

2525
type DockerRepository interface {
26-
ListAllContainers() ([]container.Summary, error)
27-
ListAllImages(opts image.ListOptions) []image.Summary
26+
ListAllContainers() ([]container.Summary, error)
27+
ListContainers(opts container.ListOptions) ([]container.Summary, error)
28+
ListAllImages(opts image.ListOptions) []image.Summary
2829

2930
StopContainer(containerID string, opts container.StopOptions) error
3031
RemoveContainer(containerID string, opts container.RemoveOptions) error
@@ -98,16 +99,26 @@ func NewDockerClient() *client.Client {
9899
// ListAllContainers returns a list of all containers running on the host, along with their
99100
// IDs, names, and statuses. The returned list is sorted by container ID in ascending order.
100101
//
101-
// If an error occurs while listing the containers, it panics with the error.
102+
// If an error occurs while listing the containers, it returns the error (no panic).
102103
func (s *DockerService) ListAllContainers() ([]container.Summary, error) {
103-
containers, err := s.Cli.ContainerList(s.Ctx, container.ListOptions{
104-
All: true,
105-
})
106-
if err != nil {
107-
panic(err)
108-
}
104+
containers, err := s.Cli.ContainerList(s.Ctx, container.ListOptions{
105+
All: true,
106+
})
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
return containers, nil
112+
}
109113

110-
return containers, nil
114+
// ListContainers returns containers using the provided docker list options
115+
// (including native filters like name/status/ancestor and optional limits).
116+
func (s *DockerService) ListContainers(opts container.ListOptions) ([]container.Summary, error) {
117+
containers, err := s.Cli.ContainerList(s.Ctx, opts)
118+
if err != nil {
119+
return nil, err
120+
}
121+
return containers, nil
111122
}
112123

113124
// StopContainer stops the container with the given ID. If the container does not exist,

0 commit comments

Comments
 (0)