Skip to content

Commit 9778a37

Browse files
committed
fix: move swarm stacks to be under projects
1 parent cd125a9 commit 9778a37

36 files changed

+3374
-874
lines changed

backend/internal/bootstrap/services_bootstrap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func initializeServices(ctx context.Context, db *database.DB, cfg *config.Config
8787
svcs.Volume = services.NewVolumeService(db, svcs.Docker, svcs.Event, svcs.Settings, svcs.Container, svcs.Image, cfg.BackupVolumeName)
8888
svcs.Network = services.NewNetworkService(db, svcs.Docker, svcs.Event)
8989
svcs.Port = services.NewPortService(svcs.Docker)
90-
svcs.Swarm = services.NewSwarmService(svcs.Docker, svcs.Settings, svcs.KV, svcs.ContainerRegistry, svcs.Environment)
90+
svcs.Swarm = services.NewSwarmService(db, svcs.Docker, svcs.Settings, svcs.KV, svcs.ContainerRegistry, svcs.Environment)
9191
svcs.Template = services.NewTemplateService(ctx, db, httpClient, svcs.Settings)
9292
svcs.Auth = services.NewAuthService(svcs.User, svcs.Settings, svcs.Event, cfg.JWTSecret, cfg)
9393
svcs.Oidc = services.NewOidcService(svcs.Auth, cfg, httpClient)

backend/internal/huma/handlers/swarm.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,64 @@ type DeleteSwarmStackOutput struct {
286286
Body base.ApiResponse[base.MessageResponse]
287287
}
288288

289+
type DownSwarmStackInput struct {
290+
EnvironmentID string `path:"id" doc:"Environment ID"`
291+
Name string `path:"name" doc:"Stack name"`
292+
}
293+
294+
type DownSwarmStackOutput struct {
295+
Body base.ApiResponse[base.MessageResponse]
296+
}
297+
298+
type ListSwarmStackProjectsInput struct {
299+
EnvironmentID string `path:"id" doc:"Environment ID"`
300+
Search string `query:"search" doc:"Search query"`
301+
Sort string `query:"sort" doc:"Column to sort by"`
302+
Order string `query:"order" default:"asc" doc:"Sort direction (asc or desc)"`
303+
Start int `query:"start" default:"0" doc:"Start index for pagination"`
304+
Limit int `query:"limit" default:"20" doc:"Number of items per page"`
305+
}
306+
307+
type ListSwarmStackProjectsOutput struct {
308+
Body SwarmPaginatedResponse[swarmtypes.StackProjectSummary]
309+
}
310+
311+
type GetSwarmStackProjectCountsInput struct {
312+
EnvironmentID string `path:"id" doc:"Environment ID"`
313+
}
314+
315+
type GetSwarmStackProjectCountsOutput struct {
316+
Body base.ApiResponse[swarmtypes.StackProjectCounts]
317+
}
318+
319+
type GetSwarmStackProjectInput struct {
320+
EnvironmentID string `path:"id" doc:"Environment ID"`
321+
Name string `path:"name" doc:"Stack name"`
322+
}
323+
324+
type GetSwarmStackProjectOutput struct {
325+
Body base.ApiResponse[swarmtypes.StackProjectDetails]
326+
}
327+
328+
type UpdateSwarmStackProjectInput struct {
329+
EnvironmentID string `path:"id" doc:"Environment ID"`
330+
Name string `path:"name" doc:"Stack name"`
331+
Body swarmtypes.StackSourceUpdateRequest
332+
}
333+
334+
type UpdateSwarmStackProjectOutput struct {
335+
Body base.ApiResponse[swarmtypes.StackProjectDetails]
336+
}
337+
338+
type DeleteSwarmStackProjectInput struct {
339+
EnvironmentID string `path:"id" doc:"Environment ID"`
340+
Name string `path:"name" doc:"Stack name"`
341+
}
342+
343+
type DeleteSwarmStackProjectOutput struct {
344+
Body base.ApiResponse[base.MessageResponse]
345+
}
346+
289347
type ListSwarmStackServicesInput struct {
290348
EnvironmentID string `path:"id" doc:"Environment ID"`
291349
Name string `path:"name" doc:"Stack name"`
@@ -542,9 +600,15 @@ func RegisterSwarm(api huma.API, swarmSvc *services.SwarmService, environmentSvc
542600
huma.Register(api, huma.Operation{OperationID: "list-swarm-stacks", Method: http.MethodGet, Path: "/environments/{id}/swarm/stacks", Summary: "List swarm stacks", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.ListStacks)
543601
huma.Register(api, huma.Operation{OperationID: "deploy-swarm-stack", Method: http.MethodPost, Path: "/environments/{id}/swarm/stacks", Summary: "Deploy swarm stack", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.DeployStack)
544602
huma.Register(api, huma.Operation{OperationID: "get-swarm-stack", Method: http.MethodGet, Path: "/environments/{id}/swarm/stacks/{name}", Summary: "Get swarm stack", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.GetStack)
603+
huma.Register(api, huma.Operation{OperationID: "down-swarm-stack", Method: http.MethodPost, Path: "/environments/{id}/swarm/stacks/{name}/down", Summary: "Bring down swarm stack runtime", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.DownStack)
545604
huma.Register(api, huma.Operation{OperationID: "get-swarm-stack-source", Method: http.MethodGet, Path: "/environments/{id}/swarm/stacks/{name}/source", Summary: "Get swarm stack source", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.GetStackSource)
546605
huma.Register(api, huma.Operation{OperationID: "update-swarm-stack-source", Method: http.MethodPut, Path: "/environments/{id}/swarm/stacks/{name}/source", Summary: "Update swarm stack source", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.UpdateStackSource)
547606
huma.Register(api, huma.Operation{OperationID: "delete-swarm-stack", Method: http.MethodDelete, Path: "/environments/{id}/swarm/stacks/{name}", Summary: "Delete swarm stack", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.DeleteStack)
607+
huma.Register(api, huma.Operation{OperationID: "list-swarm-stack-projects", Method: http.MethodGet, Path: "/environments/{id}/swarm/stackprojects", Summary: "List saved swarm stack projects", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.ListStackProjects)
608+
huma.Register(api, huma.Operation{OperationID: "get-swarm-stack-project-counts", Method: http.MethodGet, Path: "/environments/{id}/swarm/stackprojects/counts", Summary: "Get saved swarm stack project counts", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.GetStackProjectCounts)
609+
huma.Register(api, huma.Operation{OperationID: "get-swarm-stack-project", Method: http.MethodGet, Path: "/environments/{id}/swarm/stackprojects/{name}", Summary: "Get saved swarm stack project", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.GetStackProject)
610+
huma.Register(api, huma.Operation{OperationID: "update-swarm-stack-project", Method: http.MethodPut, Path: "/environments/{id}/swarm/stackprojects/{name}", Summary: "Create or update saved swarm stack project", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.UpdateStackProject)
611+
huma.Register(api, huma.Operation{OperationID: "delete-swarm-stack-project", Method: http.MethodDelete, Path: "/environments/{id}/swarm/stackprojects/{name}", Summary: "Delete saved swarm stack project", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.DeleteStackProject)
548612
huma.Register(api, huma.Operation{OperationID: "list-swarm-stack-services", Method: http.MethodGet, Path: "/environments/{id}/swarm/stacks/{name}/services", Summary: "List swarm stack services", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.ListStackServices)
549613
huma.Register(api, huma.Operation{OperationID: "list-swarm-stack-tasks", Method: http.MethodGet, Path: "/environments/{id}/swarm/stacks/{name}/tasks", Summary: "List swarm stack tasks", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.ListStackTasks)
550614
huma.Register(api, huma.Operation{OperationID: "render-swarm-stack-config", Method: http.MethodPost, Path: "/environments/{id}/swarm/stacks/config/render", Summary: "Render/validate swarm stack config", Tags: []string{"Swarm"}, Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}}}, h.RenderStackConfig)
@@ -1184,6 +1248,155 @@ func (h *SwarmHandler) DeployStack(ctx context.Context, input *DeploySwarmStackI
11841248
return &DeploySwarmStackOutput{Body: base.ApiResponse[swarmtypes.StackDeployResponse]{Success: true, Data: *resp}}, nil
11851249
}
11861250

1251+
func (h *SwarmHandler) DownStack(ctx context.Context, input *DownSwarmStackInput) (*DownSwarmStackOutput, error) {
1252+
if h.swarmService == nil {
1253+
return nil, huma.Error500InternalServerError("service not available")
1254+
}
1255+
if err := checkAdmin(ctx); err != nil {
1256+
return nil, err
1257+
}
1258+
1259+
if err := h.swarmService.DownStack(ctx, input.Name); err != nil {
1260+
return nil, mapSwarmServiceError(err, "Failed to bring down swarm stack")
1261+
}
1262+
1263+
h.auditSwarmMutation(ctx, input.EnvironmentID, "stack.down", "swarm_stack", input.Name, input.Name, map[string]any{"stack": input.Name})
1264+
1265+
return &DownSwarmStackOutput{
1266+
Body: base.ApiResponse[base.MessageResponse]{
1267+
Success: true,
1268+
Data: base.MessageResponse{Message: "Swarm stack brought down successfully"},
1269+
},
1270+
}, nil
1271+
}
1272+
1273+
func (h *SwarmHandler) ListStackProjects(
1274+
ctx context.Context,
1275+
input *ListSwarmStackProjectsInput,
1276+
) (*ListSwarmStackProjectsOutput, error) {
1277+
if h.swarmService == nil {
1278+
return nil, huma.Error500InternalServerError("service not available")
1279+
}
1280+
if err := checkAdmin(ctx); err != nil {
1281+
return nil, err
1282+
}
1283+
1284+
params := buildSwarmQueryParams(input.Search, input.Sort, input.Order, input.Start, input.Limit)
1285+
items, paginationResp, err := h.swarmService.ListStackProjectsPaginated(ctx, input.EnvironmentID, params)
1286+
if err != nil {
1287+
return nil, mapSwarmServiceError(err, "Failed to list saved swarm stack projects")
1288+
}
1289+
if items == nil {
1290+
items = []swarmtypes.StackProjectSummary{}
1291+
}
1292+
1293+
return &ListSwarmStackProjectsOutput{Body: toSwarmPaginatedResponse(items, paginationResp)}, nil
1294+
}
1295+
1296+
func (h *SwarmHandler) GetStackProjectCounts(
1297+
ctx context.Context,
1298+
input *GetSwarmStackProjectCountsInput,
1299+
) (*GetSwarmStackProjectCountsOutput, error) {
1300+
if h.swarmService == nil {
1301+
return nil, huma.Error500InternalServerError("service not available")
1302+
}
1303+
if err := checkAdmin(ctx); err != nil {
1304+
return nil, err
1305+
}
1306+
1307+
counts, err := h.swarmService.GetStackProjectStatusCounts(ctx, input.EnvironmentID)
1308+
if err != nil {
1309+
return nil, mapSwarmServiceError(err, "Failed to load saved swarm stack project counts")
1310+
}
1311+
1312+
return &GetSwarmStackProjectCountsOutput{
1313+
Body: base.ApiResponse[swarmtypes.StackProjectCounts]{Success: true, Data: counts},
1314+
}, nil
1315+
}
1316+
1317+
func (h *SwarmHandler) GetStackProject(
1318+
ctx context.Context,
1319+
input *GetSwarmStackProjectInput,
1320+
) (*GetSwarmStackProjectOutput, error) {
1321+
if h.swarmService == nil {
1322+
return nil, huma.Error500InternalServerError("service not available")
1323+
}
1324+
if err := checkAdmin(ctx); err != nil {
1325+
return nil, err
1326+
}
1327+
1328+
stackProject, err := h.swarmService.GetStackProject(ctx, input.EnvironmentID, input.Name)
1329+
if err != nil {
1330+
if errdefs.IsNotFound(err) {
1331+
return nil, huma.Error404NotFound("Saved swarm stack project not found")
1332+
}
1333+
return nil, mapSwarmServiceError(err, "Failed to load saved swarm stack project")
1334+
}
1335+
1336+
return &GetSwarmStackProjectOutput{
1337+
Body: base.ApiResponse[swarmtypes.StackProjectDetails]{Success: true, Data: *stackProject},
1338+
}, nil
1339+
}
1340+
1341+
func (h *SwarmHandler) UpdateStackProject(
1342+
ctx context.Context,
1343+
input *UpdateSwarmStackProjectInput,
1344+
) (*UpdateSwarmStackProjectOutput, error) {
1345+
if h.swarmService == nil {
1346+
return nil, huma.Error500InternalServerError("service not available")
1347+
}
1348+
if err := checkAdmin(ctx); err != nil {
1349+
return nil, err
1350+
}
1351+
1352+
stackProject, err := h.swarmService.UpsertStackProject(ctx, input.EnvironmentID, input.Name, input.Body)
1353+
if err != nil {
1354+
return nil, mapSwarmServiceError(err, "Failed to update saved swarm stack project")
1355+
}
1356+
1357+
h.auditSwarmMutation(
1358+
ctx,
1359+
input.EnvironmentID,
1360+
"stack.project.update",
1361+
"swarm_stack",
1362+
stackProject.Name,
1363+
input.Name,
1364+
map[string]any{"stack": stackProject.Name, "previousStack": input.Name},
1365+
)
1366+
1367+
return &UpdateSwarmStackProjectOutput{
1368+
Body: base.ApiResponse[swarmtypes.StackProjectDetails]{Success: true, Data: *stackProject},
1369+
}, nil
1370+
}
1371+
1372+
func (h *SwarmHandler) DeleteStackProject(
1373+
ctx context.Context,
1374+
input *DeleteSwarmStackProjectInput,
1375+
) (*DeleteSwarmStackProjectOutput, error) {
1376+
if h.swarmService == nil {
1377+
return nil, huma.Error500InternalServerError("service not available")
1378+
}
1379+
if err := checkAdmin(ctx); err != nil {
1380+
return nil, err
1381+
}
1382+
1383+
if err := h.swarmService.DeleteStackProject(ctx, input.EnvironmentID, input.Name); err != nil {
1384+
if errdefs.IsNotFound(err) {
1385+
return nil, huma.Error404NotFound("Saved swarm stack project not found")
1386+
}
1387+
return nil, mapSwarmServiceError(err, "Failed to delete saved swarm stack project")
1388+
}
1389+
1390+
h.auditSwarmMutation(ctx, input.EnvironmentID, "stack.project.delete", "swarm_stack", input.Name, input.Name, map[string]any{"stack": input.Name})
1391+
1392+
return &DeleteSwarmStackProjectOutput{
1393+
Body: base.ApiResponse[base.MessageResponse]{
1394+
Success: true,
1395+
Data: base.MessageResponse{Message: "Saved swarm stack project deleted successfully"},
1396+
},
1397+
}, nil
1398+
}
1399+
11871400
// GetStack returns detailed information for a specific swarm stack.
11881401
//
11891402
// It looks up the stack by name through the swarm service and maps missing
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package models
2+
3+
type SwarmStackProject struct {
4+
Name string `json:"name" sortable:"true"`
5+
DirName *string `json:"dir_name"`
6+
EnvironmentID string `json:"environment_id" gorm:"column:environment_id;index" sortable:"true"`
7+
Path string `json:"path" sortable:"true"`
8+
ServiceCount int `json:"service_count" gorm:"column:service_count" sortable:"true"`
9+
10+
BaseModel
11+
}
12+
13+
func (SwarmStackProject) TableName() string {
14+
return "swarm_stack_projects"
15+
}

0 commit comments

Comments
 (0)