dev-pod-api-build/internal/api/runners.go
2026-04-16 04:16:36 +00:00

246 lines
6.8 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/k8s"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/store"
)
func (s *Server) handleCreateRunner(w http.ResponseWriter, r *http.Request) {
if s.Runners == nil {
writeError(w, http.StatusServiceUnavailable, "runner management not configured")
return
}
var req model.CreateRunnerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
authUser := UserFromContext(r.Context())
role := RoleFromContext(r.Context())
if role != model.RoleAdmin && authUser.ID != req.User {
writeError(w, http.StatusForbidden, "cannot create runners for other users")
return
}
if _, err := s.Users.GetUser(r.Context(), req.User); err != nil {
writeError(w, http.StatusNotFound, "user not found")
return
}
if req.WebhookDeliveryID != "" {
isDupe, err := s.Runners.IsDeliveryProcessed(r.Context(), req.WebhookDeliveryID)
if err != nil {
s.Logger.Error("check delivery dedupe", "error", err)
writeError(w, http.StatusInternalServerError, "failed to check delivery")
return
}
if isDupe {
writeError(w, http.StatusConflict, "webhook delivery already processed")
return
}
}
runnerID, err := store.GenerateRunnerID()
if err != nil {
s.Logger.Error("generate runner id", "error", err)
writeError(w, http.StatusInternalServerError, "failed to generate runner id")
return
}
branch := req.Branch
if branch == "" {
branch = "main"
}
cpuReq := req.CPUReq
if cpuReq == "" {
cpuReq = "2"
}
memReq := req.MemReq
if memReq == "" {
memReq = "4Gi"
}
now := time.Now().UTC()
runner := &model.Runner{
ID: runnerID,
User: req.User,
RepoURL: req.Repo,
Branch: branch,
Tools: req.Tools,
Task: req.Task,
Status: model.RunnerStatusReceived,
WebhookDeliveryID: req.WebhookDeliveryID,
CPUReq: cpuReq,
MemReq: memReq,
CreatedAt: now,
}
if err := s.Runners.CreateRunner(r.Context(), runner); err != nil {
if errors.Is(err, store.ErrDuplicate) {
writeError(w, http.StatusConflict, "runner already exists")
return
}
s.Logger.Error("create runner record", "error", err)
writeError(w, http.StatusInternalServerError, "failed to create runner")
return
}
if err := s.Runners.UpdateRunnerStatus(r.Context(), runnerID, model.RunnerStatusPodCreating, ""); err != nil {
s.Logger.Error("update runner status to pod_creating", "error", err)
writeError(w, http.StatusInternalServerError, "failed to update runner status")
return
}
if s.RunnerPods != nil {
podName, podErr := s.RunnerPods.CreateRunnerPod(r.Context(), k8s.CreateRunnerPodOpts{
User: req.User,
RunnerID: runnerID,
Tools: req.Tools,
Task: req.Task,
RepoURL: req.Repo,
Branch: branch,
CPUReq: cpuReq,
MemReq: memReq,
ForgejoRunnerToken: s.ForgejoRunnerToken,
})
if podErr != nil {
s.Logger.Error("create runner pod", "runner", runnerID, "error", podErr)
_ = s.Runners.UpdateRunnerStatus(r.Context(), runnerID, model.RunnerStatusFailed, "")
writeError(w, http.StatusInternalServerError, "failed to create runner pod")
return
}
runner.PodName = podName
}
runner.Status = model.RunnerStatusPodCreating
writeJSON(w, http.StatusCreated, runner)
}
func (s *Server) handleListRunners(w http.ResponseWriter, r *http.Request) {
if s.Runners == nil {
writeError(w, http.StatusServiceUnavailable, "runner management not configured")
return
}
role := RoleFromContext(r.Context())
authUser := UserFromContext(r.Context())
userFilter := r.URL.Query().Get("user")
statusFilter := r.URL.Query().Get("status")
if role != model.RoleAdmin {
userFilter = authUser.ID
}
runners, err := s.Runners.ListRunners(r.Context(), userFilter, statusFilter)
if err != nil {
s.Logger.Error("list runners", "error", err)
writeError(w, http.StatusInternalServerError, "failed to list runners")
return
}
if runners == nil {
runners = []model.Runner{}
}
writeJSON(w, http.StatusOK, runners)
}
func (s *Server) handleDeleteRunner(w http.ResponseWriter, r *http.Request) {
if s.Runners == nil {
writeError(w, http.StatusServiceUnavailable, "runner management not configured")
return
}
id := chi.URLParam(r, "id")
runner, err := s.Runners.GetRunner(r.Context(), id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeError(w, http.StatusNotFound, "runner not found")
return
}
writeError(w, http.StatusInternalServerError, "failed to get runner")
return
}
if !canAccess(r, runner.User) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if runner.PodName != "" && s.RunnerPods != nil {
if podErr := s.RunnerPods.DeleteRunnerPod(r.Context(), runner.User, runner.PodName); podErr != nil {
s.Logger.Error("delete runner pod", "runner", id, "pod", runner.PodName, "error", podErr)
}
}
if err := s.Runners.DeleteRunner(r.Context(), id); err != nil {
s.Logger.Error("delete runner record", "id", id, "error", err)
writeError(w, http.StatusInternalServerError, "failed to delete runner")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleUpdateRunnerStatus(w http.ResponseWriter, r *http.Request) {
if s.Runners == nil {
writeError(w, http.StatusServiceUnavailable, "runner management not configured")
return
}
id := chi.URLParam(r, "id")
var req model.UpdateRunnerStatusRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
runner, err := s.Runners.GetRunner(r.Context(), id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeError(w, http.StatusNotFound, "runner not found")
return
}
writeError(w, http.StatusInternalServerError, "failed to get runner")
return
}
if !canAccess(r, runner.User) {
writeError(w, http.StatusForbidden, "access denied")
return
}
if err := s.Runners.UpdateRunnerStatus(r.Context(), id, req.Status, req.ForgejoRunnerID); err != nil {
s.Logger.Error("update runner status", "id", id, "status", req.Status, "error", err)
writeError(w, http.StatusBadRequest, err.Error())
return
}
updated, err := s.Runners.GetRunner(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get updated runner")
return
}
writeJSON(w, http.StatusOK, updated)
}