first commit
This commit is contained in:
491
transports/bifrost-http/handlers/plugins.go
Normal file
491
transports/bifrost-http/handlers/plugins.go
Normal file
@@ -0,0 +1,491 @@
|
||||
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",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user