d4659146fc
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.
144 lines
3.6 KiB
Go
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})
|
|
}
|