323 lines
9.5 KiB
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)
|
|
}
|
|
}
|