Files
tiny-forge/internal/api/volumes.go
T
alexei.dolgolyov d4659146fc feat(docker-watcher): phase 13 - volumes & environment
Per-stage env var overrides with encryption for secrets.
Volume mounts with shared/isolated modes (isolated appends
/{stage}-{tag}/ to source path). Store CRUD, API endpoints,
and frontend editors for both. Env merge during deploy.
2026-03-27 23:28:59 +03:00

144 lines
3.6 KiB
Go

package api
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/alexei/docker-watcher/internal/store"
)
// volumeRequest is the expected JSON body for creating/updating a volume.
type volumeRequest struct {
Source string `json:"source"`
Target string `json:"target"`
Mode string `json:"mode"`
}
// listVolumes handles GET /api/projects/{id}/volumes.
func (s *Server) listVolumes(w http.ResponseWriter, r *http.Request) {
projectID := chi.URLParam(r, "id")
// Verify project exists.
if _, err := s.store.GetProjectByID(projectID); err != nil {
if errors.Is(err, store.ErrNotFound) {
respondNotFound(w, "project")
return
}
respondError(w, http.StatusInternalServerError, "failed to get project: "+err.Error())
return
}
vols, err := s.store.GetVolumesByProjectID(projectID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to list volumes: "+err.Error())
return
}
respondJSON(w, http.StatusOK, vols)
}
// createVolume handles POST /api/projects/{id}/volumes.
func (s *Server) createVolume(w http.ResponseWriter, r *http.Request) {
projectID := chi.URLParam(r, "id")
// Verify project exists.
if _, err := s.store.GetProjectByID(projectID); err != nil {
if errors.Is(err, store.ErrNotFound) {
respondNotFound(w, "project")
return
}
respondError(w, http.StatusInternalServerError, "failed to get project: "+err.Error())
return
}
var req volumeRequest
if !decodeJSON(w, r, &req) {
return
}
if req.Source == "" {
respondError(w, http.StatusBadRequest, "source is required")
return
}
if req.Target == "" {
respondError(w, http.StatusBadRequest, "target is required")
return
}
if req.Mode == "" {
req.Mode = "shared"
}
if req.Mode != "shared" && req.Mode != "isolated" {
respondError(w, http.StatusBadRequest, "mode must be 'shared' or 'isolated'")
return
}
vol, err := s.store.CreateVolume(store.Volume{
ProjectID: projectID,
Source: req.Source,
Target: req.Target,
Mode: req.Mode,
})
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to create volume: "+err.Error())
return
}
respondJSON(w, http.StatusCreated, vol)
}
// updateVolume handles PUT /api/projects/{id}/volumes/{volId}.
func (s *Server) updateVolume(w http.ResponseWriter, r *http.Request) {
volID := chi.URLParam(r, "volId")
existing, err := s.store.GetVolumeByID(volID)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
respondNotFound(w, "volume")
return
}
respondError(w, http.StatusInternalServerError, "failed to get volume: "+err.Error())
return
}
var req volumeRequest
if !decodeJSON(w, r, &req) {
return
}
updated := existing
if req.Source != "" {
updated.Source = req.Source
}
if req.Target != "" {
updated.Target = req.Target
}
if req.Mode != "" {
if req.Mode != "shared" && req.Mode != "isolated" {
respondError(w, http.StatusBadRequest, "mode must be 'shared' or 'isolated'")
return
}
updated.Mode = req.Mode
}
if err := s.store.UpdateVolume(updated); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update volume: "+err.Error())
return
}
respondJSON(w, http.StatusOK, updated)
}
// deleteVolume handles DELETE /api/projects/{id}/volumes/{volId}.
func (s *Server) deleteVolume(w http.ResponseWriter, r *http.Request) {
volID := chi.URLParam(r, "volId")
if err := s.store.DeleteVolume(volID); err != nil {
if errors.Is(err, store.ErrNotFound) {
respondNotFound(w, "volume")
return
}
respondError(w, http.StatusInternalServerError, "failed to delete volume: "+err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"deleted": volID})
}