// Package plugins provides HTTP handlers for plugin management package plugins import ( "encoding/json" "net/http" "time" "github.com/jfraeys/fetch_ml/internal/auth" "github.com/jfraeys/fetch_ml/internal/logging" "github.com/jfraeys/fetch_ml/internal/tracking" ) // Handler provides plugin-related HTTP handlers type Handler struct { logger *logging.Logger registry *tracking.Registry config map[string]PluginConfig // Plugin configurations } // PluginConfig represents the configuration for a plugin type PluginConfig struct { Enabled bool `json:"enabled"` Mode string `json:"mode"` // sidecar, remote, disabled Image string `json:"image,omitempty"` Settings map[string]interface{} `json:"settings,omitempty"` LogBasePath string `json:"log_base_path,omitempty"` ArtifactPath string `json:"artifact_path,omitempty"` } // PluginInfo represents plugin information returned by the API type PluginInfo struct { Name string `json:"name"` Enabled bool `json:"enabled"` Mode string `json:"mode"` Status string `json:"status"` // healthy, unhealthy, starting, stopped Config PluginConfig `json:"config"` RequiresRestart bool `json:"requires_restart"` Version string `json:"version,omitempty"` } // PluginStatus represents the status of a plugin instance type PluginStatus struct { Name string `json:"name"` Status string `json:"status"` URL string `json:"url,omitempty"` LastCheck time.Time `json:"last_check,omitempty"` } // NewHandler creates a new plugins handler func NewHandler( logger *logging.Logger, registry *tracking.Registry, config map[string]PluginConfig, ) *Handler { return &Handler{ logger: logger, registry: registry, config: config, } } // GetV1Plugins handles GET /v1/plugins func (h *Handler) GetV1Plugins(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } plugins := []PluginInfo{} // If registry is available, get plugin status if h.registry != nil { for name := range h.config { cfg := h.config[name] info := PluginInfo{ Name: name, Enabled: cfg.Enabled, Mode: cfg.Mode, Status: "unknown", Config: cfg, RequiresRestart: false, // Default: plugins support hot-reload Version: "1.0.0", // Placeholder } // Check if plugin is registered if plugin, ok := h.registry.Get(name); ok { // Check plugin health if available if cfg.Enabled && cfg.Mode != "disabled" { info.Status = "healthy" // Default, would check actual health _ = plugin // Use plugin for actual health check } } plugins = append(plugins, info) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(plugins) } // GetV1PluginsPluginName handles GET /v1/plugins/{pluginName} func (h *Handler) GetV1PluginsPluginName(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } pluginName := r.PathValue("pluginName") if pluginName == "" { http.Error(w, `{"error":"Missing plugin name","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } cfg, ok := h.config[pluginName] if !ok { http.Error(w, `{"error":"Plugin not found","code":"NOT_FOUND"}`, http.StatusNotFound) return } info := PluginInfo{ Name: pluginName, Enabled: cfg.Enabled, Mode: cfg.Mode, Status: "unknown", Config: cfg, RequiresRestart: false, Version: "1.0.0", } if cfg.Enabled && cfg.Mode != "disabled" { info.Status = "healthy" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) } // GetV1PluginsPluginNameConfig handles GET /v1/plugins/{pluginName}/config func (h *Handler) GetV1PluginsPluginNameConfig(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } pluginName := r.PathValue("pluginName") if pluginName == "" { http.Error(w, `{"error":"Missing plugin name","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } cfg, ok := h.config[pluginName] if !ok { http.Error(w, `{"error":"Plugin not found","code":"NOT_FOUND"}`, http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(cfg) } // PutV1PluginsPluginNameConfig handles PUT /v1/plugins/{pluginName}/config func (h *Handler) PutV1PluginsPluginNameConfig(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:write") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } pluginName := r.PathValue("pluginName") if pluginName == "" { http.Error(w, `{"error":"Missing plugin name","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } var newConfig PluginConfig if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil { http.Error(w, `{"error":"Invalid request body","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } // Update config h.config[pluginName] = newConfig h.logger.Info("updated plugin config", "plugin", pluginName, "user", user.Name) // Return updated plugin info info := PluginInfo{ Name: pluginName, Enabled: newConfig.Enabled, Mode: newConfig.Mode, Status: "healthy", Config: newConfig, RequiresRestart: false, Version: "1.0.0", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) } // DeleteV1PluginsPluginNameConfig handles DELETE /v1/plugins/{pluginName}/config func (h *Handler) DeleteV1PluginsPluginNameConfig(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:write") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } pluginName := r.PathValue("pluginName") if pluginName == "" { http.Error(w, `{"error":"Missing plugin name","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } cfg, ok := h.config[pluginName] if !ok { http.Error(w, `{"error":"Plugin not found","code":"NOT_FOUND"}`, http.StatusNotFound) return } // Disable the plugin cfg.Enabled = false h.config[pluginName] = cfg h.logger.Info("disabled plugin", "plugin", pluginName, "user", user.Name) w.WriteHeader(http.StatusNoContent) } // GetV1PluginsPluginNameHealth handles GET /v1/plugins/{pluginName}/health func (h *Handler) GetV1PluginsPluginNameHealth(w http.ResponseWriter, r *http.Request) { user := auth.GetUserFromContext(r.Context()) if !h.checkPermission(user, "plugins:read") { http.Error(w, `{"error":"Insufficient permissions","code":"FORBIDDEN"}`, http.StatusForbidden) return } pluginName := r.PathValue("pluginName") if pluginName == "" { http.Error(w, `{"error":"Missing plugin name","code":"BAD_REQUEST"}`, http.StatusBadRequest) return } cfg, ok := h.config[pluginName] if !ok { http.Error(w, `{"error":"Plugin not found","code":"NOT_FOUND"}`, http.StatusNotFound) return } status := "healthy" if !cfg.Enabled || cfg.Mode == "disabled" { status = "stopped" } response := map[string]interface{}{ "status": status, "version": "1.0.0", "timestamp": time.Now().UTC(), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // checkPermission checks if the user has the required permission func (h *Handler) checkPermission(user *auth.User, permission string) bool { if user == nil { return false } // Admin has all permissions for _, role := range user.Roles { if role == "admin" { return true } } // Check specific permission for perm, hasPerm := range user.Permissions { if hasPerm && perm == permission { return true } } return false }