Files
silo-server/internal/api/handlers/tasks.go
T

175 lines
5.3 KiB
Go

package handlers
import (
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/Silo-Server/silo-server/internal/taskmanager"
)
const refreshMetadataTaskKey = "refresh_metadata"
// TaskManagerAPI is the subset of TaskManager used by the handler.
type TaskManagerAPI interface {
ListTasks(includeHidden bool) []taskmanager.TaskInfo
GetTaskInfo(key string) taskmanager.TaskInfo
RunTask(ctx context.Context, key string) error
CancelTask(key string) error
UpdateTriggers(key string, triggers []taskmanager.TriggerConfig) error
}
// TaskHistoryLister lists execution history for a task.
type TaskHistoryLister interface {
List(ctx context.Context, taskKey string, limit int) ([]taskmanager.ExecutionResult, error)
}
type TaskMetricsProvider interface {
GetRefreshMetadataMetrics(ctx context.Context) (any, error)
}
// TaskHandler handles REST API requests for /api/v1/admin/tasks.
type TaskHandler struct {
mgr TaskManagerAPI
history TaskHistoryLister
metrics TaskMetricsProvider
}
// NewTaskHandler creates a new TaskHandler.
func NewTaskHandler(mgr TaskManagerAPI, history TaskHistoryLister, metrics TaskMetricsProvider) *TaskHandler {
return &TaskHandler{mgr: mgr, history: history, metrics: metrics}
}
// HandleListTasks handles GET /api/v1/admin/tasks
func (h *TaskHandler) HandleListTasks(w http.ResponseWriter, r *http.Request) {
includeHidden := r.URL.Query().Get("include_hidden") == "true"
tasks := h.mgr.ListTasks(includeHidden)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
}
// HandleGetTask handles GET /api/v1/admin/tasks/{key}
func (h *TaskHandler) HandleGetTask(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
info := h.mgr.GetTaskInfo(key)
if info.Key == "" {
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
}
// HandleRunTask handles POST /api/v1/admin/tasks/{key}/run
func (h *TaskHandler) HandleRunTask(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
info := h.mgr.GetTaskInfo(key)
if info.Key == "" {
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
return
}
if info.State == taskmanager.TaskStateRunning || info.State == taskmanager.TaskStateCancelling {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(map[string]string{"error": "task is already running"})
return
}
go h.mgr.RunTask(context.Background(), key)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"status": "started"})
}
// HandleCancelTask handles POST /api/v1/admin/tasks/{key}/cancel
func (h *TaskHandler) HandleCancelTask(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
err := h.mgr.CancelTask(key)
if err != nil {
if errors.Is(err, taskmanager.ErrTaskNotFound) {
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
return
}
if errors.Is(err, taskmanager.ErrTaskNotRunning) {
http.Error(w, `{"error":"task is not running"}`, http.StatusConflict)
return
}
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "cancelling"})
}
// HandleUpdateTriggers handles PUT /api/v1/admin/tasks/{key}/triggers
func (h *TaskHandler) HandleUpdateTriggers(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
var triggers []taskmanager.TriggerConfig
if err := json.NewDecoder(r.Body).Decode(&triggers); err != nil {
http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
return
}
if err := h.mgr.UpdateTriggers(key, triggers); err != nil {
if errors.Is(err, taskmanager.ErrTaskNotFound) {
http.Error(w, `{"error":"task not found"}`, http.StatusNotFound)
return
}
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(h.mgr.GetTaskInfo(key))
}
// HandleGetHistory handles GET /api/v1/admin/tasks/{key}/history
func (h *TaskHandler) HandleGetHistory(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
limit := 20
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
results, err := h.history.List(r.Context(), key, limit)
if err != nil {
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
if results == nil {
results = []taskmanager.ExecutionResult{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
}
func (h *TaskHandler) HandleGetMetrics(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
if key != refreshMetadataTaskKey || h.metrics == nil {
http.Error(w, `{"error":"task metrics not found"}`, http.StatusNotFound)
return
}
metrics, err := h.metrics.GetRefreshMetadataMetrics(r.Context())
if err != nil {
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(metrics)
}