middleware-manager/api/handlers/plugin_handler.go
hhftechnologies 7ad07565d7
Some checks are pending
Build and Push Docker Image / build-and-push (push) Waiting to run
bugfixes-ui-update
2026-01-14 17:11:32 +05:30

605 lines
18 KiB
Go

package handlers
import (
"context"
"database/sql"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/hhftechnology/middleware-manager/models"
"github.com/hhftechnology/middleware-manager/services"
"gopkg.in/yaml.v3"
)
// PluginHandler handles plugin-related requests
type PluginHandler struct {
DB *sql.DB
TraefikStaticConfigPath string
ConfigManager *services.ConfigManager
pluginFetcher *services.PluginFetcher
}
// NewPluginHandler creates a new plugin handler
func NewPluginHandler(db *sql.DB, traefikStaticConfigPath string, configManager *services.ConfigManager) *PluginHandler {
handler := &PluginHandler{
DB: db,
TraefikStaticConfigPath: traefikStaticConfigPath,
ConfigManager: configManager,
}
// Initialize plugin fetcher with data source config
if configManager != nil {
dsConfig, err := configManager.GetActiveDataSourceConfig()
if err == nil && dsConfig.Type == models.TraefikAPI {
handler.pluginFetcher = services.NewPluginFetcher(dsConfig)
}
}
return handler
}
// RefreshPluginFetcher refreshes the plugin fetcher with current config
func (h *PluginHandler) RefreshPluginFetcher() error {
if h.ConfigManager == nil {
return fmt.Errorf("config manager not initialized")
}
dsConfig, err := h.ConfigManager.GetActiveDataSourceConfig()
if err != nil {
return fmt.Errorf("failed to get data source config: %w", err)
}
// Only create fetcher for Traefik API type
if dsConfig.Type == models.TraefikAPI {
h.pluginFetcher = services.NewPluginFetcher(dsConfig)
} else {
// For Pangolin, we can also fetch plugins from the config
h.pluginFetcher = services.NewPluginFetcher(dsConfig)
}
return nil
}
// GetPlugins fetches plugins from Traefik API
func (h *PluginHandler) GetPlugins(c *gin.Context) {
// Refresh fetcher if needed
if h.pluginFetcher == nil {
if err := h.RefreshPluginFetcher(); err != nil {
log.Printf("Warning: Failed to refresh plugin fetcher: %v", err)
}
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
var plugins []models.PluginResponse
// Try to fetch from Traefik API
if h.pluginFetcher != nil {
var err error
plugins, err = h.pluginFetcher.FetchPlugins(ctx)
if err != nil {
log.Printf("Error fetching plugins from Traefik API: %v", err)
// Fall back to local config check
plugins = h.getPluginsFromLocalConfig()
}
} else {
// No fetcher available, check local config
plugins = h.getPluginsFromLocalConfig()
}
// Merge with local config to get installation status
localPlugins, err := h.getLocalInstalledPlugins()
if err != nil {
log.Printf("Warning: Could not read local Traefik config: %v", err)
} else {
plugins = h.mergeWithLocalConfig(plugins, localPlugins)
}
c.JSON(http.StatusOK, plugins)
}
// getPluginsFromLocalConfig reads plugins from local Traefik static config
func (h *PluginHandler) getPluginsFromLocalConfig() []models.PluginResponse {
plugins := []models.PluginResponse{}
localPlugins, err := h.getLocalInstalledPlugins()
if err != nil {
return plugins
}
for key, config := range localPlugins {
plugin := models.PluginResponse{
Name: key,
Type: "middleware",
Status: "configured",
IsInstalled: true,
}
if moduleName, ok := config["moduleName"].(string); ok {
plugin.ModuleName = moduleName
} else {
plugin.ModuleName = key
}
if version, ok := config["version"].(string); ok {
plugin.Version = version
plugin.InstalledVersion = version
}
plugins = append(plugins, plugin)
}
return plugins
}
// mergeWithLocalConfig merges API plugins with local config info
func (h *PluginHandler) mergeWithLocalConfig(apiPlugins []models.PluginResponse, localPlugins map[string]map[string]interface{}) []models.PluginResponse {
// Create a map for quick lookup by name
apiPluginMap := make(map[string]*models.PluginResponse)
for i := range apiPlugins {
apiPluginMap[apiPlugins[i].Name] = &apiPlugins[i]
}
// Also track which plugins are confirmed enabled (from API with status=enabled)
enabledPlugins := make(map[string]bool)
for _, plugin := range apiPlugins {
if plugin.Status == "enabled" {
enabledPlugins[plugin.Name] = true
}
}
// Update API plugins with local config info
for key, localConfig := range localPlugins {
if plugin, exists := apiPluginMap[key]; exists {
plugin.IsInstalled = true
if version, ok := localConfig["version"].(string); ok {
plugin.InstalledVersion = version
}
} else {
// Plugin exists in local config but not directly in API
// Check if it might be enabled under a different detection method
// If plugin is in enabledPlugins map, it's actually running
newPlugin := models.PluginResponse{
Name: key,
Type: "middleware",
IsInstalled: true,
}
// Check if this plugin is actually enabled (might be detected via middleware usage)
if enabledPlugins[key] {
newPlugin.Status = "enabled"
} else {
// Plugin is configured but not detected as loaded - needs restart
newPlugin.Status = "not_loaded"
}
if moduleName, ok := localConfig["moduleName"].(string); ok {
newPlugin.ModuleName = moduleName
} else {
newPlugin.ModuleName = key
}
if version, ok := localConfig["version"].(string); ok {
newPlugin.Version = version
newPlugin.InstalledVersion = version
}
apiPlugins = append(apiPlugins, newPlugin)
}
}
return apiPlugins
}
// getLocalInstalledPlugins reads the Traefik static config and returns installed plugins
func (h *PluginHandler) getLocalInstalledPlugins() (map[string]map[string]interface{}, error) {
if h.TraefikStaticConfigPath == "" {
return nil, fmt.Errorf("Traefik static configuration path is not set")
}
cleanPath := filepath.Clean(h.TraefikStaticConfigPath)
config, err := h.readTraefikStaticConfig(cleanPath)
if err != nil {
if os.IsNotExist(err) {
return make(map[string]map[string]interface{}), nil
}
return nil, fmt.Errorf("reading traefik static config: %w", err)
}
installedPlugins := make(map[string]map[string]interface{})
if experimentalSection, ok := config["experimental"].(map[string]interface{}); ok {
if pluginsConfig, ok := experimentalSection["plugins"].(map[string]interface{}); ok {
for key, pluginData := range pluginsConfig {
if pluginEntry, okData := pluginData.(map[string]interface{}); okData {
installedPlugins[key] = pluginEntry
}
}
}
}
return installedPlugins, nil
}
// InstallPluginBody defines the expected request body for installing a plugin
type InstallPluginBody struct {
ModuleName string `json:"moduleName" binding:"required"`
Version string `json:"version,omitempty"`
}
// InstallPlugin adds a plugin to the Traefik static configuration
func (h *PluginHandler) InstallPlugin(c *gin.Context) {
var body InstallPluginBody
if err := c.ShouldBindJSON(&body); err != nil {
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
return
}
if h.TraefikStaticConfigPath == "" {
ResponseWithError(c, http.StatusInternalServerError, "Traefik static configuration file path is not configured. Please set it in settings.")
return
}
cleanPath := filepath.Clean(h.TraefikStaticConfigPath)
traefikStaticConfig, err := h.readTraefikStaticConfig(cleanPath)
if err != nil {
if os.IsNotExist(err) {
traefikStaticConfig = make(map[string]interface{})
LogInfo(fmt.Sprintf("Traefik static config file not found at %s, will create a new one.", cleanPath))
} else {
LogError(fmt.Sprintf("reading traefik static config file %s", cleanPath), err)
ResponseWithError(c, http.StatusInternalServerError, "Failed to read Traefik static configuration file.")
return
}
}
experimentalSection, ok := traefikStaticConfig["experimental"].(map[string]interface{})
if !ok {
if traefikStaticConfig["experimental"] != nil {
ResponseWithError(c, http.StatusInternalServerError, "Traefik static configuration 'experimental' section has an unexpected format.")
return
}
experimentalSection = make(map[string]interface{})
traefikStaticConfig["experimental"] = experimentalSection
}
pluginsConfig, ok := experimentalSection["plugins"].(map[string]interface{})
if !ok {
if experimentalSection["plugins"] != nil {
ResponseWithError(c, http.StatusInternalServerError, "Traefik static configuration 'plugins' section has an unexpected format.")
return
}
pluginsConfig = make(map[string]interface{})
experimentalSection["plugins"] = pluginsConfig
}
pluginKey := getPluginKey(body.ModuleName)
if pluginKey == "" {
ResponseWithError(c, http.StatusBadRequest, "Invalid plugin module name, could not derive a configuration key.")
return
}
pluginEntry := map[string]interface{}{
"moduleName": body.ModuleName,
}
if body.Version != "" {
pluginEntry["version"] = body.Version
}
pluginsConfig[pluginKey] = pluginEntry
if err := h.writeTraefikStaticConfig(cleanPath, traefikStaticConfig); err != nil {
LogError("writing traefik static config", err)
ResponseWithError(c, http.StatusInternalServerError, err.Error())
return
}
// Invalidate plugin cache
if h.pluginFetcher != nil {
h.pluginFetcher.InvalidateCache()
}
log.Printf("Successfully configured plugin '%s' (key: '%s') in %s", body.ModuleName, pluginKey, cleanPath)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Plugin %s configured. A Traefik restart is required to load the plugin.", body.ModuleName),
"pluginKey": pluginKey,
"moduleName": body.ModuleName,
"version": body.Version,
})
}
// RemovePluginBody defines the expected request body for removing a plugin
type RemovePluginBody struct {
ModuleName string `json:"moduleName" binding:"required"`
}
// RemovePlugin removes a plugin from the Traefik static configuration
func (h *PluginHandler) RemovePlugin(c *gin.Context) {
var body RemovePluginBody
if err := c.ShouldBindJSON(&body); err != nil {
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
return
}
if h.TraefikStaticConfigPath == "" {
ResponseWithError(c, http.StatusInternalServerError, "Traefik static configuration file path is not configured.")
return
}
cleanPath := filepath.Clean(h.TraefikStaticConfigPath)
traefikStaticConfig, err := h.readTraefikStaticConfig(cleanPath)
if err != nil {
if os.IsNotExist(err) {
ResponseWithError(c, http.StatusNotFound, fmt.Sprintf("Traefik static configuration file not found at: %s", cleanPath))
} else {
LogError(fmt.Sprintf("reading traefik static config file %s for removal", cleanPath), err)
ResponseWithError(c, http.StatusInternalServerError, "Failed to read Traefik static configuration file.")
}
return
}
pluginKey := getPluginKey(body.ModuleName)
pluginRemoved := false
if experimentalSection, ok := traefikStaticConfig["experimental"].(map[string]interface{}); ok {
if pluginsConfig, ok := experimentalSection["plugins"].(map[string]interface{}); ok {
if _, exists := pluginsConfig[pluginKey]; exists {
delete(pluginsConfig, pluginKey)
pluginRemoved = true
if len(pluginsConfig) == 0 {
delete(experimentalSection, "plugins")
}
if len(experimentalSection) == 0 {
delete(traefikStaticConfig, "experimental")
}
}
}
}
if !pluginRemoved {
LogInfo(fmt.Sprintf("Plugin '%s' (key: '%s') not found in Traefik static configuration.", body.ModuleName, pluginKey))
ResponseWithError(c, http.StatusNotFound, fmt.Sprintf("Plugin '%s' not found in configuration.", body.ModuleName))
return
}
if err := h.writeTraefikStaticConfig(cleanPath, traefikStaticConfig); err != nil {
LogError("writing traefik static config after removal", err)
ResponseWithError(c, http.StatusInternalServerError, err.Error())
return
}
// Invalidate plugin cache
if h.pluginFetcher != nil {
h.pluginFetcher.InvalidateCache()
}
log.Printf("Successfully removed plugin '%s' (key: '%s') from %s", body.ModuleName, pluginKey, cleanPath)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Plugin %s removed. A Traefik restart is required for changes to take effect.", body.ModuleName),
"pluginKey": pluginKey,
"moduleName": body.ModuleName,
})
}
// GetTraefikStaticConfigPath returns the current Traefik static config path
func (h *PluginHandler) GetTraefikStaticConfigPath(c *gin.Context) {
if h.TraefikStaticConfigPath == "" {
c.JSON(http.StatusOK, gin.H{
"path": "",
"message": "Traefik static config path is not currently set.",
})
return
}
c.JSON(http.StatusOK, gin.H{"path": h.TraefikStaticConfigPath})
}
// UpdatePathBody defines the request body for updating config path
type UpdatePathBody struct {
Path string `json:"path" binding:"required"`
}
// UpdateTraefikStaticConfigPath updates the Traefik static config path
func (h *PluginHandler) UpdateTraefikStaticConfigPath(c *gin.Context) {
var body UpdatePathBody
if err := c.ShouldBindJSON(&body); err != nil {
ResponseWithError(c, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
return
}
cleanPath := filepath.Clean(body.Path)
if cleanPath == "" || cleanPath == "." || cleanPath == "/" || strings.HasSuffix(cleanPath, "/") {
ResponseWithError(c, http.StatusBadRequest, "Invalid configuration path provided.")
return
}
oldPath := h.TraefikStaticConfigPath
h.TraefikStaticConfigPath = cleanPath
log.Printf("Traefik static config path updated from '%s' to: '%s'", oldPath, cleanPath)
c.JSON(http.StatusOK, gin.H{
"message": "Traefik static config path updated.",
"path": cleanPath,
})
}
// GetPluginUsage returns usage information for a specific plugin
func (h *PluginHandler) GetPluginUsage(c *gin.Context) {
pluginName := c.Param("name")
if pluginName == "" {
ResponseWithError(c, http.StatusBadRequest, "Plugin name is required")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
if h.pluginFetcher == nil {
if err := h.RefreshPluginFetcher(); err != nil {
ResponseWithError(c, http.StatusInternalServerError, "Failed to initialize plugin fetcher")
return
}
}
plugins, err := h.pluginFetcher.FetchPlugins(ctx)
if err != nil {
ResponseWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch plugins: %v", err))
return
}
for _, plugin := range plugins {
if plugin.Name == pluginName {
c.JSON(http.StatusOK, gin.H{
"name": plugin.Name,
"usageCount": plugin.UsageCount,
"usedBy": plugin.UsedBy,
"status": plugin.Status,
})
return
}
}
ResponseWithError(c, http.StatusNotFound, fmt.Sprintf("Plugin '%s' not found", pluginName))
}
// Helper functions
func (h *PluginHandler) readTraefikStaticConfig(filePath string) (map[string]interface{}, error) {
yamlFile, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var config map[string]interface{}
if err := yaml.Unmarshal(yamlFile, &config); err != nil {
return nil, fmt.Errorf("failed to parse Traefik static configuration: %w", err)
}
return config, nil
}
func (h *PluginHandler) writeTraefikStaticConfig(filePath string, config map[string]interface{}) error {
updatedYaml, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to prepare updated Traefik configuration: %w", err)
}
// Create backup
backupPath := filePath + ".bak." + time.Now().Format("20060102150405")
if err := copyFile(filePath, backupPath); err != nil {
LogInfo(fmt.Sprintf("Warning: Could not create backup: %v", err))
} else {
LogInfo(fmt.Sprintf("Created backup at %s", backupPath))
}
// Write to temp file first
tempFile := filePath + ".tmp"
if err := os.WriteFile(tempFile, updatedYaml, 0644); err != nil {
_ = os.Remove(tempFile)
return fmt.Errorf("failed to write configuration: %w", err)
}
// Atomic rename
if err := os.Rename(tempFile, filePath); err != nil {
return fmt.Errorf("failed to finalize configuration: %w", err)
}
return nil
}
func getPluginKey(moduleName string) string {
if moduleName == "" {
return ""
}
parts := strings.Split(moduleName, "/")
if len(parts) > 0 {
lastKeyPart := parts[len(parts)-1]
lastKeyPart = strings.Split(lastKeyPart, "@")[0]
lastKeyPart = strings.TrimSuffix(lastKeyPart, ".git")
lastKeyPart = strings.TrimSuffix(lastKeyPart, "-plugin")
return strings.ToLower(lastKeyPart)
}
key := strings.Split(moduleName, "@")[0]
key = strings.TrimSuffix(key, ".git")
key = strings.TrimSuffix(key, "-plugin")
return strings.ToLower(key)
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("could not open source file %s: %w", src, err)
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("could not create destination file %s: %w", dst, err)
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
if err != nil {
return fmt.Errorf("could not copy content: %w", err)
}
return destinationFile.Sync()
}
func LogInfo(message string) {
log.Println("INFO:", message)
}
// GetPluginCatalogue fetches the full plugin catalogue from plugins.traefik.io
func (h *PluginHandler) GetPluginCatalogue(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Second)
defer cancel()
plugins, err := services.FetchPluginCatalogue(ctx)
if err != nil {
log.Printf("Error fetching plugin catalogue: %v", err)
ResponseWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch plugin catalogue: %v", err))
return
}
// Also get installed plugins to mark them
installedPlugins, _ := h.getLocalInstalledPlugins()
// Mark installed plugins in the catalogue
result := make([]map[string]interface{}, len(plugins))
for i, plugin := range plugins {
entry := map[string]interface{}{
"id": plugin.ID,
"name": plugin.Name,
"displayName": plugin.DisplayName,
"author": plugin.Author,
"type": plugin.Type,
"import": plugin.Import,
"summary": plugin.Summary,
"iconUrl": plugin.IconURL,
"bannerUrl": plugin.BannerURL,
"latestVersion": plugin.LatestVersion,
"versions": plugin.Versions,
"stars": plugin.Stars,
"snippet": plugin.Snippet,
"isInstalled": false,
}
// Check if plugin is installed by looking up the module name in local plugins
pluginKey := getPluginKey(plugin.Import)
if _, installed := installedPlugins[pluginKey]; installed {
entry["isInstalled"] = true
}
result[i] = entry
}
c.JSON(http.StatusOK, result)
}