492 lines
16 KiB
Go
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",
|
|
})
|
|
}
|