Basic plugin system with centralized registry

This commit is contained in:
wpetit 2020-11-17 09:19:15 +01:00
parent fe90d81b5d
commit ad5c02bd80
13 changed files with 333 additions and 1 deletions

1
example/pluggable/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/bin

View File

@ -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

View File

@ -0,0 +1,5 @@
package hook
type Initializable interface {
HookInit() error
}

44
example/pluggable/main.go Normal file
View File

@ -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))
}
}

View File

@ -0,0 +1,6 @@
**/*.go
modd.conf
Makefile {
prep: make build
prep: make run
}

View File

@ -0,0 +1,11 @@
package main
import (
"context"
"gitlab.com/wpetit/goweb/plugin"
)
func RegisterPlugin(ctx context.Context) (plugin.Plugin, error) {
return &MyPlugin{}, nil
}

View File

@ -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
View File

@ -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
View File

@ -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=

16
plugin/error.go Normal file
View File

@ -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")
)

6
plugin/plugin.go Normal file
View File

@ -0,0 +1,6 @@
package plugin
type Plugin interface {
PluginName() string
PluginVersion() string
}

117
plugin/registry.go Normal file
View File

@ -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),
}
}

94
plugin/registry_test.go Normal file
View File

@ -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)
}
}