Refactor getPluginVersion to accept PluginConfig parameter: - Change signature from getPluginVersion(pluginName) to getPluginVersion(pluginName, cfg) - Update all call sites to pass config - Add TODO comment for future implementation querying actual plugin binary/container Update plugin handlers to use dynamic version retrieval: - GetV1Plugins: Use h.getPluginVersion(name, cfg) instead of hardcoded "1.0.0" - PutV1PluginsPluginNameConfig: Pass newConfig to version retrieval - GetV1PluginsPluginNameHealth: Use actual version from config This prepares the API for dynamic version reporting while maintaining backward compatibility with the current placeholder implementation.
310 lines
9.3 KiB
Go
310 lines
9.3 KiB
Go
// Package plugins provides HTTP handlers for plugin management
|
|
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/api/errors"
|
|
"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]any `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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
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: h.getPluginVersion(name, cfg),
|
|
}
|
|
|
|
// 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")
|
|
if err := json.NewEncoder(w).Encode(plugins); err != nil {
|
|
h.logger.Warn("failed to encode plugins response", "error", err)
|
|
}
|
|
}
|
|
|
|
// 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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
return
|
|
}
|
|
|
|
pluginName := r.PathValue("pluginName")
|
|
if pluginName == "" {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Missing plugin name", "")
|
|
return
|
|
}
|
|
|
|
cfg, ok := h.config[pluginName]
|
|
if !ok {
|
|
errors.WriteHTTPError(w, http.StatusNotFound, errors.CodeNotFound, "Plugin not found", "")
|
|
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")
|
|
if err := json.NewEncoder(w).Encode(info); err != nil {
|
|
h.logger.Warn("failed to encode plugin info", "error", err)
|
|
}
|
|
}
|
|
|
|
// 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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
return
|
|
}
|
|
|
|
pluginName := r.PathValue("pluginName")
|
|
if pluginName == "" {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Missing plugin name", "")
|
|
return
|
|
}
|
|
|
|
cfg, ok := h.config[pluginName]
|
|
if !ok {
|
|
errors.WriteHTTPError(w, http.StatusNotFound, errors.CodeNotFound, "Plugin not found", "")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
|
h.logger.Warn("failed to encode plugin config", "error", err)
|
|
}
|
|
}
|
|
|
|
// 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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
return
|
|
}
|
|
|
|
pluginName := r.PathValue("pluginName")
|
|
if pluginName == "" {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Missing plugin name", "")
|
|
return
|
|
}
|
|
|
|
var newConfig PluginConfig
|
|
if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Invalid request body", err.Error())
|
|
return
|
|
}
|
|
|
|
// Update config
|
|
h.config[pluginName] = newConfig
|
|
h.logger.Info("updated plugin config", "plugin", pluginName, "user", user.Name)
|
|
|
|
// Return updated plugin info with actual version
|
|
version := h.getPluginVersion(pluginName, newConfig)
|
|
info := PluginInfo{
|
|
Name: pluginName,
|
|
Enabled: newConfig.Enabled,
|
|
Mode: newConfig.Mode,
|
|
Status: "healthy",
|
|
Config: newConfig,
|
|
RequiresRestart: false,
|
|
Version: version,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(info); err != nil {
|
|
h.logger.Warn("failed to encode plugin info", "error", err)
|
|
}
|
|
}
|
|
|
|
// 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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
return
|
|
}
|
|
|
|
pluginName := r.PathValue("pluginName")
|
|
if pluginName == "" {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Missing plugin name", "")
|
|
return
|
|
}
|
|
|
|
cfg, ok := h.config[pluginName]
|
|
if !ok {
|
|
errors.WriteHTTPError(w, http.StatusNotFound, errors.CodeNotFound, "Plugin not found", "")
|
|
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") {
|
|
errors.WriteHTTPError(w, http.StatusForbidden, errors.CodePermissionDenied, "Insufficient permissions", "")
|
|
return
|
|
}
|
|
|
|
pluginName := r.PathValue("pluginName")
|
|
if pluginName == "" {
|
|
errors.WriteHTTPError(w, http.StatusBadRequest, errors.CodeBadRequest, "Missing plugin name", "")
|
|
return
|
|
}
|
|
|
|
cfg, ok := h.config[pluginName]
|
|
if !ok {
|
|
errors.WriteHTTPError(w, http.StatusNotFound, errors.CodeNotFound, "Plugin not found", "")
|
|
return
|
|
}
|
|
|
|
status := "healthy"
|
|
if !cfg.Enabled || cfg.Mode == "disabled" {
|
|
status = "stopped"
|
|
}
|
|
|
|
response := map[string]any{
|
|
"status": status,
|
|
"version": h.getPluginVersion(pluginName, cfg),
|
|
"timestamp": time.Now().UTC(),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
h.logger.Warn("failed to encode health response", "error", err)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if slices.Contains(user.Roles, "admin") {
|
|
return true
|
|
}
|
|
|
|
// Check specific permission
|
|
for perm, hasPerm := range user.Permissions {
|
|
if hasPerm && perm == permission {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getPluginVersion retrieves the version for a plugin
|
|
// TODO: Implement actual version query from plugin binary/container
|
|
func (h *Handler) getPluginVersion(pluginName string, cfg PluginConfig) string {
|
|
// In production, this should query the actual plugin binary/container
|
|
// For now, return a default version
|
|
_ = pluginName
|
|
_ = cfg
|
|
return "1.0.0"
|
|
}
|