332 lines
9.8 KiB
Go
332 lines
9.8 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.casaderoll.de/michael/urbm/internal/model"
|
|
"git.casaderoll.de/michael/urbm/internal/service"
|
|
)
|
|
|
|
type Server struct {
|
|
service *service.Service
|
|
log *slog.Logger
|
|
http *http.Server
|
|
socket string
|
|
}
|
|
|
|
var Version = "dev"
|
|
|
|
func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
|
s := &Server{service: svc, log: log, socket: socket}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /v1/health", s.health)
|
|
mux.HandleFunc("GET /v1/config", s.getConfig)
|
|
mux.HandleFunc("PUT /v1/config", s.putConfig)
|
|
mux.HandleFunc("PUT /v1/secrets/{id}", s.putSecret)
|
|
mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
|
|
mux.HandleFunc("GET /v1/runs", s.runs)
|
|
mux.HandleFunc("DELETE /v1/runs", s.clearRuns)
|
|
mux.HandleFunc("GET /v1/filesystem/directories", s.directories)
|
|
mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads)
|
|
mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob)
|
|
mux.HandleFunc("POST /v1/runs/{id}/cancel", s.cancelRun)
|
|
mux.HandleFunc("POST /v1/runs/{id}/pause", s.pauseRun)
|
|
mux.HandleFunc("POST /v1/runs/{id}/resume", s.resumeRun)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/init", s.initRepository)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/unlock", s.unlockRepository)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/force-unlock", s.forceUnlockRepository)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/password", s.changeRepositoryPassword)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/password/adopt", s.adoptRepositoryPassword)
|
|
mux.HandleFunc("POST /v1/repositories/{id}/{action}", s.maintenance)
|
|
mux.HandleFunc("GET /v1/repositories/stats", s.repositoryStats)
|
|
mux.HandleFunc("GET /v1/repositories/{id}/snapshots", s.snapshots)
|
|
mux.HandleFunc("GET /v1/repositories/{id}/snapshots/{snapshot}/files", s.snapshotFiles)
|
|
mux.HandleFunc("POST /v1/restores", s.restore)
|
|
s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 5 * time.Minute, IdleTimeout: 30 * time.Second}
|
|
return s
|
|
}
|
|
|
|
func (s *Server) ListenAndServe() error {
|
|
if err := os.MkdirAll(filepath.Dir(s.socket), 0750); err != nil {
|
|
return err
|
|
}
|
|
_ = os.Remove(s.socket)
|
|
listener, err := net.Listen("unix", s.socket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(s.socket, 0660); err != nil {
|
|
listener.Close()
|
|
return err
|
|
}
|
|
err = s.http.Serve(listener)
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
defer os.Remove(s.socket)
|
|
return s.http.Shutdown(ctx)
|
|
}
|
|
|
|
func (s *Server) health(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, map[string]any{"status": "ok", "version": Version, "schemaVersion": model.SchemaVersion})
|
|
}
|
|
func (s *Server) getConfig(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, s.service.Config())
|
|
}
|
|
|
|
func (s *Server) putConfig(w http.ResponseWriter, r *http.Request) {
|
|
var config model.Config
|
|
if err := decode(r, &config); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
if err := s.service.SaveConfig(config); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, config)
|
|
}
|
|
|
|
func (s *Server) putSecret(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
}
|
|
if err := decode(r, &body); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
if err := s.service.PutSecret(r.PathValue("id"), body.Type, body.Value); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 204, nil)
|
|
}
|
|
func (s *Server) deleteSecret(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.service.DeleteSecret(r.PathValue("id")); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(204)
|
|
}
|
|
func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Runs()) }
|
|
func (s *Server) clearRuns(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, map[string]int{"cleared": s.service.ClearRunHistory()})
|
|
}
|
|
func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.service.BrowseDirectories(r.URL.Query().Get("path"))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, items)
|
|
}
|
|
func (s *Server) workloads(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
|
defer cancel()
|
|
items, err := s.service.DiscoverWorkloads(ctx, model.JobType(r.PathValue("kind")))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, items)
|
|
}
|
|
|
|
func (s *Server) runJob(w http.ResponseWriter, r *http.Request) {
|
|
run, err := s.service.EnqueueJob(r.PathValue("id"), 10)
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 202, run)
|
|
}
|
|
func (s *Server) cancelRun(w http.ResponseWriter, r *http.Request) {
|
|
if !s.service.Cancel(r.PathValue("id")) {
|
|
writeJSON(w, 404, map[string]string{"error": "run not found"})
|
|
return
|
|
}
|
|
w.WriteHeader(204)
|
|
}
|
|
|
|
func (s *Server) pauseRun(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.service.Pause(r.PathValue("id")); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(204)
|
|
}
|
|
|
|
func (s *Server) resumeRun(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.service.Resume(r.PathValue("id")); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(204)
|
|
}
|
|
|
|
func (s *Server) testRepository(w http.ResponseWriter, r *http.Request) {
|
|
s.repositoryAction(w, r, false)
|
|
}
|
|
func (s *Server) initRepository(w http.ResponseWriter, r *http.Request) {
|
|
s.repositoryAction(w, r, true)
|
|
}
|
|
func (s *Server) unlockRepository(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
|
defer cancel()
|
|
if err := s.service.UnlockRepository(ctx, r.PathValue("id")); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|
|
func (s *Server) forceUnlockRepository(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
|
defer cancel()
|
|
if err := s.service.ForceUnlockRepository(ctx, r.PathValue("id")); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|
|
func (s *Server) changeRepositoryPassword(w http.ResponseWriter, r *http.Request) {
|
|
s.repositoryPasswordAction(w, r, false)
|
|
}
|
|
func (s *Server) adoptRepositoryPassword(w http.ResponseWriter, r *http.Request) {
|
|
s.repositoryPasswordAction(w, r, true)
|
|
}
|
|
func (s *Server) repositoryPasswordAction(w http.ResponseWriter, r *http.Request, adopt bool) {
|
|
var body struct {
|
|
Password string `json:"password"`
|
|
}
|
|
if err := decode(r, &body); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
|
defer cancel()
|
|
var err error
|
|
if adopt {
|
|
err = s.service.AdoptRepositoryPassword(ctx, r.PathValue("id"), body.Password)
|
|
} else {
|
|
err = s.service.ChangeRepositoryPassword(ctx, r.PathValue("id"), body.Password)
|
|
}
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|
|
func (s *Server) repositoryAction(w http.ResponseWriter, r *http.Request, initialize bool) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
|
defer cancel()
|
|
if err := s.service.TestRepository(ctx, r.PathValue("id"), initialize); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (s *Server) maintenance(w http.ResponseWriter, r *http.Request) {
|
|
run, err := s.service.EnqueueMaintenance(r.PathValue("id"), r.PathValue("action"))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 202, run)
|
|
}
|
|
|
|
func (s *Server) snapshots(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
|
defer cancel()
|
|
items, err := s.service.Snapshots(ctx, r.PathValue("id"))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, items)
|
|
}
|
|
|
|
func (s *Server) repositoryStats(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
|
defer cancel()
|
|
writeJSON(w, 200, s.service.RepositoryStats(ctx))
|
|
}
|
|
func (s *Server) snapshotFiles(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Minute)
|
|
defer cancel()
|
|
items, err := s.service.SnapshotFiles(ctx, r.PathValue("id"), r.PathValue("snapshot"), r.URL.Query().Get("path"))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, items)
|
|
}
|
|
func (s *Server) restore(w http.ResponseWriter, r *http.Request) {
|
|
var task model.RestoreTask
|
|
if err := decode(r, &task); err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
run, err := s.service.EnqueueRestore(task)
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, 202, run)
|
|
}
|
|
|
|
func decode(r *http.Request, target any) error {
|
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 2*1024*1024))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return fmt.Errorf("validation: invalid JSON: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, value any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if status != 204 && value != nil {
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|
|
}
|
|
func writeError(w http.ResponseWriter, err error) {
|
|
status := 500
|
|
message := err.Error()
|
|
if strings.HasPrefix(message, "validation:") {
|
|
status = 400
|
|
}
|
|
if strings.Contains(message, "unknown") {
|
|
status = 404
|
|
}
|
|
if strings.Contains(message, "already queued") {
|
|
status = 409
|
|
}
|
|
writeJSON(w, status, map[string]string{"error": message})
|
|
}
|
|
|
|
func requestLog(log *slog.Logger, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
started := time.Now()
|
|
next.ServeHTTP(w, r)
|
|
log.Info("api request", "method", r.Method, "path", r.URL.Path, "durationMs", time.Since(started).Milliseconds())
|
|
})
|
|
}
|