Basic plugin system with centralized registry
This commit is contained in:
parent
fe90d81b5d
commit
ad5c02bd80
|
@ -0,0 +1 @@
|
|||
/bin
|
|
@ -0,0 +1,11 @@
|
|||
build: extension
|
||||
go build -o ./bin/app ./
|
||||
|
||||
extension:
|
||||
go build -o ./bin/myplugin.so -buildmode=plugin ./myplugin
|
||||
|
||||
watch:
|
||||
modd
|
||||
|
||||
run:
|
||||
./bin/app
|
|
@ -0,0 +1,5 @@
|
|||
package hook
|
||||
|
||||
type Initializable interface {
|
||||
HookInit() error
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/example/pluggable/hook"
|
||||
"gitlab.com/wpetit/goweb/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
reg := plugin.NewRegistry()
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := reg.LoadAll(ctx, "./bin/*.so")
|
||||
if err != nil {
|
||||
log.Fatal(errors.WithStack(err))
|
||||
}
|
||||
|
||||
for _, ext := range reg.Plugins() {
|
||||
log.Printf("Loaded plugin '%s', version '%s'", ext.PluginName(), ext.PluginVersion())
|
||||
}
|
||||
|
||||
// Iterate over plugins
|
||||
err = reg.Each(func(p plugin.Plugin) error {
|
||||
h, ok := p.(hook.Initializable)
|
||||
if !ok {
|
||||
// Skip non initializable plugins
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize plugin
|
||||
if err := h.HookInit(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(errors.WithStack(err))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
**/*.go
|
||||
modd.conf
|
||||
Makefile {
|
||||
prep: make build
|
||||
prep: make run
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/wpetit/goweb/plugin"
|
||||
)
|
||||
|
||||
func RegisterPlugin(ctx context.Context) (plugin.Plugin, error) {
|
||||
return &MyPlugin{}, nil
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import "log"
|
||||
|
||||
type MyPlugin struct{}
|
||||
|
||||
func (e *MyPlugin) PluginName() string {
|
||||
return "my.plugin"
|
||||
}
|
||||
|
||||
func (e *MyPlugin) PluginVersion() string {
|
||||
return "0.0.0"
|
||||
}
|
||||
|
||||
func (e *MyPlugin) HookInit() error {
|
||||
log.Println("MyPlugin initialized !")
|
||||
|
||||
return nil
|
||||
}
|
2
go.mod
2
go.mod
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -104,6 +104,8 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwU
|
|||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package plugin
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrInvalidRegisterFunc is returned when the plugin package
|
||||
// could not find the expected RegisterPlugin func in the loaded
|
||||
// plugin.
|
||||
ErrInvalidRegisterFunc = errors.New("invalid register func")
|
||||
// ErrInvalidPlugin is returned when a loaded plugin does
|
||||
// not match the expected interface.
|
||||
ErrInvalidPlugin = errors.New("invalid plugin")
|
||||
// ErrPluginNotFound is returned when the given plugin could
|
||||
// not be found in the registry.
|
||||
ErrPluginNotFound = errors.New("plugin not found")
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package plugin
|
||||
|
||||
type Plugin interface {
|
||||
PluginName() string
|
||||
PluginVersion() string
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
plugins map[string]Plugin
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *Registry) Add(plg Plugin) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
r.plugins[plg.PluginName()] = plg
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) (Plugin, error) {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
plg, exists := r.plugins[name]
|
||||
if !exists {
|
||||
return nil, errors.WithStack(ErrPluginNotFound)
|
||||
}
|
||||
|
||||
return plg, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Load(ctx context.Context, path string) (Plugin, error) {
|
||||
p, err := plugin.Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
registerFuncSymbol, err := p.Lookup("RegisterPlugin")
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
register, ok := registerFuncSymbol.(func(context.Context) (Plugin, error))
|
||||
if !ok {
|
||||
return nil, errors.WithStack(ErrInvalidRegisterFunc)
|
||||
}
|
||||
|
||||
plg, err := register(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if plg == nil {
|
||||
return nil, errors.WithStack(ErrInvalidPlugin)
|
||||
}
|
||||
|
||||
r.Add(plg)
|
||||
|
||||
return plg, nil
|
||||
}
|
||||
|
||||
func (r *Registry) LoadAll(ctx context.Context, pattern string) ([]Plugin, error) {
|
||||
extensions := make([]Plugin, 0)
|
||||
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, m := range matches {
|
||||
ext, err := r.Load(ctx, m)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
extensions = append(extensions, ext)
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Plugins() []Plugin {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
plugins := make([]Plugin, 0, len(r.plugins))
|
||||
for _, p := range r.plugins {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
type IteratorFunc func(plg Plugin) error
|
||||
|
||||
func (r *Registry) Each(iterator IteratorFunc) error {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
for _, p := range r.plugins {
|
||||
if err := iterator(p); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
plugins: make(map[string]Plugin),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package plugin_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/plugin"
|
||||
)
|
||||
|
||||
type testPlugin struct {
|
||||
name string
|
||||
version string
|
||||
}
|
||||
|
||||
func (p *testPlugin) PluginName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *testPlugin) PluginVersion() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
func (p *testPlugin) Foo() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
func TestRegistryEach(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reg := plugin.NewRegistry()
|
||||
|
||||
plugins := []*testPlugin{
|
||||
{"plugin.a", "0.0.0"},
|
||||
{"plugin.b", "0.0.1"},
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
reg.Add(p)
|
||||
}
|
||||
|
||||
total := 0
|
||||
|
||||
err := reg.Each(func(p plugin.Plugin) error {
|
||||
total++
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := len(plugins), total; e != g {
|
||||
t.Errorf("total: expected '%v', got '%v'", e, g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reg := plugin.NewRegistry()
|
||||
|
||||
plugins := []*testPlugin{
|
||||
{"plugin.a", "0.0.0"},
|
||||
{"plugin.b", "0.0.1"},
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
reg.Add(p)
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
plugin, err := reg.Get(p.name)
|
||||
if err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := p.name, plugin.PluginName(); e != g {
|
||||
t.Errorf("plugin.PluginName(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := p.version, plugin.PluginVersion(); e != g {
|
||||
t.Errorf("plugin.PluginVersion(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
}
|
||||
|
||||
p, err := reg.Get("plugin.c")
|
||||
if !errors.Is(err, plugin.ErrPluginNotFound) {
|
||||
t.Errorf("err: expected '%v', got '%v'", plugin.ErrPluginNotFound, err)
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
t.Errorf("reg.Get(\"plugin.c\"): expected '%v', got '%v'", nil, p)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue