From 402491b7e52c4d836c1274976cdb387852cfd17b Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 11 Sep 2017 10:02:02 -0500 Subject: PLT-7407: Back-end plugins (#7409) * tie back-end plugins together * fix comment typo * add tests and a bit of polish * tests and polish * add test, don't let backend executable paths escape the plugin directory --- plugin/pluginenv/environment.go | 59 +++++++++++++++++++++++++----- plugin/pluginenv/environment_test.go | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) (limited to 'plugin/pluginenv') diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go index a943b24c6..e4a7f1b3b 100644 --- a/plugin/pluginenv/environment.go +++ b/plugin/pluginenv/environment.go @@ -4,6 +4,7 @@ package pluginenv import ( "fmt" "io/ioutil" + "net/http" "sync" "github.com/pkg/errors" @@ -27,7 +28,7 @@ type Environment struct { apiProvider APIProviderFunc supervisorProvider SupervisorProviderFunc activePlugins map[string]ActivePlugin - mutex sync.Mutex + mutex sync.RWMutex } type Option func(*Environment) @@ -61,15 +62,13 @@ func (env *Environment) SearchPath() string { // Returns a list of all plugins found within the environment. func (env *Environment) Plugins() ([]*model.BundleInfo, error) { - env.mutex.Lock() - defer env.mutex.Unlock() return ScanSearchPath(env.searchPath) } // Returns a list of all currently active plugins within the environment. func (env *Environment) ActivePlugins() ([]*model.BundleInfo, error) { - env.mutex.Lock() - defer env.mutex.Unlock() + env.mutex.RLock() + defer env.mutex.RUnlock() activePlugins := []*model.BundleInfo{} for _, p := range env.activePlugins { @@ -81,8 +80,8 @@ func (env *Environment) ActivePlugins() ([]*model.BundleInfo, error) { // Returns the ids of the currently active plugins. func (env *Environment) ActivePluginIds() (ids []string) { - env.mutex.Lock() - defer env.mutex.Unlock() + env.mutex.RLock() + defer env.mutex.RUnlock() for id := range env.activePlugins { ids = append(ids, id) @@ -200,13 +199,55 @@ func (env *Environment) Shutdown() (errs []error) { for _, activePlugin := range env.activePlugins { if activePlugin.Supervisor != nil { if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil { - errs = append(errs, err) + errs = append(errs, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id)) } if err := activePlugin.Supervisor.Stop(); err != nil { - errs = append(errs, err) + errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id)) } } } env.activePlugins = make(map[string]ActivePlugin) return } + +type EnvironmentHooks struct { + env *Environment +} + +func (env *Environment) Hooks() *EnvironmentHooks { + return &EnvironmentHooks{env} +} + +// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors +// encountered will be returned. +func (h *EnvironmentHooks) OnConfigurationChange() (errs []error) { + h.env.mutex.RLock() + defer h.env.mutex.RUnlock() + for _, activePlugin := range h.env.activePlugins { + if activePlugin.Supervisor == nil { + continue + } + if err := activePlugin.Supervisor.Hooks().OnConfigurationChange(); err != nil { + errs = append(errs, errors.Wrapf(err, "OnConfigurationChange error for %v", activePlugin.BundleInfo.Manifest.Id)) + } + } + return +} + +// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a +// 404 not found. +// +// It expects the request's context to have a plugin_id set. +func (h *EnvironmentHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if id := r.Context().Value("plugin_id"); id != nil { + if idstr, ok := id.(string); ok { + h.env.mutex.RLock() + defer h.env.mutex.RUnlock() + if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil { + plugin.Supervisor.Hooks().ServeHTTP(w, r) + return + } + } + } + http.NotFound(w, r) +} diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go index e9d0820bb..f24ef8d3d 100644 --- a/plugin/pluginenv/environment_test.go +++ b/plugin/pluginenv/environment_test.go @@ -1,10 +1,14 @@ package pluginenv import ( + "context" "fmt" "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -298,3 +302,70 @@ func TestEnvironment_ShutdownError(t *testing.T) { assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) assert.Len(t, env.Shutdown(), 2) } + +func TestEnvironment_ConcurrentHookInvocations(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + "foo/plugin.json": `{"id": "foo", "backend": {}}`, + }) + defer os.RemoveAll(dir) + + var provider MockProvider + defer provider.AssertExpectations(t) + + var api struct{ plugin.API } + var supervisor MockSupervisor + defer supervisor.AssertExpectations(t) + var hooks plugintest.Hooks + defer hooks.AssertExpectations(t) + + env, err := New( + SearchPath(dir), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + provider.On("API").Return(&api, nil) + provider.On("Supervisor").Return(&supervisor, nil) + + supervisor.On("Start").Return(nil) + supervisor.On("Stop").Return(nil) + supervisor.On("Hooks").Return(&hooks) + + ch := make(chan bool) + + hooks.On("OnActivate", &api).Return(nil) + hooks.On("OnDeactivate").Return(nil) + hooks.On("ServeHTTP", mock.AnythingOfType("*httptest.ResponseRecorder"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) { + r := args.Get(1).(*http.Request) + if r.URL.Path == "/1" { + <-ch + } else { + ch <- true + } + }) + + assert.NoError(t, env.ActivatePlugin("foo")) + + rec := httptest.NewRecorder() + + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + req, err := http.NewRequest("GET", "/1", nil) + require.NoError(t, err) + env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo"))) + wg.Done() + }() + + go func() { + req, err := http.NewRequest("GET", "/2", nil) + require.NoError(t, err) + env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo"))) + wg.Done() + }() + + wg.Wait() +} -- cgit v1.2.3-1-g7c22