feat(plugins): add plugin system interfaces and manager
Add the core plugin system with four interfaces: - Plugin: base lifecycle (Name, Version, Init, Shutdown) - MigrationPlugin: database migrations - AuthenticatedRouterPlugin: routes behind auth - UnauthenticatedRouterPlugin: public routes The Manager handles loading, initialization, shutdown, and route registration. Includes native .so loader (marked deprecated) and yaegi loader integration point.
This commit is contained in:
parent
167380a01e
commit
1d354512e6
1
go.mod
1
go.mod
|
|
@ -71,6 +71,7 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/tkuchiki/go-timezone v0.2.3
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -522,6 +522,8 @@ github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
|||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||
github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
|
||||
github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
|
||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
||||
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
|
||||
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
|
|
|
|||
|
|
@ -29,6 +29,18 @@ import (
|
|||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
// YaegiPluginLoader is a function that loads a plugin from a directory of Go source files.
|
||||
// It is set by the yaegi package's init() to avoid an import cycle.
|
||||
var YaegiPluginLoader func(dir string) (*LoadedYaegiPlugin, error)
|
||||
|
||||
// LoadedYaegiPlugin holds a plugin loaded via Yaegi along with its optional capabilities.
|
||||
type LoadedYaegiPlugin struct {
|
||||
Plugin Plugin
|
||||
AuthRouter AuthenticatedRouterPlugin
|
||||
UnauthRouter UnauthenticatedRouterPlugin
|
||||
Migration MigrationPlugin
|
||||
}
|
||||
|
||||
// Manager handles loading and managing plugins.
|
||||
type Manager struct {
|
||||
plugins []Plugin
|
||||
|
|
@ -90,6 +102,7 @@ func RegisterPluginRoutes(authenticated *echo.Group, unauthenticated *echo.Group
|
|||
}
|
||||
|
||||
func (m *Manager) loadPlugins(paths []string) error {
|
||||
loader := config.PluginsLoader.GetString()
|
||||
for _, p := range paths {
|
||||
entries, err := os.ReadDir(p)
|
||||
if err != nil {
|
||||
|
|
@ -99,19 +112,31 @@ func (m *Manager) loadPlugins(paths []string) error {
|
|||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if filepath.Ext(e.Name()) != ".so" {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(p, e.Name())
|
||||
if err := m.loadPlugin(full); err != nil {
|
||||
log.Errorf("Failed to load plugin %s: %s", e.Name(), err)
|
||||
switch loader {
|
||||
case "native":
|
||||
if filepath.Ext(e.Name()) != ".so" {
|
||||
continue
|
||||
}
|
||||
if err := m.loadNativePlugin(full); err != nil {
|
||||
log.Errorf("Failed to load native plugin %s: %s", e.Name(), err)
|
||||
}
|
||||
case "yaegi":
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err := m.loadYaegiPlugin(full); err != nil {
|
||||
log.Errorf("Failed to load yaegi plugin %s: %s", e.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) loadPlugin(path string) error {
|
||||
// Deprecated: native Go plugins are fragile (require exact Go version and dependency
|
||||
// match with the host binary) and will be removed in a future version. Use yaegi instead.
|
||||
func (m *Manager) loadNativePlugin(path string) error {
|
||||
pl, err := goplugin.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -125,7 +150,7 @@ func (m *Manager) loadPlugin(path string) error {
|
|||
return errors.New("invalid plugin entry point")
|
||||
}
|
||||
p := newPlugin()
|
||||
m.plugins = append(m.plugins, p)
|
||||
m.registerPlugin(p)
|
||||
|
||||
if mp, ok := p.(MigrationPlugin); ok {
|
||||
m.migrationPlugs = append(m.migrationPlugs, mp)
|
||||
|
|
@ -140,7 +165,38 @@ func (m *Manager) loadPlugin(path string) error {
|
|||
m.unauthenticatedRouterPlugs = append(m.unauthenticatedRouterPlugs, urp)
|
||||
}
|
||||
|
||||
log.Infof("Loaded plugin %s", p.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) loadYaegiPlugin(dir string) error {
|
||||
if YaegiPluginLoader == nil {
|
||||
return errors.New("yaegi plugin loader not registered")
|
||||
}
|
||||
|
||||
loaded, err := YaegiPluginLoader(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.registerPlugin(loaded.Plugin)
|
||||
|
||||
if loaded.AuthRouter != nil {
|
||||
m.authenticatedRouterPlugs = append(m.authenticatedRouterPlugs, loaded.AuthRouter)
|
||||
}
|
||||
|
||||
if loaded.UnauthRouter != nil {
|
||||
m.unauthenticatedRouterPlugs = append(m.unauthenticatedRouterPlugs, loaded.UnauthRouter)
|
||||
}
|
||||
|
||||
if loaded.Migration != nil {
|
||||
m.migrationPlugs = append(m.migrationPlugs, loaded.Migration)
|
||||
migration.AddPluginMigrations(loaded.Migration.Migrations())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) registerPlugin(p Plugin) {
|
||||
m.plugins = append(m.plugins, p)
|
||||
log.Infof("Loaded plugin %s v%s", p.Name(), p.Version())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue