fetch_ml/internal/api/handlers.go

323 lines
9.5 KiB
Go

// Package api provides HTTP handlers for the fetch_ml API server
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/jfraeys/fetch_ml/internal/auth"
"github.com/jfraeys/fetch_ml/internal/experiment"
"github.com/jfraeys/fetch_ml/internal/jupyter"
"github.com/jfraeys/fetch_ml/internal/logging"
)
// Handlers groups all HTTP handlers
type Handlers struct {
expManager *experiment.Manager
jupyterServiceMgr *jupyter.ServiceManager
logger *logging.Logger
}
// NewHandlers creates a new handler group
func NewHandlers(
expManager *experiment.Manager,
jupyterServiceMgr *jupyter.ServiceManager,
logger *logging.Logger,
) *Handlers {
return &Handlers{
expManager: expManager,
jupyterServiceMgr: jupyterServiceMgr,
logger: logger,
}
}
// RegisterHandlers registers all HTTP handlers with the mux
func (h *Handlers) RegisterHandlers(mux *http.ServeMux) {
// Health check endpoints
mux.HandleFunc("/db-status", h.handleDBStatus)
// Jupyter service endpoints
if h.jupyterServiceMgr != nil {
mux.HandleFunc("/api/jupyter/services", h.handleJupyterServices)
mux.HandleFunc("/api/jupyter/experiments/link", h.handleJupyterExperimentLink)
mux.HandleFunc("/api/jupyter/experiments/sync", h.handleJupyterExperimentSync)
}
}
// handleHealth responds with a simple health check
func (h *Handlers) handleHealth(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(w, "OK\n")
}
// handleDBStatus responds with database connection status
func (h *Handlers) handleDBStatus(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
// This would need the DB instance passed to handlers
// For now, return a basic response
response := map[string]any{
"status": "unknown",
"message": "Database status check not implemented",
}
jsonBytes, _ := json.Marshal(response)
w.WriteHeader(http.StatusOK)
if _, err := w.Write(jsonBytes); err != nil {
h.logger.Error("failed to write response", "error", err)
}
}
// handleJupyterServices handles Jupyter service management requests
func (h *Handlers) handleJupyterServices(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
user := auth.GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized: No user context", http.StatusUnauthorized)
return
}
switch r.Method {
case http.MethodGet:
if !user.HasPermission("jupyter:read") {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
h.listJupyterServices(w, r)
case http.MethodPost:
if !user.HasPermission("jupyter:manage") {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
h.startJupyterService(w, r)
case http.MethodDelete:
if !user.HasPermission("jupyter:manage") {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
h.stopJupyterService(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// listJupyterServices lists all Jupyter services
func (h *Handlers) listJupyterServices(w http.ResponseWriter, _ *http.Request) {
services := h.jupyterServiceMgr.ListServices()
jsonBytes, err := json.Marshal(services)
if err != nil {
http.Error(w, "Failed to marshal services", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if _, err := w.Write(jsonBytes); err != nil {
h.logger.Error("failed to write response", "error", err)
}
}
// startJupyterService starts a new Jupyter service
func (h *Handlers) startJupyterService(w http.ResponseWriter, r *http.Request) {
var req jupyter.StartRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
ctx := r.Context()
service, err := h.jupyterServiceMgr.StartService(ctx, &req)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to start service: %v", err), http.StatusInternalServerError)
return
}
jsonBytes, err := json.Marshal(service)
if err != nil {
http.Error(w, "Failed to marshal service", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(jsonBytes); err != nil {
h.logger.Error("failed to write response", "error", err)
}
}
// stopJupyterService stops a Jupyter service
func (h *Handlers) stopJupyterService(w http.ResponseWriter, r *http.Request) {
serviceID := r.URL.Query().Get("id")
if serviceID == "" {
http.Error(w, "Service ID is required", http.StatusBadRequest)
return
}
ctx := r.Context()
if err := h.jupyterServiceMgr.StopService(ctx, serviceID); err != nil {
http.Error(w, fmt.Sprintf("Failed to stop service: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]string{
"status": "stopped",
"id": serviceID,
}); err != nil {
h.logger.Error("failed to encode response", "error", err)
}
}
// handleJupyterExperimentLink handles linking Jupyter workspaces with experiments
func (h *Handlers) handleJupyterExperimentLink(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
user := auth.GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized: No user context", http.StatusUnauthorized)
return
}
if !user.HasPermission("jupyter:manage") {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Workspace string `json:"workspace"`
ExperimentID string `json:"experiment_id"`
ServiceID string `json:"service_id,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Workspace == "" || req.ExperimentID == "" {
http.Error(w, "Workspace and experiment_id are required", http.StatusBadRequest)
return
}
if !h.expManager.ExperimentExists(req.ExperimentID) {
http.Error(w, "Experiment not found", http.StatusNotFound)
return
}
// Link workspace with experiment using service manager
if err := h.jupyterServiceMgr.LinkWorkspaceWithExperiment(
req.Workspace,
req.ExperimentID,
req.ServiceID,
); err != nil {
http.Error(w, fmt.Sprintf("Failed to link workspace: %v", err), http.StatusInternalServerError)
return
}
// Get workspace metadata to return
metadata, err := h.jupyterServiceMgr.GetWorkspaceMetadata(req.Workspace)
if err != nil {
http.Error(
w,
fmt.Sprintf("Failed to get workspace metadata: %v", err),
http.StatusInternalServerError,
)
return
}
h.logger.Info("jupyter workspace linked with experiment",
"workspace", req.Workspace,
"experiment_id", req.ExperimentID,
"service_id", req.ServiceID)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"status": "linked",
"data": metadata,
}); err != nil {
h.logger.Error("failed to encode response", "error", err)
}
}
// handleJupyterExperimentSync handles synchronization between Jupyter workspaces and experiments
func (h *Handlers) handleJupyterExperimentSync(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
user := auth.GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized: No user context", http.StatusUnauthorized)
return
}
if !user.HasPermission("jupyter:manage") {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Workspace string `json:"workspace"`
ExperimentID string `json:"experiment_id"`
Direction string `json:"direction"` // "pull" or "push"
SyncType string `json:"sync_type"` // "data", "notebooks", "all"
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Workspace == "" || req.ExperimentID == "" || req.Direction == "" {
http.Error(w, "Workspace, experiment_id, and direction are required", http.StatusBadRequest)
return
}
// Validate experiment exists
if !h.expManager.ExperimentExists(req.ExperimentID) {
http.Error(w, "Experiment not found", http.StatusNotFound)
return
}
// Perform sync operation using service manager
ctx := r.Context()
if err := h.jupyterServiceMgr.SyncWorkspaceWithExperiment(
ctx, req.Workspace, req.ExperimentID, req.Direction); err != nil {
http.Error(w, fmt.Sprintf("Failed to sync workspace: %v", err), http.StatusInternalServerError)
return
}
// Get updated metadata
metadata, err := h.jupyterServiceMgr.GetWorkspaceMetadata(req.Workspace)
if err != nil {
http.Error(
w,
fmt.Sprintf("Failed to get workspace metadata: %v", err),
http.StatusInternalServerError,
)
return
}
// Create sync result
syncResult := map[string]interface{}{
"workspace": req.Workspace,
"experiment_id": req.ExperimentID,
"direction": req.Direction,
"sync_type": req.SyncType,
"synced_at": metadata.LastSync,
"status": "completed",
"metadata": metadata,
}
h.logger.Info("jupyter workspace sync completed",
"workspace", req.Workspace,
"experiment_id", req.ExperimentID,
"direction", req.Direction,
"sync_type", req.SyncType)
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(syncResult); err != nil {
h.logger.Error("failed to encode response", "error", err)
}
}