package httpserver import ( "net/http" "strings" "github.com/toki/oto/services/core/internal/cicdstate" "github.com/toki/oto/services/core/internal/runnerregistry" ) type runnerControlDispatcher interface { DispatchQueuedJobs() CancelExecution(runnerID, execID, reason string) error } // registerRoutes registers all HTTP routes on the given ServeMux. // This file exists to separate route registration logic from handler // implementation, making it easier to refactor handlers and DTOs later. // Note: HTTP runner job and registration endpoints are maintained as compatibility fallback routes. func registerRoutes(mux *http.ServeMux, registry *runnerregistry.Registry, store *cicdstate.Store, dispatcher runnerControlDispatcher) { mux.HandleFunc("/healthz", withCORS(handleHealthz)) mux.HandleFunc("/readyz", withCORS(handleReadyz)) mux.HandleFunc("/api/v1/runners/register", withCORS(handleRunnerRegister(registry))) mux.HandleFunc("/api/v1/runners/bootstrap-command", withCORS(handleRunnerBootstrapCommand(registry))) mux.HandleFunc("/api/v1/runners/{id}/heartbeat", withCORS(handleRunnerHeartbeat(registry))) mux.HandleFunc("/api/v1/runners/{id}/disconnect", withCORS(handleRunnerDisconnect(registry))) mux.HandleFunc("/api/v1/runners/{id}", withCORS(handleRunnerResource(registry))) mux.HandleFunc("/bootstrap/oto-agent.sh", withCORS(handleServeBootstrapScript(embeddedBootstrapProvider{}))) mux.HandleFunc("/bootstrap/oto-agent.ps1", withCORS(handleServeBootstrapPs1(embeddedBootstrapPs1Provider{}))) mux.HandleFunc("/api/v1/", withCORS(handleRouterWithDispatcher(store, registry, dispatcher))) } func withCORS(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Max-Age", "600") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next(w, r) } } // Extract path segments after "/api/v1/" func apiPathSegments(path string) []string { if !strings.HasPrefix(path, "/api/v1/") { return nil } trimmed := strings.TrimPrefix(path, "/api/v1/") trimmed = strings.TrimRight(trimmed, "/") if trimmed == "" { return nil } return strings.Split(trimmed, "/") } func handleRouter(store *cicdstate.Store, registry *runnerregistry.Registry) http.HandlerFunc { return handleRouterWithDispatcher(store, registry, nil) } func handleRouterWithDispatcher(store *cicdstate.Store, registry *runnerregistry.Registry, dispatcher runnerControlDispatcher) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { parts := apiPathSegments(r.URL.Path) if parts == nil { http.NotFound(w, r) return } switch parts[0] { case "jobs": switch { case len(parts) == 1 && r.Method == http.MethodPost: handleCreateJob(store, dispatcher)(w, r) case len(parts) == 1 && r.Method == http.MethodGet: writeResponse(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) return case len(parts) == 2 && r.Method == http.MethodGet: handleGetJob(store)(w, r) case len(parts) == 3 && parts[2] == "executions" && r.Method == http.MethodPost: handleCreateExecution(store)(w, r) default: http.NotFound(w, r) } case "executions": if len(parts) < 2 { http.NotFound(w, r) return } switch { case len(parts) == 2 && r.Method == http.MethodGet: handleGetExecution(store)(w, r) case len(parts) == 3 && parts[2] == "logs" && r.Method == http.MethodPost: handleAppendLog(store)(w, r) case len(parts) == 3 && parts[2] == "logs" && r.Method == http.MethodGet: handleGetLogs(store)(w, r) case len(parts) == 3 && parts[2] == "artifacts" && r.Method == http.MethodPost: handleAppendArtifact(store)(w, r) case len(parts) == 3 && parts[2] == "artifacts" && r.Method == http.MethodGet: handleGetArtifacts(store)(w, r) default: http.NotFound(w, r) } case "runners": if len(parts) < 2 { http.NotFound(w, r) return } runnerID := parts[1] switch { case len(parts) == 3 && parts[2] == "status" && r.Method == http.MethodGet: handleRunnerStatus(store, registry, runnerID)(w, r) case len(parts) == 3 && parts[2] == "self-update" && r.Method == http.MethodPost: handleRunnerSelfUpdate(store, registry, runnerID)(w, r) case len(parts) == 4 && parts[2] == "jobs" && parts[3] == "claim" && r.Method == http.MethodPost: handleRunnerClaimJob(store, registry, runnerID)(w, r) case len(parts) == 5 && parts[2] == "executions" && parts[4] == "cancel" && r.Method == http.MethodPost: handleRunnerCancelExecution(store, registry, dispatcher, runnerID, parts[3])(w, r) case len(parts) == 5 && parts[2] == "executions" && parts[4] == "report" && r.Method == http.MethodPost: handleRunnerReportExecution(store, registry, runnerID, parts[3])(w, r) case len(parts) == 5 && parts[2] == "executions" && parts[4] == "logs" && r.Method == http.MethodPost: handleRunnerAppendLog(store, registry, runnerID, parts[3])(w, r) case len(parts) == 5 && parts[2] == "executions" && parts[4] == "artifacts" && r.Method == http.MethodPost: handleRunnerAppendArtifact(store, registry, runnerID, parts[3])(w, r) default: http.NotFound(w, r) } default: http.NotFound(w, r) } } }