246 lines
6.8 KiB
Go
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)
|
|
}
|