[MM-52924] Implement ConfigurationWillBeSaved plugin hook (#23567)

* Implement ConfigurationWillBeSaved plugin hook

* Add comment

* Update comment

* Fix potential nil dereference if plugin environment is unset

* Address PR review
Этот коммит содержится в:
Claudio Costa
2023-06-12 18:23:02 -06:00
коммит произвёл GitHub
родитель d0ad46b496
Коммит 62a3ee8adc
10 изменённых файлов: 267 добавлений и 9 удалений

Просмотреть файл

@@ -18,6 +18,7 @@ import (
"strconv"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/product"
@@ -74,6 +75,24 @@ func (ps *PlatformService) IsConfigReadOnly() bool {
// SaveConfig replaces the active configuration, optionally notifying cluster peers.
// It returns both the previous and current configs.
func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) {
if ps.pluginEnv != nil {
var hookErr error
ps.pluginEnv.RunMultiHook(func(hooks plugin.Hooks) bool {
var cfg *model.Config
cfg, hookErr = hooks.ConfigurationWillBeSaved(newCfg)
if hookErr == nil && cfg != nil {
newCfg = cfg
}
return hookErr == nil
}, plugin.ConfigurationWillBeSavedID)
if hookErr != nil {
if appErr, ok := hookErr.(*model.AppError); ok {
return nil, nil, appErr
}
return nil, nil, model.NewAppError("saveConfig", "app.save_config.plugin_hook_error", nil, "", http.StatusBadRequest).Wrap(hookErr)
}
}
oldCfg, newCfg, err := ps.configStore.Set(newCfg)
if errors.Is(err, config.ErrReadOnlyConfiguration) {
return nil, nil, model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, "", http.StatusForbidden).Wrap(err)

Просмотреть файл

@@ -5,6 +5,7 @@ package app
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
@@ -2195,3 +2196,106 @@ func TestPluginUploadsAPI(t *testing.T) {
require.NotNil(t, manifest)
require.True(t, activated)
}
//go:embed plugin_api_tests/manual.test_configuration_will_be_saved_hook/main.tmpl
var configurationWillBeSavedHookTemplate string
func TestConfigurationWillBeSavedHook(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
getPluginCode := func(hookCode string) string {
return fmt.Sprintf(configurationWillBeSavedHookTemplate, hookCode)
}
runPlugin := func(t *testing.T, code string) {
pluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
webappPluginDir, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer os.RemoveAll(pluginDir)
defer os.RemoveAll(webappPluginDir)
newPluginAPI := func(manifest *model.Manifest) plugin.API {
return th.App.NewPluginAPI(th.Context, manifest)
}
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil)
require.NoError(t, err)
th.App.ch.SetPluginsEnvironment(env)
pluginID := "testplugin"
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, code, backend)
os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
manifest, activated, reterr := env.Activate(pluginID)
require.NoError(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
}
t.Run("error", func(t *testing.T) {
hookCode := `
return nil, fmt.Errorf("plugin hook failed")
`
runPlugin(t, getPluginCode(hookCode))
cfg := th.App.Config()
_, _, appErr := th.App.SaveConfig(cfg, false)
require.NotNil(t, appErr)
require.Equal(t, "saveConfig: An error occurred running the plugin hook on configuration save., plugin hook failed", appErr.Error())
require.Equal(t, cfg, th.App.Config())
})
t.Run("AppError", func(t *testing.T) {
hookCode := `
return nil, model.NewAppError("saveConfig", "custom_error", nil, "", 400)
`
runPlugin(t, getPluginCode(hookCode))
cfg := th.App.Config()
_, _, appErr := th.App.SaveConfig(cfg, false)
require.NotNil(t, appErr)
require.Equal(t, "custom_error", appErr.Id)
require.Equal(t, cfg, th.App.Config())
})
t.Run("no error, no config change", func(t *testing.T) {
hookCode := `
return nil, nil
`
runPlugin(t, getPluginCode(hookCode))
cfg := th.App.Config()
_, newCfg, appErr := th.App.SaveConfig(cfg, false)
require.Nil(t, appErr)
require.Equal(t, cfg, newCfg)
})
t.Run("config change", func(t *testing.T) {
hookCode := `
cfg := newCfg.Clone()
cfg.PluginSettings.Plugins["custom_plugin"] = map[string]any{
"custom_key": "custom_val",
}
return cfg, nil
`
runPlugin(t, getPluginCode(hookCode))
cfg := th.App.Config()
_, newCfg, appErr := th.App.SaveConfig(cfg, false)
require.Nil(t, appErr)
require.NotEqual(t, cfg, newCfg)
require.Equal(t, map[string]any{
"custom_key": "custom_val",
}, newCfg.PluginSettings.Plugins["custom_plugin"])
})
}

Просмотреть файл

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type TestPlugin struct {
plugin.MattermostPlugin
}
func (p *TestPlugin) OnActivate() error {
fmt.Println("activated")
return nil
}
// This acts like a template as the content of this file gets passed to
// fmt.Sprintf to inject additional logic based on the test case.
func (p *TestPlugin) ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) {
%s
}
func main() {
plugin.ClientMain(&TestPlugin{})
}