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/gorilla/sessions v1.2.0
|
||||||
github.com/leodido/go-urn v1.1.0 // indirect
|
github.com/leodido/go-urn v1.1.0 // indirect
|
||||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
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/assert.v1 v1.2.1 // indirect
|
||||||
gopkg.in/go-playground/validator.v9 v9.29.1
|
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.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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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=
|
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