Files
bifrost/transports/bifrost-http/handlers/plugins.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

492 lines
16 KiB
Go

package handlers
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/fasthttp/router"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore"
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/plugins"
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
"github.com/valyala/fasthttp"
)
type PluginsLoader interface {
ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any, placement *schemas.PluginPlacement, order *int) error
RemovePlugin(ctx context.Context, name string) error
GetPluginStatus(ctx context.Context) map[string]schemas.PluginStatus
}
// PluginsHandler is the handler for the plugins API
type PluginsHandler struct {
configStore configstore.ConfigStore
pluginsLoader PluginsLoader
}
// NewPluginsHandler creates a new PluginsHandler
func NewPluginsHandler(pluginsLoader PluginsLoader, configStore configstore.ConfigStore) *PluginsHandler {
return &PluginsHandler{
pluginsLoader: pluginsLoader,
configStore: configStore,
}
}
// CreatePluginRequest is the request body for creating a plugin
type CreatePluginRequest struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Config map[string]any `json:"config"`
Path *string `json:"path"`
Placement *schemas.PluginPlacement `json:"placement,omitempty"`
Order *int `json:"order,omitempty"`
}
// UpdatePluginRequest is the request body for updating a plugin
type UpdatePluginRequest struct {
Enabled bool `json:"enabled"`
Path *string `json:"path"`
Config map[string]any `json:"config"`
Placement *schemas.PluginPlacement `json:"placement,omitempty"`
Order *int `json:"order,omitempty"`
}
// RegisterRoutes registers the routes for the PluginsHandler
func (h *PluginsHandler) RegisterRoutes(r *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
r.GET("/api/plugins", lib.ChainMiddlewares(h.getPlugins, middlewares...))
r.GET("/api/plugins/{name}", lib.ChainMiddlewares(h.getPlugin, middlewares...))
r.POST("/api/plugins", lib.ChainMiddlewares(h.createPlugin, middlewares...))
r.PUT("/api/plugins/{name}", lib.ChainMiddlewares(h.updatePlugin, middlewares...))
r.DELETE("/api/plugins/{name}", lib.ChainMiddlewares(h.deletePlugin, middlewares...))
}
type PluginResponse struct {
Name string `json:"name"`
ActualName string `json:"actualName"`
Enabled bool `json:"enabled"`
Config any `json:"config"`
IsCustom bool `json:"isCustom"`
Path *string `json:"path"`
Placement *schemas.PluginPlacement `json:"placement,omitempty"`
Order *int `json:"order,omitempty"`
Status schemas.PluginStatus `json:"status"`
}
// buildPluginResponse constructs a PluginResponse with status for a given TablePlugin.
func (h *PluginsHandler) buildPluginResponse(ctx context.Context, plugin *configstoreTables.TablePlugin) PluginResponse {
pluginStatus := schemas.PluginStatus{
Name: plugin.Name,
Status: schemas.PluginStatusUninitialized,
Logs: []string{},
}
if !plugin.Enabled {
pluginStatus.Status = schemas.PluginStatusDisabled
} else {
for _, status := range h.pluginsLoader.GetPluginStatus(ctx) {
if plugin.Name == status.Name {
pluginStatus = status
break
}
}
}
return PluginResponse{
Name: plugin.Name,
ActualName: pluginStatus.Name,
Enabled: plugin.Enabled,
Config: plugin.Config,
IsCustom: plugin.IsCustom,
Path: plugin.Path,
Placement: plugin.Placement,
Order: plugin.Order,
Status: pluginStatus,
}
}
// getPlugins gets all plugins
func (h *PluginsHandler) getPlugins(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
finalPlugins := []PluginResponse{}
for name, pluginStatus := range pluginStatus {
finalPlugins = append(finalPlugins, PluginResponse{
Name: pluginStatus.Name,
ActualName: name,
Enabled: true,
Config: map[string]any{},
IsCustom: true,
Path: nil,
Status: pluginStatus,
})
}
SendJSON(ctx, map[string]any{
"plugins": finalPlugins,
"count": len(finalPlugins),
})
return
}
plugins, err := h.configStore.GetPlugins(ctx)
if err != nil {
logger.Error("failed to get plugins: %v", err)
SendError(ctx, 500, "Failed to retrieve plugins")
return
}
// Fetching status
pluginStatuses := h.pluginsLoader.GetPluginStatus(ctx)
// Creating ephemeral struct for the plugins
finalPlugins := []PluginResponse{}
// Iterating over plugin status to get the plugin info
for _, plugin := range plugins {
pluginStatus := schemas.PluginStatus{
Name: plugin.Name,
Status: schemas.PluginStatusUninitialized,
Logs: []string{},
}
if !plugin.Enabled {
pluginStatus.Status = schemas.PluginStatusDisabled
}
for _, status := range pluginStatuses {
if plugin.Name == status.Name {
pluginStatus = status
break
}
}
finalPlugins = append(finalPlugins, PluginResponse{
Name: plugin.Name,
ActualName: pluginStatus.Name,
Enabled: plugin.Enabled,
Config: plugin.Config,
IsCustom: plugin.IsCustom,
Path: plugin.Path,
Placement: plugin.Placement,
Order: plugin.Order,
Status: pluginStatus,
})
}
// Creating ephemeral struct
SendJSON(ctx, map[string]any{
"plugins": finalPlugins,
"count": len(finalPlugins),
})
}
// getPlugin gets a plugin by name
func (h *PluginsHandler) getPlugin(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
pluginInfo := PluginResponse{}
for name, pluginStatus := range pluginStatus {
if pluginStatus.Name == ctx.UserValue("name") {
pluginInfo = PluginResponse{
Name: pluginStatus.Name,
ActualName: name,
Enabled: true,
Config: map[string]any{},
IsCustom: true,
Path: nil,
Status: pluginStatus,
}
break
}
}
SendJSON(ctx, pluginInfo)
return
}
// Safely validate the "name" parameter
nameValue := ctx.UserValue("name")
if nameValue == nil {
logger.Warn("missing required 'name' parameter in request")
SendError(ctx, 400, "Missing required 'name' parameter")
return
}
name, ok := nameValue.(string)
if !ok {
logger.Warn("invalid 'name' parameter type, expected string but got %T", nameValue)
SendError(ctx, 400, "Invalid 'name' parameter type, expected string")
return
}
if name == "" {
logger.Warn("empty 'name' parameter provided")
SendError(ctx, 400, "Empty 'name' parameter not allowed")
return
}
plugin, err := h.configStore.GetPlugin(ctx, name)
if err != nil {
if errors.Is(err, configstore.ErrNotFound) {
SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
return
}
logger.Error("failed to get plugin: %v", err)
SendError(ctx, 500, "Failed to retrieve plugin")
return
}
SendJSON(ctx, plugin)
}
// createPlugin creates a new plugin
func (h *PluginsHandler) createPlugin(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
SendError(ctx, 400, "Plugins creation is not supported when configstore is disabled")
return
}
var request CreatePluginRequest
if err := json.Unmarshal(ctx.PostBody(), &request); err != nil {
logger.Error("failed to unmarshal create plugin request: %v", err)
SendError(ctx, 400, "Invalid request body")
return
}
// Validate required fields
if request.Name == "" {
SendError(ctx, fasthttp.StatusBadRequest, "Plugin name is required")
return
}
// Validate placement value
if request.Placement != nil && *request.Placement != "" &&
*request.Placement != schemas.PluginPlacementPreBuiltin &&
*request.Placement != schemas.PluginPlacementPostBuiltin {
SendError(ctx, fasthttp.StatusBadRequest, "Invalid placement value. Must be 'pre_builtin' or 'post_builtin'")
return
}
if request.Placement != nil && *request.Placement == "" {
request.Placement = nil
}
// Normalize empty path to nil (treat empty string as built-in plugin)
if request.Path != nil && *request.Path == "" {
request.Path = nil
}
// Check if plugin already exists
existingPlugin, err := h.configStore.GetPlugin(ctx, request.Name)
if err == nil && existingPlugin != nil {
SendError(ctx, fasthttp.StatusConflict, "Plugin already exists")
return
}
// Determine if this is a built-in or custom plugin
isBuiltin := lib.IsBuiltinPlugin(request.Name)
// Built-in plugins should not have a path
if isBuiltin && request.Path != nil {
request.Path = nil
}
// Create DB entry first to avoid orphaned in-memory state if DB write fails
if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
Name: request.Name,
Enabled: request.Enabled,
Config: request.Config,
Path: request.Path,
IsCustom: !isBuiltin,
Placement: request.Placement,
Order: request.Order,
}); err != nil {
logger.Error("failed to create plugin: %v", err)
SendError(ctx, 500, "Failed to create plugin")
return
}
// Reload the plugin into memory if it's enabled
if request.Enabled {
if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config, request.Placement, request.Order); err != nil {
logger.Error("failed to load plugin: %v", err)
if rbErr := h.configStore.DeletePlugin(ctx, request.Name); rbErr != nil {
logger.Error("failed to rollback plugin creation: %v", rbErr)
}
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
return
}
}
plugin, err := h.configStore.GetPlugin(ctx, request.Name)
if err != nil {
logger.Error("failed to get plugin: %v", err)
SendError(ctx, 500, "Failed to retrieve plugin")
return
}
ctx.SetStatusCode(fasthttp.StatusCreated)
SendJSON(ctx, map[string]any{
"message": "Plugin created successfully",
"plugin": h.buildPluginResponse(ctx, plugin),
})
}
// updatePlugin updates an existing plugin
func (h *PluginsHandler) updatePlugin(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
SendError(ctx, 400, "Plugins update is not supported when configstore is disabled")
return
}
// Safely validate the "name" parameter
nameValue := ctx.UserValue("name")
if nameValue == nil {
logger.Warn("missing required 'name' parameter in update plugin request")
SendError(ctx, 400, "Missing required 'name' parameter")
return
}
name, ok := nameValue.(string)
if !ok {
logger.Warn("invalid 'name' parameter type in update plugin request, expected string but got %T", nameValue)
SendError(ctx, 400, "Invalid 'name' parameter type, expected string")
return
}
if name == "" {
logger.Warn("empty 'name' parameter provided in update plugin request")
SendError(ctx, 400, "Empty 'name' parameter not allowed")
return
}
var plugin *configstoreTables.TablePlugin
var err error
// Check if plugin exists
_, err = h.configStore.GetPlugin(ctx, name)
if err != nil {
// If doesn't exist, create it
if errors.Is(err, configstore.ErrNotFound) {
plugin = &configstoreTables.TablePlugin{
Name: name,
Enabled: false,
Config: map[string]any{},
Path: nil,
IsCustom: false,
}
if err := h.configStore.CreatePlugin(ctx, plugin); err != nil {
logger.Error("failed to create plugin: %v", err)
SendError(ctx, 500, "Failed to create plugin")
return
}
} else {
logger.Error("failed to get plugin: %v", err)
SendError(ctx, 500, "Failed to update plugin")
return
}
}
// Unmarshalling the request body
var request UpdatePluginRequest
if err := json.Unmarshal(ctx.PostBody(), &request); err != nil {
logger.Error("failed to unmarshal update plugin request: %v", err)
SendError(ctx, 400, "Invalid request body")
return
}
// Validate placement value
if request.Placement != nil && *request.Placement != "" &&
*request.Placement != schemas.PluginPlacementPreBuiltin &&
*request.Placement != schemas.PluginPlacementPostBuiltin {
SendError(ctx, fasthttp.StatusBadRequest, "Invalid placement value. Must be 'pre_builtin' or 'post_builtin'")
return
}
if request.Placement != nil && *request.Placement == "" {
request.Placement = nil
}
// Normalize empty path to nil (treat empty string as built-in plugin)
if request.Path != nil && *request.Path == "" {
request.Path = nil
}
// Determine if this is a built-in plugin
isBuiltin := lib.IsBuiltinPlugin(name)
// Built-in plugins should not have a path
if isBuiltin && request.Path != nil {
request.Path = nil
}
// Updating the plugin
if err := h.configStore.UpdatePlugin(ctx, &configstoreTables.TablePlugin{
Name: name,
Enabled: request.Enabled,
Config: request.Config,
Path: request.Path,
IsCustom: !isBuiltin,
Placement: request.Placement,
Order: request.Order,
}); err != nil {
logger.Error("failed to update plugin: %v", err)
SendError(ctx, 500, "Failed to update plugin")
return
}
plugin, err = h.configStore.GetPlugin(ctx, name)
if err != nil {
if errors.Is(err, configstore.ErrNotFound) {
SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
return
}
logger.Error("failed to get plugin: %v", err)
SendError(ctx, 500, "Failed to retrieve plugin")
return
}
// We reload the plugin if its enabled, otherwise we stop it
if request.Enabled {
if err := h.pluginsLoader.ReloadPlugin(ctx, name, request.Path, request.Config, request.Placement, request.Order); err != nil {
logger.Error("failed to load plugin: %v", err)
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to load: %v", err))
return
}
} else {
ctx.SetUserValue(PluginDisabledKey, true)
if err := h.pluginsLoader.RemovePlugin(ctx, name); err != nil {
if !errors.Is(err, plugins.ErrPluginNotFound) {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to stop: %v", err))
return
}
// If not found then we don't need to do anything
}
}
SendJSON(ctx, map[string]interface{}{
"message": "Plugin updated successfully",
"plugin": h.buildPluginResponse(ctx, plugin),
})
}
// deletePlugin deletes an existing plugin
func (h *PluginsHandler) deletePlugin(ctx *fasthttp.RequestCtx) {
if h.configStore == nil {
SendError(ctx, 400, "Plugins deletion is not supported when configstore is disabled")
return
}
// Safely validate the "name" parameter
nameValue := ctx.UserValue("name")
if nameValue == nil {
logger.Warn("missing required 'name' parameter in delete plugin request")
SendError(ctx, 400, "Missing required 'name' parameter")
return
}
name, ok := nameValue.(string)
if !ok {
logger.Warn("invalid 'name' parameter type in delete plugin request, expected string but got %T", nameValue)
SendError(ctx, 400, "Invalid 'name' parameter type, expected string")
return
}
if name == "" {
logger.Warn("empty 'name' parameter provided in delete plugin request")
SendError(ctx, 400, "Empty 'name' parameter not allowed")
return
}
if err := h.configStore.DeletePlugin(ctx, name); err != nil {
if errors.Is(err, configstore.ErrNotFound) {
SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
return
}
logger.Error("failed to delete plugin: %v", err)
SendError(ctx, 500, "Failed to delete plugin")
return
}
if err := h.pluginsLoader.RemovePlugin(ctx, name); err != nil {
if !errors.Is(err, plugins.ErrPluginNotFound) {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin deleted in database but failed to stop: %v", err))
return
}
}
SendJSON(ctx, map[string]interface{}{
"message": "Plugin deleted successfully",
})
}