From 0adbfa8478f5b3cb36d88486bdb8e4deb7093e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lker=20G=C3=B6ktu=C4=9F=20=C3=96zt=C3=BCrk?= Date: Fri, 29 Nov 2019 14:41:17 +0300 Subject: [PATCH] plugin: fix InstallPlugin() API by manually creating RPC code (#13041) Flugin: fix InstallPlugin() API by manually creating RPC code previous implementation of InstallPlugin()-#12232 's RPC funcs wasn't working because `io.Reader` isn't supported by the RPC code generation tool. RPC does not support streaming data and RPC code generation tool does not handle this exception. thus, RPC funcs are now implemented manually to stream `io.Reader` through a separate multiplexed connection. --- app/plugin_api_test.go | 124 +++++++++++++++++++++++++++++ plugin/client_rpc.go | 52 ++++++++++++ plugin/client_rpc_generated.go | 31 -------- plugin/interface_generator/main.go | 1 + 4 files changed, 177 insertions(+), 31 deletions(-) diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index beae2aa37e0c..8400c43894df 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -11,6 +11,8 @@ import ( "image/color" "image/png" "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -731,6 +733,128 @@ func TestPluginAPIInstallPlugin(t *testing.T) { assert.True(t, found) } +func TestInstallPlugin(t *testing.T) { + // TODO(ilgooz): remove this setup func to use existent setupPluginApiTest(). + // following setupTest() func is a modified version of setupPluginApiTest(). + // we need a modified version of setupPluginApiTest() because it wasn't possible to use it directly here + // since it removes plugin dirs right after it returns, does not update App configs with the plugin + // dirs and this behavior tends to break this test as a result. + setupTest := func(t *testing.T, pluginCode string, pluginManifest string, pluginID string, app *App) (func(), string) { + pluginDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + webappPluginDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + + app.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Directory = pluginDir + *cfg.PluginSettings.ClientDirectory = webappPluginDir + }) + + env, err := plugin.NewEnvironment(app.NewPluginAPI, pluginDir, webappPluginDir, app.Log) + require.NoError(t, err) + + app.SetPluginsEnvironment(env) + + backend := filepath.Join(pluginDir, pluginID, "backend.exe") + utils.CompileGo(t, pluginCode, backend) + + ioutil.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600) + manifest, activated, reterr := env.Activate(pluginID) + require.Nil(t, reterr) + require.NotNil(t, manifest) + require.True(t, activated) + + return func() { + os.RemoveAll(pluginDir) + os.RemoveAll(webappPluginDir) + }, pluginDir + } + + th := Setup(t).InitBasic() + defer th.TearDown() + + // start an http server to serve plugin's tarball to the test. + path, _ := fileutils.FindDir("tests") + ts := httptest.NewServer(http.FileServer(http.Dir(path))) + defer ts.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableUploads = true + cfg.PluginSettings.Plugins["testinstallplugin"] = map[string]interface{}{ + "DownloadURL": ts.URL + "/testplugin.tar.gz", + } + }) + + tearDown, _ := setupTest(t, + ` + package main + + import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-server/v5/plugin" + ) + + type configuration struct { + DownloadURL string + } + + type Plugin struct { + plugin.MattermostPlugin + + configuration configuration + } + + func (p *Plugin) OnConfigurationChange() error { + if err := p.API.LoadPluginConfiguration(&p.configuration); err != nil { + return err + } + return nil + } + + func (p *Plugin) OnActivate() error { + resp, err := http.Get(p.configuration.DownloadURL) + if err != nil { + return err + } + defer resp.Body.Close() + _, aerr := p.API.InstallPlugin(resp.Body, true) + if aerr != nil { + return errors.Wrap(aerr, "cannot install plugin") + } + return nil + } + + func main() { + plugin.ClientMain(&Plugin{}) + } + + `, + `{"id": "testinstallplugin", "backend": {"executable": "backend.exe"}, "settings_schema": { + "settings": [ + { + "key": "DownloadURL", + "type": "text" + } + ] + }}`, "testinstallplugin", th.App) + defer tearDown() + + hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testinstallplugin") + require.NoError(t, err) + + err = hooks.OnActivate() + require.NoError(t, err) + + plugins, aerr := th.App.GetPlugins() + require.Nil(t, aerr) + require.Len(t, plugins.Inactive, 1) + require.Equal(t, "testplugin", plugins.Inactive[0].Id) +} + func TestPluginAPIGetTeamIcon(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/plugin/client_rpc.go b/plugin/client_rpc.go index d04f138caa0e..cb7ac386d06c 100644 --- a/plugin/client_rpc.go +++ b/plugin/client_rpc.go @@ -720,3 +720,55 @@ func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns } return nil } + +type Z_InstallPluginArgs struct { + PluginStreamID uint32 + B bool +} + +type Z_InstallPluginReturns struct { + A *model.Manifest + B *model.AppError +} + +func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) { + pluginStreamID := g.muxBroker.NextId() + + go func() { + uploadPluginConnection, err := g.muxBroker.Accept(pluginStreamID) + if err != nil { + log.Print("Plugin failed to upload plugin. MuxBroker could not Accept connection", mlog.Err(err)) + return + } + defer uploadPluginConnection.Close() + serveIOReader(file, uploadPluginConnection) + }() + + _args := &Z_InstallPluginArgs{pluginStreamID, replace} + _returns := &Z_InstallPluginReturns{} + if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil { + log.Print("RPC call InstallPlugin to plugin failed.", mlog.Err(err)) + } + + return _returns.A, _returns.B +} + +func (g *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error { + hook, ok := g.impl.(interface { + InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) + }) + if !ok { + return encodableError(fmt.Errorf("API InstallPlugin called but not implemented.")) + } + + receivePluginConnection, err := g.muxBroker.Dial(args.PluginStreamID) + if err != nil { + fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error()) + return err + } + pluginReader := connectIOReader(receivePluginConnection) + defer pluginReader.Close() + + returns.A, returns.B = hook.InstallPlugin(pluginReader, args.B) + return nil +} diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index afbf0fbdbf45..b87df1055af6 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -8,7 +8,6 @@ package plugin import ( "fmt" - "io" "log" "github.com/mattermost/mattermost-server/v5/mlog" @@ -3607,36 +3606,6 @@ func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_G return nil } -type Z_InstallPluginArgs struct { - A io.Reader - B bool -} - -type Z_InstallPluginReturns struct { - A *model.Manifest - B *model.AppError -} - -func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) { - _args := &Z_InstallPluginArgs{file, replace} - _returns := &Z_InstallPluginReturns{} - if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil { - log.Printf("RPC call to InstallPlugin API failed: %s", err.Error()) - } - return _returns.A, _returns.B -} - -func (s *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error { - if hook, ok := s.impl.(interface { - InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) - }); ok { - returns.A, returns.B = hook.InstallPlugin(args.A, args.B) - } else { - return encodableError(fmt.Errorf("API InstallPlugin called but not implemented.")) - } - return nil -} - type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go index 4e139e693a25..2dcc7cbe0284 100644 --- a/plugin/interface_generator/main.go +++ b/plugin/interface_generator/main.go @@ -395,6 +395,7 @@ func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { "FileWillBeUploaded", "Implemented", "LoadPluginConfiguration", + "InstallPlugin", "LogDebug", "LogError", "LogInfo",