Differentiate between installed and activated states for plugins (#7706)

This commit is contained in:
Joram Wilander
2017-10-25 08:17:17 -04:00
committed by GitHub
parent 9c0575ce6e
commit 16b845c0d7
11 changed files with 406 additions and 49 deletions

View File

@@ -24,6 +24,9 @@ func (api *API) InitPlugin() {
api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(getPlugins)).Methods("GET")
api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugin.Handle("/activate", api.ApiSessionRequired(activatePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/deactivate", api.ApiSessionRequired(deactivatePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/webapp", api.ApiHandler(getWebappPlugins)).Methods("GET")
}
@@ -64,7 +67,7 @@ func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
}
defer file.Close()
manifest, unpackErr := c.App.UnpackAndActivatePlugin(file)
manifest, unpackErr := c.App.InstallPlugin(file)
if unpackErr != nil {
c.Err = unpackErr
@@ -86,13 +89,13 @@ func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
manifests, err := c.App.GetActivePluginManifests()
response, err := c.App.GetPluginManifests()
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.ManifestListToJson(manifests)))
w.Write([]byte(response.ToJson()))
}
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -141,3 +144,51 @@ func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.ManifestListToJson(clientManifests)))
}
func activatePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("activatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
if err := c.App.EnablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func deactivatePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("deactivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
if err := c.App.DisablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}

View File

@@ -65,12 +65,12 @@ func TestPlugin(t *testing.T) {
_, resp = th.Client.UploadPlugin(file)
CheckForbiddenStatus(t, resp)
// Successful get
manifests, resp := th.SystemAdminClient.GetPlugins()
// Successful gets
pluginsResp, resp := th.SystemAdminClient.GetPlugins()
CheckNoError(t, resp)
found := false
for _, m := range manifests {
for _, m := range pluginsResp.Inactive {
if m.Id == manifest.Id {
found = true
}
@@ -78,6 +78,64 @@ func TestPlugin(t *testing.T) {
assert.True(t, found)
found = false
for _, m := range pluginsResp.Active {
if m.Id == manifest.Id {
found = true
}
}
assert.False(t, found)
states := th.App.Config().PluginSettings.PluginStates
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates = states })
}()
// Successful activate
ok, resp := th.SystemAdminClient.ActivatePlugin(manifest.Id)
CheckNoError(t, resp)
assert.True(t, ok)
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
CheckNoError(t, resp)
found = false
for _, m := range pluginsResp.Active {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
// Activate error case
ok, resp = th.SystemAdminClient.ActivatePlugin("junk")
CheckBadRequestStatus(t, resp)
assert.False(t, ok)
// Successful deactivate
ok, resp = th.SystemAdminClient.DeactivatePlugin(manifest.Id)
CheckNoError(t, resp)
assert.True(t, ok)
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
CheckNoError(t, resp)
found = false
for _, m := range pluginsResp.Inactive {
if m.Id == manifest.Id {
found = true
}
}
assert.True(t, found)
// Deactivate error case
ok, resp = th.SystemAdminClient.DeactivatePlugin("junk")
CheckBadRequestStatus(t, resp)
assert.False(t, ok)
// Get error cases
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })
_, resp = th.SystemAdminClient.GetPlugins()
@@ -88,7 +146,10 @@ func TestPlugin(t *testing.T) {
CheckForbiddenStatus(t, resp)
// Successful webapp get
manifests, resp = th.Client.GetWebappPlugins()
_, resp = th.SystemAdminClient.ActivatePlugin(manifest.Id)
CheckNoError(t, resp)
manifests, resp := th.Client.GetWebappPlugins()
CheckNoError(t, resp)
found = false
@@ -101,15 +162,13 @@ func TestPlugin(t *testing.T) {
assert.True(t, found)
// Successful remove
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id)
CheckNoError(t, resp)
assert.True(t, ok)
// Remove error cases
ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id)
CheckBadRequestStatus(t, resp)
assert.False(t, ok)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false })

View File

@@ -273,6 +273,8 @@ func (a *App) InitBuiltInPlugins() {
}
}
// ActivatePlugins will activate any plugins enabled in the config
// and deactivate all other plugins.
func (a *App) ActivatePlugins() {
if a.PluginEnv == nil {
l4g.Error("plugin env not initialized")
@@ -281,20 +283,52 @@ func (a *App) ActivatePlugins() {
plugins, err := a.PluginEnv.Plugins()
if err != nil {
l4g.Error("failed to start up plugins: " + err.Error())
l4g.Error("failed to activate plugins: " + err.Error())
return
}
for _, plugin := range plugins {
err := a.PluginEnv.ActivatePlugin(plugin.Manifest.Id)
if err != nil {
l4g.Error(err.Error())
id := plugin.Manifest.Id
pluginState := &model.PluginState{Enable: false}
if state, ok := a.Config().PluginSettings.PluginStates[id]; ok {
pluginState = state
}
active := a.PluginEnv.IsPluginActive(id)
if pluginState.Enable && !active {
if err := a.PluginEnv.ActivatePlugin(id); err != nil {
l4g.Error(err.Error())
continue
}
if plugin.Manifest.HasClient() {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
message.Add("manifest", plugin.Manifest.ClientManifest())
a.Publish(message)
}
l4g.Info("Activated %v plugin", id)
} else if !pluginState.Enable && active {
if err := a.PluginEnv.DeactivatePlugin(id); err != nil {
l4g.Error(err.Error())
continue
}
if plugin.Manifest.HasClient() {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
message.Add("manifest", plugin.Manifest.ClientManifest())
a.Publish(message)
}
l4g.Info("Deactivated %v plugin", id)
}
l4g.Info("Activated %v plugin", plugin.Manifest.Id)
}
}
func (a *App) UnpackAndActivatePlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
// InstallPlugin unpacks and installs a plugin but does not activate it.
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
@@ -331,20 +365,31 @@ func (a *App) UnpackAndActivatePlugin(pluginFile io.Reader) (*model.Manifest, *m
// Should add manifest validation and error handling here
err = a.PluginEnv.ActivatePlugin(manifest.Id)
if err != nil {
return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
}
if manifest.HasClient() {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
message.Add("manifest", manifest.ClientManifest())
a.Publish(message)
}
return manifest, nil
}
func (a *App) GetPluginManifests() (*model.PluginsResponse, *model.AppError) {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return nil, model.NewAppError("GetPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return nil, model.NewAppError("GetPluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
}
resp := &model.PluginsResponse{Active: []*model.Manifest{}, Inactive: []*model.Manifest{}}
for _, plugin := range plugins {
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
resp.Active = append(resp.Active, plugin.Manifest)
} else {
resp.Inactive = append(resp.Inactive, plugin.Manifest)
}
}
return resp, nil
}
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
@@ -365,8 +410,12 @@ func (a *App) RemovePlugin(id string) *model.AppError {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins := a.PluginEnv.ActivePlugins()
manifest := &model.Manifest{}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
}
var manifest *model.Manifest
for _, p := range plugins {
if p.Manifest.Id == id {
manifest = p.Manifest
@@ -374,9 +423,21 @@ func (a *App) RemovePlugin(id string) *model.AppError {
}
}
err := a.PluginEnv.DeactivatePlugin(id)
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
if manifest == nil {
return model.NewAppError("RemovePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
}
if a.PluginEnv.IsPluginActive(id) {
err := a.PluginEnv.DeactivatePlugin(id)
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
}
if manifest.HasClient() {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
message.Add("manifest", manifest.ClientManifest())
a.Publish(message)
}
}
err = os.RemoveAll(filepath.Join(a.PluginEnv.SearchPath(), id))
@@ -384,10 +445,70 @@ func (a *App) RemovePlugin(id string) *model.AppError {
return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if manifest.HasClient() {
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
message.Add("manifest", manifest.ClientManifest())
a.Publish(message)
return nil
}
// EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive.
func (a *App) EnablePlugin(id string) *model.AppError {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var manifest *model.Manifest
for _, p := range plugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
}
cfg := a.Config()
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
if err := a.SaveConfig(cfg, true); err != nil {
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
func (a *App) DisablePlugin(id string) *model.AppError {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
}
var manifest *model.Manifest
for _, p := range plugins {
if p.Manifest.Id == id {
manifest = p.Manifest
break
}
}
if manifest == nil {
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
}
cfg := a.Config()
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
if err := a.SaveConfig(cfg, true); err != nil {
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
@@ -432,7 +553,20 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
return
}
a.PluginConfigListenerId = utils.AddConfigListener(func(_, _ *model.Config) {
utils.RemoveConfigListener(a.PluginConfigListenerId)
a.PluginConfigListenerId = utils.AddConfigListener(func(prevCfg, cfg *model.Config) {
if !*prevCfg.PluginSettings.Enable && *cfg.PluginSettings.Enable {
a.InitPlugins(pluginPath, webappPath)
} else if *prevCfg.PluginSettings.Enable && !*cfg.PluginSettings.Enable {
a.ShutDownPlugins()
} else if *prevCfg.PluginSettings.Enable && *cfg.PluginSettings.Enable {
a.ActivatePlugins()
}
if a.PluginEnv == nil {
return
}
for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() {
l4g.Error(err.Error())
}

View File

@@ -75,14 +75,6 @@ func runServer(configFileLocation string) {
if webappDir, ok := utils.FindDir(model.CLIENT_DIR); ok {
a.InitPlugins("plugins", webappDir+"/plugins")
utils.AddConfigListener(func(prevCfg *model.Config, cfg *model.Config) {
if !*prevCfg.PluginSettings.Enable && *cfg.PluginSettings.Enable {
a.InitPlugins("plugins", webappDir+"/plugins")
} else if *prevCfg.PluginSettings.Enable && !*cfg.PluginSettings.Enable {
a.ShutDownPlugins()
}
})
} else {
l4g.Error("Unable to find webapp directory, could not initialize plugins")
}

View File

@@ -3471,6 +3471,18 @@
"id": "app.plugin.activate.app_error",
"translation": "Unable to activate extracted plugin. Plugin may already exist and be activated."
},
{
"id": "app.plugin.get_plugins.app_error",
"translation": "Unable to get plugins"
},
{
"id": "app.plugin.not_installed.app_error",
"translation": "Plugin is not installed"
},
{
"id": "app.plugin.config.app_error",
"translation": "Error saving plugin state in config"
},
{
"id": "app.plugin.deactivate.app_error",
"translation": "Unable to deactivate plugin"

View File

@@ -3150,12 +3150,12 @@ func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) {
// GetPlugins will return a list of plugin manifests for currently active plugins.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetPlugins() ([]*Manifest, *Response) {
func (c *Client4) GetPlugins() (*PluginsResponse, *Response) {
if r, err := c.DoApiGet(c.GetPluginsRoute(), ""); err != nil {
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return ManifestListFromJson(r.Body), BuildResponse(r)
return PluginsResponseFromJson(r.Body), BuildResponse(r)
}
}
@@ -3180,3 +3180,25 @@ func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) {
return ManifestListFromJson(r.Body), BuildResponse(r)
}
}
// ActivatePlugin will activate an plugin installed.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) ActivatePlugin(id string) (bool, *Response) {
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/activate", ""); err != nil {
return false, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
}
// DeactivatePlugin will deactivate an active plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) DeactivatePlugin(id string) (bool, *Response) {
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/deactivate", ""); err != nil {
return false, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
}

View File

@@ -504,9 +504,14 @@ type JobSettings struct {
RunScheduler *bool
}
type PluginState struct {
Enable bool
}
type PluginSettings struct {
Enable *bool
Plugins map[string]interface{}
Enable *bool
Plugins map[string]interface{}
PluginStates map[string]*PluginState
}
type Config struct {
@@ -1454,6 +1459,10 @@ func (o *Config) SetDefaults() {
o.PluginSettings.Plugins = make(map[string]interface{})
}
if o.PluginSettings.PluginStates == nil {
o.PluginSettings.PluginStates = make(map[string]*PluginState)
}
o.defaultWebrtcSettings()
}

31
model/plugins_response.go Normal file
View File

@@ -0,0 +1,31 @@
package model
import (
"encoding/json"
"io"
)
type PluginsResponse struct {
Active []*Manifest `json:"active"`
Inactive []*Manifest `json:"inactive"`
}
func (m *PluginsResponse) ToJson() string {
b, err := json.Marshal(m)
if err != nil {
return ""
} else {
return string(b)
}
}
func PluginsResponseFromJson(data io.Reader) *PluginsResponse {
decoder := json.NewDecoder(data)
var m PluginsResponse
err := decoder.Decode(&m)
if err == nil {
return &m
} else {
return nil
}
}

View File

@@ -0,0 +1,31 @@
package model
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPluginsResponseJson(t *testing.T) {
manifest := &Manifest{
Id: "theid",
Backend: &ManifestBackend{
Executable: "theexecutable",
},
Webapp: &ManifestWebapp{
BundlePath: "thebundlepath",
},
}
response := &PluginsResponse{
Active: []*Manifest{manifest},
Inactive: []*Manifest{},
}
json := response.ToJson()
newResponse := PluginsResponseFromJson(strings.NewReader(json))
assert.Equal(t, newResponse, response)
assert.Equal(t, newResponse.ToJson(), json)
assert.Equal(t, PluginsResponseFromJson(strings.NewReader("junk")), (*PluginsResponse)(nil))
}

View File

@@ -89,6 +89,20 @@ func (env *Environment) ActivePluginIds() (ids []string) {
return
}
// Returns true if the plugin is active, false otherwise.
func (env *Environment) IsPluginActive(pluginId string) bool {
env.mutex.RLock()
defer env.mutex.RUnlock()
for id := range env.activePlugins {
if id == pluginId {
return true
}
}
return false
}
// Activates the plugin with the given id.
func (env *Environment) ActivatePlugin(id string) error {
env.mutex.Lock()

View File

@@ -152,10 +152,12 @@ func TestEnvironment(t *testing.T) {
activePlugins = env.ActivePlugins()
assert.Len(t, activePlugins, 1)
assert.Error(t, env.ActivatePlugin("foo"))
assert.True(t, env.IsPluginActive("foo"))
hooks.On("OnDeactivate").Return(nil)
assert.NoError(t, env.DeactivatePlugin("foo"))
assert.Error(t, env.DeactivatePlugin("foo"))
assert.False(t, env.IsPluginActive("foo"))
assert.NoError(t, env.ActivatePlugin("foo"))
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})