Files
orca/internal/daemon/jobs_handler.go
T
Jon Chery 708d983429 feat(P05): health check daemon with /healthz, /readyz, /v1/* handlers
- internal/daemon/server.go: HTTP Server with lifecycle, logging middleware
- internal/daemon/health.go: /healthz (liveness), /readyz (db+ready), /v1/status
- internal/daemon/jobs_handler.go: GET /v1/jobs, /v1/jobs/{id}, /v1/jobs/{id}/tasks
- internal/daemon/nodes_handler.go: GET /v1/nodes
- internal/daemon/tasks_handler.go: GET /v1/tasks (with ?job_id and ?limit)
- internal/daemon/validate.go: input validation for path IDs
- internal/daemon/version.go: ldflags-friendly version var
- internal/store: added TaskRepo.ListRecent for unfiltered task listing
- internal/cli/daemon.go: CLI wiring with signal.NotifyContext shutdown

Personas: backend-engineer (handlers), cli-engineer (CLI wiring),
security-engineer (input validation, no secrets in access logs, slog JSON).

---ci---
project: orca
phase: 5
milestone: v0.1
status: execute
requirements:
  covered: [REQ-006, REQ-017, REQ-019]
  partial: []
---/ci---
2026-06-03 19:23:21 +00:00

110 lines
3.1 KiB
Go

package daemon
import (
"context"
"errors"
"log/slog"
"net/http"
"strings"
"time"
"git.cloudinit.dev/coreci/orca/internal/model"
"git.cloudinit.dev/coreci/orca/internal/store"
)
// handleJobsCollection handles /v1/jobs.
// - GET → list all jobs
// - POST → not yet supported (job submission is CLI-only in v0.1)
func (s *Server) handleJobsCollection(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
switch r.Method {
case http.MethodGet:
jobs, err := store.NewJobRepo(s.db).List(ctx)
if err != nil {
s.log.Error("list jobs",
slog.String("component", "daemon"),
slog.String("error", err.Error()))
writeError(w, http.StatusInternalServerError, "failed to list jobs")
return
}
if jobs == nil {
jobs = []*model.Job{}
}
writeJSON(w, http.StatusOK, map[string]any{"jobs": jobs, "count": len(jobs)})
case http.MethodPost:
// Job submission via HTTP is intentionally not exposed in v0.1.
// The CLI submits jobs to the local store directly; the daemon
// exists for observability and lifecycle control.
writeError(w, http.StatusNotImplemented, "job submission via API is not supported in v0.1; use 'orca job run'")
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
// handleJobsItem handles /v1/jobs/{id} and /v1/jobs/{id}/tasks.
// - GET /v1/jobs/{id} → job details
// - GET /v1/jobs/{id}/tasks → tasks for a job
func (s *Server) handleJobsItem(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
// Path is /v1/jobs/{id} or /v1/jobs/{id}/tasks
path := strings.TrimPrefix(r.URL.Path, "/v1/jobs/")
parts := strings.Split(path, "/")
if len(parts) == 0 || parts[0] == "" {
writeError(w, http.StatusBadRequest, "job id required")
return
}
id := parts[0]
if err := validateID(id); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
// /v1/jobs/{id}/tasks
if len(parts) == 2 && parts[1] == "tasks" {
tasks, err := store.NewTaskRepo(s.db).ListByJob(ctx, id)
if err != nil {
s.log.Error("list tasks for job",
slog.String("component", "daemon"),
slog.String("job_id", id),
slog.String("error", err.Error()))
writeError(w, http.StatusInternalServerError, "failed to list tasks")
return
}
if tasks == nil {
tasks = []*model.Task{}
}
writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks, "count": len(tasks), "job_id": id})
return
}
// /v1/jobs/{id} (with no further path)
if len(parts) != 1 {
writeError(w, http.StatusNotFound, "not found")
return
}
job, err := store.NewJobRepo(s.db).Get(ctx, id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeError(w, http.StatusNotFound, "job not found")
return
}
s.log.Error("get job",
slog.String("component", "daemon"),
slog.String("job_id", id),
slog.String("error", err.Error()))
writeError(w, http.StatusInternalServerError, "failed to get job")
return
}
writeJSON(w, http.StatusOK, job)
}