Skip to content

Commit 8de6b2f

Browse files
committed
fix: merge conflict
1 parent dfd3dc1 commit 8de6b2f

410 files changed

Lines changed: 21473 additions & 16470 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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-12-16T14:18:54.683478+05:30",
6+
"release_date": "2026-01-11T03:07:25.946061+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: 153 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,53 @@ func (c *ContainerController) ListContainers(fuegoCtx fuego.ContextNoBody) (*con
2929
Status: http.StatusInternalServerError,
3030
}
3131
}
32-
// Build summaries, then search/sort/paginate
32+
// Build summaries, then search/sort
3333
rows := summarizeContainers(containers)
34-
pageRows, totalCount := applySearchSortPaginate(rows, params)
34+
filteredRows := applySearchFilter(rows, params)
35+
sortedRows := applySort(filteredRows, params)
3536

36-
result := c.appendContainerInfo(pageRows, containers)
37+
// Group containers by application ID
38+
groups, ungrouped := groupContainersByApplication(sortedRows, containers, c.dockerService)
39+
40+
// Sort groups by application name
41+
sort.SliceStable(groups, func(i, j int) bool {
42+
if params.SortOrder == "desc" {
43+
return groups[i].ApplicationName > groups[j].ApplicationName
44+
}
45+
return groups[i].ApplicationName < groups[j].ApplicationName
46+
})
47+
48+
// Paginate groups
49+
totalGroupCount := len(groups)
50+
start := (params.Page - 1) * params.PageSize
51+
if start > totalGroupCount {
52+
start = totalGroupCount
53+
}
54+
end := start + params.PageSize
55+
if end > totalGroupCount {
56+
end = totalGroupCount
57+
}
58+
paginatedGroups := groups[start:end]
59+
60+
// Calculate total container count
61+
totalContainerCount := 0
62+
for _, group := range groups {
63+
totalContainerCount += len(group.Containers)
64+
}
65+
totalContainerCount += len(ungrouped)
66+
67+
// Include ungrouped containers on every page
68+
// This ensures they're always visible regardless of pagination
69+
paginatedUngrouped := ungrouped
3770

3871
return &containertypes.ListContainersResponse{
3972
Status: "success",
4073
Message: "Containers fetched successfully",
4174
Data: containertypes.ListContainersResponseData{
42-
Containers: result,
43-
TotalCount: totalCount,
75+
Groups: paginatedGroups,
76+
Ungrouped: paginatedUngrouped,
77+
TotalCount: totalContainerCount,
78+
GroupCount: totalGroupCount,
4479
Page: params.Page,
4580
PageSize: params.PageSize,
4681
SortBy: params.SortBy,
@@ -133,7 +168,7 @@ func summarizeContainers(summaries []container.Summary) []containertypes.Contain
133168
return rows
134169
}
135170

136-
func applySearchSortPaginate(rows []containertypes.ContainerListRow, p containertypes.ContainerListParams) ([]containertypes.ContainerListRow, int) {
171+
func applySearchFilter(rows []containertypes.ContainerListRow, p containertypes.ContainerListParams) []containertypes.ContainerListRow {
137172
if p.Search != "" {
138173
lower := strings.ToLower(p.Search)
139174
filtered := make([]containertypes.ContainerListRow, 0, len(rows))
@@ -144,9 +179,12 @@ func applySearchSortPaginate(rows []containertypes.ContainerListRow, p container
144179
filtered = append(filtered, r)
145180
}
146181
}
147-
rows = filtered
182+
return filtered
148183
}
184+
return rows
185+
}
149186

187+
func applySort(rows []containertypes.ContainerListRow, p containertypes.ContainerListParams) []containertypes.ContainerListRow {
150188
sort.SliceStable(rows, func(i, j int) bool {
151189
switch p.SortBy {
152190
case "status":
@@ -172,17 +210,117 @@ func applySearchSortPaginate(rows []containertypes.ContainerListRow, p container
172210
return ai < aj
173211
}
174212
})
213+
return rows
214+
}
215+
216+
func groupContainersByApplication(
217+
rows []containertypes.ContainerListRow,
218+
summaries []container.Summary,
219+
dockerService interface {
220+
GetContainerById(id string) (container.InspectResponse, error)
221+
},
222+
) ([]containertypes.ContainerGroup, []containertypes.Container) {
223+
groupsMap := make(map[string]*containertypes.ContainerGroup)
224+
ungrouped := make([]containertypes.Container, 0)
225+
226+
// Create a map of summaries by ID for quick lookup
227+
summaryMap := make(map[string]container.Summary)
228+
for _, s := range summaries {
229+
summaryMap[s.ID] = s
230+
}
231+
232+
for _, row := range rows {
233+
applicationID := ""
234+
applicationName := "Unknown Application"
235+
if row.Labels != nil {
236+
if id, ok := row.Labels["com.application.id"]; ok {
237+
applicationID = id
238+
}
239+
if name, ok := row.Labels["com.application.name"]; ok {
240+
applicationName = name
241+
}
242+
}
243+
244+
// Get full container info
245+
info, err := dockerService.GetContainerById(row.ID)
246+
if err != nil {
247+
continue
248+
}
249+
250+
containerData := containertypes.Container{
251+
ID: row.ID,
252+
Name: row.Name,
253+
Image: row.Image,
254+
Status: row.Status,
255+
State: row.State,
256+
Created: info.Created,
257+
Labels: row.Labels,
258+
Command: "",
259+
IPAddress: info.NetworkSettings.IPAddress,
260+
HostConfig: containertypes.HostConfig{
261+
Memory: info.HostConfig.Memory,
262+
MemorySwap: info.HostConfig.MemorySwap,
263+
CPUShares: info.HostConfig.CPUShares,
264+
},
265+
}
266+
267+
if info.Config != nil && info.Config.Cmd != nil && len(info.Config.Cmd) > 0 {
268+
containerData.Command = info.Config.Cmd[0]
269+
}
270+
271+
// Add ports from summary
272+
if s, ok := summaryMap[row.ID]; ok {
273+
for _, p := range s.Ports {
274+
containerData.Ports = append(containerData.Ports, containertypes.Port{
275+
PrivatePort: int(p.PrivatePort),
276+
PublicPort: int(p.PublicPort),
277+
Type: p.Type,
278+
})
279+
}
280+
}
175281

176-
totalCount := len(rows)
177-
start := (p.Page - 1) * p.PageSize
178-
if start > totalCount {
179-
start = totalCount
282+
// Add mounts
283+
for _, m := range info.Mounts {
284+
containerData.Mounts = append(containerData.Mounts, containertypes.Mount{
285+
Type: string(m.Type),
286+
Source: m.Source,
287+
Destination: m.Destination,
288+
Mode: m.Mode,
289+
})
290+
}
291+
292+
// Add networks
293+
for name, network := range info.NetworkSettings.Networks {
294+
containerData.Networks = append(containerData.Networks, containertypes.Network{
295+
Name: name,
296+
IPAddress: network.IPAddress,
297+
Gateway: network.Gateway,
298+
MacAddress: network.MacAddress,
299+
Aliases: network.Aliases,
300+
})
301+
}
302+
303+
if applicationID != "" {
304+
if _, exists := groupsMap[applicationID]; !exists {
305+
groupsMap[applicationID] = &containertypes.ContainerGroup{
306+
ApplicationID: applicationID,
307+
ApplicationName: applicationName,
308+
Containers: make([]containertypes.Container, 0),
309+
}
310+
}
311+
groupsMap[applicationID].Containers = append(groupsMap[applicationID].Containers, containerData)
312+
} else {
313+
ungrouped = append(ungrouped, containerData)
314+
}
180315
}
181-
end := start + p.PageSize
182-
if end > totalCount {
183-
end = totalCount
316+
317+
// Convert map to slice
318+
groups := make([]containertypes.ContainerGroup, 0, len(groupsMap))
319+
for _, group := range groupsMap {
320+
groups = append(groups, *group)
184321
}
185-
return rows[start:end], totalCount
322+
323+
return groups, ungrouped
186324
}
187325

188326
func (c *ContainerController) appendContainerInfo(pageRows []containertypes.ContainerListRow, summaries []container.Summary) []containertypes.Container {

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,28 @@ type ContainerListRow struct {
130130
Labels map[string]string `json:"labels"`
131131
}
132132

133+
// ContainerGroup represents a group of containers belonging to the same application
134+
type ContainerGroup struct {
135+
ApplicationID string `json:"application_id"`
136+
ApplicationName string `json:"application_name"`
137+
Containers []Container `json:"containers"`
138+
}
139+
133140
// ListContainersResponseData contains the data for list containers response
134141
type ListContainersResponseData struct {
135-
Containers []Container `json:"containers"`
136-
TotalCount int `json:"total_count"`
137-
Page int `json:"page"`
138-
PageSize int `json:"page_size"`
139-
SortBy string `json:"sort_by"`
140-
SortOrder string `json:"sort_order"`
141-
Search string `json:"search"`
142-
Status string `json:"status"`
143-
Name string `json:"name"`
144-
Image string `json:"image"`
142+
Containers []Container `json:"containers"` // Deprecated: use Groups instead. Kept for backward compatibility
143+
Groups []ContainerGroup `json:"groups,omitempty"`
144+
Ungrouped []Container `json:"ungrouped,omitempty"`
145+
TotalCount int `json:"total_count"` // Total number of containers (not groups)
146+
GroupCount int `json:"group_count"` // Total number of groups
147+
Page int `json:"page"`
148+
PageSize int `json:"page_size"`
149+
SortBy string `json:"sort_by"`
150+
SortOrder string `json:"sort_order"`
151+
Search string `json:"search"`
152+
Status string `json:"status"`
153+
Name string `json:"name"`
154+
Image string `json:"image"`
145155
}
146156

147157
// ListContainersResponse is the typed response for listing containers

api/internal/features/extension/loader/loader.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ func (l *ExtensionLoader) LoadExtensionsFromDirectory(ctx context.Context, dirPa
4747
}
4848
}
4949

50-
// Remove extensions from database that are no longer in templates directory
51-
if err := l.removeDeletedExtensions(ctx, foundExtensionIDs); err != nil {
52-
log.Printf("Warning: Failed to remove deleted extensions: %v", err)
53-
// Don't return error here as the main loading succeeded
54-
}
50+
// // Remove extensions from database that are no longer in templates directory
51+
// if err := l.removeDeletedExtensions(ctx, foundExtensionIDs); err != nil {
52+
// log.Printf("Warning: Failed to remove deleted extensions: %v", err)
53+
// // Don't return error here as the main loading succeeded
54+
// }
5555

5656
return nil
5757
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package controller
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/go-fuego/fuego"
7+
"github.com/google/uuid"
8+
"github.com/raghavyuva/nixopus-api/internal/features/healthcheck/types"
9+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
10+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
11+
"github.com/raghavyuva/nixopus-api/internal/utils"
12+
)
13+
14+
func (c *HealthCheckController) CreateHealthCheck(f fuego.ContextWithBody[types.CreateHealthCheckRequest]) (*shared_types.Response, error) {
15+
w, r := f.Response(), f.Request()
16+
user := utils.GetUser(w, r)
17+
18+
if user == nil {
19+
return nil, fuego.HTTPError{Status: http.StatusUnauthorized}
20+
}
21+
22+
orgID := utils.GetOrganizationID(r)
23+
if orgID == (uuid.UUID{}) {
24+
return nil, fuego.HTTPError{Status: http.StatusBadRequest, Err: types.ErrInvalidApplicationID}
25+
}
26+
27+
body, err := f.Body()
28+
if err != nil {
29+
c.logger.Log(logger.Error, err.Error(), "")
30+
return nil, fuego.HTTPError{Err: err, Status: http.StatusBadRequest}
31+
}
32+
33+
if err := c.validator.ValidateRequest(&body); err != nil {
34+
c.logger.Log(logger.Error, err.Error(), "")
35+
statusCode, mappedErr := mapHealthCheckError(err)
36+
return &shared_types.Response{
37+
Status: "error",
38+
Error: mappedErr.Error(),
39+
}, fuego.HTTPError{Status: statusCode}
40+
}
41+
42+
healthCheck, err := c.service.CreateHealthCheck(user.ID, orgID, &body)
43+
if err != nil {
44+
c.logger.Log(logger.Error, err.Error(), "")
45+
statusCode, mappedErr := mapHealthCheckError(err)
46+
return &shared_types.Response{
47+
Status: "error",
48+
Error: mappedErr.Error(),
49+
}, fuego.HTTPError{Status: statusCode}
50+
}
51+
52+
return &shared_types.Response{
53+
Status: "success",
54+
Message: "Health check created successfully",
55+
Data: healthCheck,
56+
}, nil
57+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package controller
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/go-fuego/fuego"
7+
"github.com/google/uuid"
8+
"github.com/raghavyuva/nixopus-api/internal/features/healthcheck/types"
9+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
10+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
11+
"github.com/raghavyuva/nixopus-api/internal/utils"
12+
)
13+
14+
func (c *HealthCheckController) DeleteHealthCheck(f fuego.ContextNoBody) (*shared_types.Response, error) {
15+
w, r := f.Response(), f.Request()
16+
user := utils.GetUser(w, r)
17+
18+
if user == nil {
19+
return nil, fuego.HTTPError{Status: http.StatusUnauthorized}
20+
}
21+
22+
orgID := utils.GetOrganizationID(r)
23+
if orgID == (uuid.UUID{}) {
24+
return nil, fuego.HTTPError{Status: http.StatusBadRequest}
25+
}
26+
27+
q := r.URL.Query()
28+
applicationID := q.Get("application_id")
29+
if applicationID == "" {
30+
return nil, fuego.HTTPError{Status: http.StatusBadRequest, Err: types.ErrInvalidApplicationID}
31+
}
32+
33+
if err := c.service.DeleteHealthCheck(applicationID, orgID); err != nil {
34+
c.logger.Log(logger.Error, err.Error(), "")
35+
statusCode, mappedErr := mapHealthCheckError(err)
36+
return &shared_types.Response{
37+
Status: "error",
38+
Error: mappedErr.Error(),
39+
}, fuego.HTTPError{Status: statusCode}
40+
}
41+
42+
return &shared_types.Response{
43+
Status: "success",
44+
Message: "Health check deleted successfully",
45+
Data: nil,
46+
}, nil
47+
}

0 commit comments

Comments
 (0)