From ad5c02bd804f24eb92962bf8d45c8d3918ee7f39 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 17 Nov 2020 09:19:15 +0100 Subject: [PATCH] Basic plugin system with centralized registry --- example/pluggable/.gitignore | 1 + example/pluggable/Makefile | 11 +++ example/pluggable/hook/initializable.go | 5 + example/pluggable/main.go | 44 +++++++++ example/pluggable/modd.conf | 6 ++ example/pluggable/myplugin/main.go | 11 +++ example/pluggable/myplugin/plugin.go | 19 ++++ go.mod | 2 +- go.sum | 2 + plugin/error.go | 16 ++++ plugin/plugin.go | 6 ++ plugin/registry.go | 117 ++++++++++++++++++++++++ plugin/registry_test.go | 94 +++++++++++++++++++ 13 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 example/pluggable/.gitignore create mode 100644 example/pluggable/Makefile create mode 100644 example/pluggable/hook/initializable.go create mode 100644 example/pluggable/main.go create mode 100644 example/pluggable/modd.conf create mode 100644 example/pluggable/myplugin/main.go create mode 100644 example/pluggable/myplugin/plugin.go create mode 100644 plugin/error.go create mode 100644 plugin/plugin.go create mode 100644 plugin/registry.go create mode 100644 plugin/registry_test.go diff --git a/example/pluggable/.gitignore b/example/pluggable/.gitignore new file mode 100644 index 0000000..7447f89 --- /dev/null +++ b/example/pluggable/.gitignore @@ -0,0 +1 @@ +/bin \ No newline at end of file diff --git a/example/pluggable/Makefile b/example/pluggable/Makefile new file mode 100644 index 0000000..7c94b69 --- /dev/null +++ b/example/pluggable/Makefile @@ -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 \ No newline at end of file diff --git a/example/pluggable/hook/initializable.go b/example/pluggable/hook/initializable.go new file mode 100644 index 0000000..27698a4 --- /dev/null +++ b/example/pluggable/hook/initializable.go @@ -0,0 +1,5 @@ +package hook + +type Initializable interface { + HookInit() error +} diff --git a/example/pluggable/main.go b/example/pluggable/main.go new file mode 100644 index 0000000..88229a7 --- /dev/null +++ b/example/pluggable/main.go @@ -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)) + } +} diff --git a/example/pluggable/modd.conf b/example/pluggable/modd.conf new file mode 100644 index 0000000..f2ed2c2 --- /dev/null +++ b/example/pluggable/modd.conf @@ -0,0 +1,6 @@ +**/*.go +modd.conf +Makefile { + prep: make build + prep: make run +} \ No newline at end of file diff --git a/example/pluggable/myplugin/main.go b/example/pluggable/myplugin/main.go new file mode 100644 index 0000000..e62e0d4 --- /dev/null +++ b/example/pluggable/myplugin/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "context" + + "gitlab.com/wpetit/goweb/plugin" +) + +func RegisterPlugin(ctx context.Context) (plugin.Plugin, error) { + return &MyPlugin{}, nil +} diff --git a/example/pluggable/myplugin/plugin.go b/example/pluggable/myplugin/plugin.go new file mode 100644 index 0000000..aa946ee --- /dev/null +++ b/example/pluggable/myplugin/plugin.go @@ -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 +} diff --git a/go.mod b/go.mod index 2418211..9ba034b 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 8a15a96..305eac1 100644 --- a/go.sum +++ b/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= diff --git a/plugin/error.go b/plugin/error.go new file mode 100644 index 0000000..dc07c3d --- /dev/null +++ b/plugin/error.go @@ -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") +) diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..af5b7b6 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,6 @@ +package plugin + +type Plugin interface { + PluginName() string + PluginVersion() string +} diff --git a/plugin/registry.go b/plugin/registry.go new file mode 100644 index 0000000..502608f --- /dev/null +++ b/plugin/registry.go @@ -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), + } +} diff --git a/plugin/registry_test.go b/plugin/registry_test.go new file mode 100644 index 0000000..3a978f1 --- /dev/null +++ b/plugin/registry_test.go @@ -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) + } +}