151 lines
3.4 KiB
Go
151 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
|
|
"github.com/burrow/forgejo-nsc/internal/app"
|
|
)
|
|
|
|
type Server struct {
|
|
httpServer *http.Server
|
|
app *app.Service
|
|
log *slog.Logger
|
|
}
|
|
|
|
func New(listen string, svc *app.Service, logger *slog.Logger) *Server {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(middleware.RequestID)
|
|
router.Use(middleware.RealIP)
|
|
router.Use(middleware.Logger)
|
|
router.Use(middleware.Recoverer)
|
|
|
|
s := &Server{
|
|
app: svc,
|
|
log: logger,
|
|
httpServer: &http.Server{
|
|
Addr: listen,
|
|
Handler: router,
|
|
ReadTimeout: 30 * time.Second,
|
|
// Dispatch requests can legitimately run for the duration of a build.
|
|
// A short WriteTimeout will kill the request context mid-provisioning.
|
|
WriteTimeout: 2 * time.Hour,
|
|
IdleTimeout: 60 * time.Second,
|
|
},
|
|
}
|
|
|
|
router.Get("/healthz", s.handleHealthz)
|
|
router.Post("/api/v1/dispatch", s.handleDispatch)
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *Server) ListenAndServe() error {
|
|
return s.httpServer.ListenAndServe()
|
|
}
|
|
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
return s.httpServer.Shutdown(ctx)
|
|
}
|
|
|
|
// Handler exposes the underlying HTTP handler for tests.
|
|
func (s *Server) Handler() http.Handler {
|
|
return s.httpServer.Handler
|
|
}
|
|
|
|
type dispatchRequest struct {
|
|
Count int `json:"count"`
|
|
Labels []string `json:"labels"`
|
|
Scope *dispatchScope `json:"scope"`
|
|
TTL string `json:"ttl"`
|
|
Machine string `json:"machine_type"`
|
|
Image string `json:"image"`
|
|
Env map[string]string `json:"env"`
|
|
}
|
|
|
|
type dispatchScope struct {
|
|
Level string `json:"level"`
|
|
Owner string `json:"owner"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (s *Server) handleDispatch(w http.ResponseWriter, r *http.Request) {
|
|
var payload dispatchRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
s.writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
duration, err := parseDuration(payload.TTL)
|
|
if err != nil {
|
|
s.writeError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
var scope *app.Scope
|
|
if payload.Scope != nil {
|
|
scope = &app.Scope{
|
|
Level: payload.Scope.Level,
|
|
Owner: payload.Scope.Owner,
|
|
Name: payload.Scope.Name,
|
|
}
|
|
}
|
|
|
|
resp, err := s.app.Dispatch(r.Context(), app.DispatchRequest{
|
|
Count: payload.Count,
|
|
Labels: payload.Labels,
|
|
Scope: scope,
|
|
TTL: duration,
|
|
Machine: payload.Machine,
|
|
Image: payload.Image,
|
|
ExtraEnv: payload.Env,
|
|
})
|
|
if err != nil {
|
|
s.writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
s.writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func parseDuration(value string) (time.Duration, error) {
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
dur, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if dur <= 0 {
|
|
return 0, errors.New("ttl must be positive")
|
|
}
|
|
return dur, nil
|
|
}
|
|
|
|
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
|
|
s.writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) writeError(w http.ResponseWriter, code int, err error) {
|
|
s.log.Error("request failed", "err", err, "status", code)
|
|
s.writeJSON(w, code, map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
func (s *Server) writeJSON(w http.ResponseWriter, code int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|