diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index beae2aa37e..8400c43894 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 d04f138caa..cb7ac386d0 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 afbf0fbdbf..b87df1055a 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 4e139e693a..2dcc7cbe02 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",