Browse Source

Basic plugin system with centralized registry

plugin
wpetit 1 year ago
parent
commit
ad5c02bd80
13 changed files with 333 additions and 1 deletions
  1. +1
    -0
      example/pluggable/.gitignore
  2. +11
    -0
      example/pluggable/Makefile
  3. +5
    -0
      example/pluggable/hook/initializable.go
  4. +44
    -0
      example/pluggable/main.go
  5. +6
    -0
      example/pluggable/modd.conf
  6. +11
    -0
      example/pluggable/myplugin/main.go
  7. +19
    -0
      example/pluggable/myplugin/plugin.go
  8. +1
    -1
      go.mod
  9. +2
    -0
      go.sum
  10. +16
    -0
      plugin/error.go
  11. +6
    -0
      plugin/plugin.go
  12. +117
    -0
      plugin/registry.go
  13. +94
    -0
      plugin/registry_test.go

+ 1
- 0
example/pluggable/.gitignore View File

@ -0,0 +1 @@
/bin

+ 11
- 0
example/pluggable/Makefile 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

+ 5
- 0
example/pluggable/hook/initializable.go View File

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

+ 44
- 0
example/pluggable/main.go 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))
}
}

+ 6
- 0
example/pluggable/modd.conf View File

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

+ 11
- 0
example/pluggable/myplugin/main.go 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
}

+ 19
- 0
example/pluggable/myplugin/plugin.go 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
}

+ 1
- 1
go.mod View File

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


+ 16
- 0
plugin/error.go 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
- 0
plugin/plugin.go View File

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

+ 117
- 0
plugin/registry.go 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
- 0
plugin/registry_test.go 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)
}
}

Loading…
Cancel
Save