1package admin
2
3import (
4 "encoding/json"
5 "errors"
6 "net/http"
7
8 "github.com/bnema/zerowrap"
9
10 "github.com/bnema/gordon/internal/adapters/dto"
11 "github.com/bnema/gordon/internal/domain"
12)
13
14// handleListVolumes handles GET /admin/volumes endpoint.
15func (h *Handler) handleListVolumes(w http.ResponseWriter, r *http.Request) {
16 if r.Method != http.MethodGet {
17 h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
18 return
19 }
20
21 ctx := r.Context()
22
23 if !HasAccess(ctx, domain.AdminResourceVolumes, domain.AdminActionRead) {
24 h.sendError(w, http.StatusForbidden, "insufficient permissions for volumes:read")
25 return
26 }
27
28 if h.volumeSvc == nil {
29 h.sendError(w, http.StatusServiceUnavailable, "volume service not available")
30 return
31 }
32
33 vols, err := h.volumeSvc.ListVolumes(ctx)
34 if err != nil {
35 log := zerowrap.FromCtx(ctx)
36 log.Error().Err(err).Msg("failed to list volumes")
37 h.sendError(w, http.StatusInternalServerError, "failed to list volumes")
38 return
39 }
40
41 result := make([]dto.Volume, len(vols))
42 for i, v := range vols {
43 result[i] = dto.Volume{
44 Name: v.Name,
45 Driver: v.Driver,
46 MountPoint: v.MountPoint,
47 Size: v.Size,
48 CreatedAt: v.CreatedAt,
49 InUse: v.InUse,
50 Containers: v.Containers,
51 Labels: v.Labels,
52 }
53 }
54
55 h.sendJSON(w, http.StatusOK, result)
56}
57
58// handlePruneVolumes handles POST /admin/volumes/prune endpoint.
59func (h *Handler) handlePruneVolumes(w http.ResponseWriter, r *http.Request) {
60 if r.Method != http.MethodPost {
61 h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
62 return
63 }
64
65 ctx := r.Context()
66
67 if !HasAccess(ctx, domain.AdminResourceVolumes, domain.AdminActionWrite) {
68 h.sendError(w, http.StatusForbidden, "insufficient permissions for volumes:write")
69 return
70 }
71
72 if h.volumeSvc == nil {
73 h.sendError(w, http.StatusServiceUnavailable, "volume service not available")
74 return
75 }
76
77 var req dto.VolumePruneRequest
78 r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize)
79 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
80 h.sendError(w, http.StatusBadRequest, "invalid request body")
81 return
82 }
83
84 report, removed, err := h.volumeSvc.PruneVolumes(ctx, req.DryRun)
85 if err != nil {
86 log := zerowrap.FromCtx(ctx)
87 log.Error().Err(err).Msg("failed to prune volumes")
88 if errors.Is(err, domain.ErrPruneDisabled) {
89 h.sendJSON(w, http.StatusConflict, dto.AppError{Error: "prune-disabled", Message: err.Error()})
90 return
91 }
92 h.sendError(w, http.StatusInternalServerError, "failed to prune volumes")
93 return
94 }
95
96 vols := make([]dto.Volume, len(removed))
97 for i, v := range removed {
98 vols[i] = dto.Volume{
99 Name: v.Name,
100 Size: v.Size,
101 }
102 }
103
104 h.sendJSON(w, http.StatusOK, dto.VolumePruneResponse{
105 VolumesRemoved: report.VolumesRemoved,
106 SpaceReclaimed: report.SpaceReclaimed,
107 Volumes: vols,
108 Plan: toPruneSummary(report.Plan),
109 })
110}