Skip to content
Merged
2 changes: 1 addition & 1 deletion api/api/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"version": "v1",
"status": "active",
"release_date": "2025-08-28T18:36:20.486085+05:30",
"release_date": "2025-09-03T02:32:19.06053+05:30",
"end_of_life": "0001-01-01T00:00:00Z",
"changes": [
"Initial API version"
Expand Down
265 changes: 209 additions & 56 deletions api/internal/features/container/controller/list_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,247 @@ package controller

import (
"net/http"
"sort"
"strconv"
"strings"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/go-fuego/fuego"
"github.com/raghavyuva/nixopus-api/internal/features/container/types"
containertypes "github.com/raghavyuva/nixopus-api/internal/features/container/types"
"github.com/raghavyuva/nixopus-api/internal/features/logger"
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
)

func (c *ContainerController) ListContainers(f fuego.ContextNoBody) (*shared_types.Response, error) {
containers, err := c.dockerService.ListAllContainers()
func (c *ContainerController) ListContainers(fuegoCtx fuego.ContextNoBody) (*shared_types.Response, error) {
// normalize query params
params := parseContainerListParams(fuegoCtx.Request())

// Get pre-filtered summaries from Docker
containers, err := c.dockerService.ListContainers(container.ListOptions{
All: true,
Filters: buildDockerFilters(params),
})
if err != nil {
c.logger.Log(logger.Error, err.Error(), "")
return nil, fuego.HTTPError{
Err: err,
Status: http.StatusInternalServerError,
}
}
// Build summaries, then search/sort/paginate
rows := summarizeContainers(containers)
pageRows, totalCount := applySearchSortPaginate(rows, params)

var result []types.Container
for _, container := range containers {
containerInfo, err := c.dockerService.GetContainerById(container.ID)
if err != nil {
c.logger.Log(logger.Error, "Error inspecting container", container.ID)
continue
}
result := c.appendContainerInfo(pageRows, containers)

containerData := types.Container{
ID: container.ID,
Name: "",
Image: container.Image,
Status: container.Status,
State: container.State,
Created: containerInfo.Created,
Labels: container.Labels,
Command: "",
IPAddress: containerInfo.NetworkSettings.IPAddress,
HostConfig: types.HostConfig{
Memory: containerInfo.HostConfig.Memory,
MemorySwap: containerInfo.HostConfig.MemorySwap,
CPUShares: containerInfo.HostConfig.CPUShares,
},
}
return &shared_types.Response{
Status: "success",
Message: "Containers fetched successfully",
Data: map[string]interface{}{
"containers": result,
"total_count": totalCount,
"page": params.Page,
"page_size": params.PageSize,
"sort_by": params.SortBy,
"sort_order": params.SortOrder,
"search": params.Search,
"status": params.Status,
"name": params.Name,
"image": params.Image,
},
}, nil
}

func parseContainerListParams(r *http.Request) containertypes.ContainerListParams {
q := r.URL.Query()
pageStr := q.Get("page")
pageSizeStr := q.Get("page_size")
sortBy := strings.ToLower(strings.TrimSpace(q.Get("sort_by")))
sortOrder := strings.ToLower(strings.TrimSpace(q.Get("sort_order")))

if pageStr == "" {
pageStr = "1"
}
if pageSizeStr == "" {
pageSizeStr = "10"
}
if sortBy == "" {
sortBy = "name"
}
if sortOrder == "" {
sortOrder = "asc"
}

page, _ := strconv.Atoi(pageStr)
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(pageSizeStr)
if pageSize < 1 {
pageSize = 10
}

return containertypes.ContainerListParams{
Page: page,
PageSize: pageSize,
Search: strings.TrimSpace(q.Get("search")),
SortBy: sortBy,
SortOrder: sortOrder,
Status: strings.TrimSpace(q.Get("status")),
Name: strings.TrimSpace(q.Get("name")),
Image: strings.TrimSpace(q.Get("image")),
}
}

func buildDockerFilters(p containertypes.ContainerListParams) filters.Args {
f := filters.NewArgs()
if p.Status != "" {
f.Add("status", p.Status)
}
if p.Name != "" {
f.Add("name", p.Name)
}
if p.Image != "" {
f.Add("ancestor", p.Image)
}
return f
}

if container.Names != nil && len(container.Names) > 0 {
name := container.Names[0]
if len(name) > 1 {
containerData.Name = name[1:]
func summarizeContainers(summaries []container.Summary) []containertypes.ContainerListRow {
rows := make([]containertypes.ContainerListRow, 0, len(summaries))
for _, csum := range summaries {
name := ""
if len(csum.Names) > 0 {
n := csum.Names[0]
if len(n) > 1 {
name = n[1:]
} else {
containerData.Name = name
name = n
}
}
rows = append(rows, containertypes.ContainerListRow{
ID: csum.ID,
Name: name,
Image: csum.Image,
Status: csum.Status,
State: csum.State,
Created: csum.Created,
Labels: csum.Labels,
})
}
return rows
}

if containerInfo.Config != nil && containerInfo.Config.Cmd != nil && len(containerInfo.Config.Cmd) > 0 {
containerData.Command = containerInfo.Config.Cmd[0]
func applySearchSortPaginate(rows []containertypes.ContainerListRow, p containertypes.ContainerListParams) ([]containertypes.ContainerListRow, int) {
if p.Search != "" {
lower := strings.ToLower(p.Search)
filtered := make([]containertypes.ContainerListRow, 0, len(rows))
for _, r := range rows {
if strings.Contains(strings.ToLower(r.Name), lower) ||
strings.Contains(strings.ToLower(r.Image), lower) ||
strings.Contains(strings.ToLower(r.Status), lower) {
filtered = append(filtered, r)
}
}
rows = filtered
}

for _, port := range container.Ports {
containerData.Ports = append(containerData.Ports, types.Port{
PrivatePort: int(port.PrivatePort),
PublicPort: int(port.PublicPort),
Type: port.Type,
})
sort.SliceStable(rows, func(i, j int) bool {
switch p.SortBy {
case "status":
a := strings.ToLower(rows[i].Status)
b := strings.ToLower(rows[j].Status)
if p.SortOrder == "desc" {
return a > b
}
return a < b
case "name":
a := strings.ToLower(rows[i].Name)
b := strings.ToLower(rows[j].Name)
if p.SortOrder == "desc" {
return a > b
}
return a < b
default:
ai := rows[i].Created
aj := rows[j].Created
if p.SortOrder == "desc" {
return ai > aj
}
return ai < aj
}
})

for _, mount := range containerInfo.Mounts {
containerData.Mounts = append(containerData.Mounts, types.Mount{
Type: string(mount.Type),
Source: mount.Source,
Destination: mount.Destination,
Mode: mount.Mode,
totalCount := len(rows)
start := (p.Page - 1) * p.PageSize
if start > totalCount {
start = totalCount
}
end := start + p.PageSize
if end > totalCount {
end = totalCount
}
return rows[start:end], totalCount
}

func (c *ContainerController) appendContainerInfo(pageRows []containertypes.ContainerListRow, summaries []container.Summary) []containertypes.Container {
result := make([]containertypes.Container, 0, len(pageRows))
for _, r := range pageRows {
info, err := c.dockerService.GetContainerById(r.ID)
if err != nil {
c.logger.Log(logger.Error, "Error inspecting container", r.ID)
continue
}
cd := containertypes.Container{
ID: r.ID,
Name: r.Name,
Image: r.Image,
Status: r.Status,
State: r.State,
Created: info.Created,
Labels: r.Labels,
Command: "",
IPAddress: info.NetworkSettings.IPAddress,
HostConfig: containertypes.HostConfig{
Memory: info.HostConfig.Memory,
MemorySwap: info.HostConfig.MemorySwap,
CPUShares: info.HostConfig.CPUShares,
},
}
if info.Config != nil && info.Config.Cmd != nil && len(info.Config.Cmd) > 0 {
cd.Command = info.Config.Cmd[0]
}
for _, s := range summaries {
if s.ID == r.ID {
for _, p := range s.Ports {
cd.Ports = append(cd.Ports, containertypes.Port{
PrivatePort: int(p.PrivatePort),
PublicPort: int(p.PublicPort),
Type: p.Type,
})
}
break
}
}
for _, m := range info.Mounts {
cd.Mounts = append(cd.Mounts, containertypes.Mount{
Type: string(m.Type),
Source: m.Source,
Destination: m.Destination,
Mode: m.Mode,
})
}

for name, network := range containerInfo.NetworkSettings.Networks {
containerData.Networks = append(containerData.Networks, types.Network{
for name, network := range info.NetworkSettings.Networks {
cd.Networks = append(cd.Networks, containertypes.Network{
Name: name,
IPAddress: network.IPAddress,
Gateway: network.Gateway,
MacAddress: network.MacAddress,
Aliases: network.Aliases,
})
}

result = append(result, containerData)
result = append(result, cd)
}

return &shared_types.Response{
Status: "success",
Message: "Containers fetched successfully",
Data: result,
}, nil
return result
}
21 changes: 21 additions & 0 deletions api/internal/features/container/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,24 @@ type VolumeCreateRequest struct {
type VolumeListOptions struct {
Filters map[string][]string `json:"filters"`
}

type ContainerListParams struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Search string `json:"search"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
Status string `json:"status"`
Name string `json:"name"`
Image string `json:"image"`
}

type ContainerListRow struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Status string `json:"status"`
State string `json:"state"`
Created int64 `json:"created"`
Labels map[string]string `json:"labels"`
}
31 changes: 21 additions & 10 deletions api/internal/features/deploy/docker/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ type DockerService struct {
}

type DockerRepository interface {
ListAllContainers() ([]container.Summary, error)
ListAllImages(opts image.ListOptions) []image.Summary
ListAllContainers() ([]container.Summary, error)
ListContainers(opts container.ListOptions) ([]container.Summary, error)
ListAllImages(opts image.ListOptions) []image.Summary

StopContainer(containerID string, opts container.StopOptions) error
RemoveContainer(containerID string, opts container.RemoveOptions) error
Expand Down Expand Up @@ -98,16 +99,26 @@ func NewDockerClient() *client.Client {
// ListAllContainers returns a list of all containers running on the host, along with their
// IDs, names, and statuses. The returned list is sorted by container ID in ascending order.
//
// If an error occurs while listing the containers, it panics with the error.
// If an error occurs while listing the containers, it returns the error (no panic).
func (s *DockerService) ListAllContainers() ([]container.Summary, error) {
containers, err := s.Cli.ContainerList(s.Ctx, container.ListOptions{
All: true,
})
if err != nil {
panic(err)
}
containers, err := s.Cli.ContainerList(s.Ctx, container.ListOptions{
All: true,
})
if err != nil {
return nil, err
}

return containers, nil
}

return containers, nil
// ListContainers returns containers using the provided docker list options
// (including native filters like name/status/ancestor and optional limits).
func (s *DockerService) ListContainers(opts container.ListOptions) ([]container.Summary, error) {
containers, err := s.Cli.ContainerList(s.Ctx, opts)
if err != nil {
return nil, err
}
return containers, nil
}

// StopContainer stops the container with the given ID. If the container does not exist,
Expand Down
Loading
Loading