Skip to content

Commit 0adbfa8

Browse files
ilgoozhanzei
authored andcommitted
plugin: fix InstallPlugin() API by manually creating RPC code (mattermost#13041)
Flugin: fix InstallPlugin() API by manually creating RPC code previous implementation of InstallPlugin()-mattermost#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.
1 parent e034117 commit 0adbfa8

File tree

4 files changed

+177
-31
lines changed

4 files changed

+177
-31
lines changed

app/plugin_api_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"image/color"
1212
"image/png"
1313
"io/ioutil"
14+
"net/http"
15+
"net/http/httptest"
1416
"os"
1517
"path/filepath"
1618
"strings"
@@ -731,6 +733,128 @@ func TestPluginAPIInstallPlugin(t *testing.T) {
731733
assert.True(t, found)
732734
}
733735

736+
func TestInstallPlugin(t *testing.T) {
737+
// TODO(ilgooz): remove this setup func to use existent setupPluginApiTest().
738+
// following setupTest() func is a modified version of setupPluginApiTest().
739+
// we need a modified version of setupPluginApiTest() because it wasn't possible to use it directly here
740+
// since it removes plugin dirs right after it returns, does not update App configs with the plugin
741+
// dirs and this behavior tends to break this test as a result.
742+
setupTest := func(t *testing.T, pluginCode string, pluginManifest string, pluginID string, app *App) (func(), string) {
743+
pluginDir, err := ioutil.TempDir("", "")
744+
require.NoError(t, err)
745+
webappPluginDir, err := ioutil.TempDir("", "")
746+
require.NoError(t, err)
747+
748+
app.UpdateConfig(func(cfg *model.Config) {
749+
*cfg.PluginSettings.Directory = pluginDir
750+
*cfg.PluginSettings.ClientDirectory = webappPluginDir
751+
})
752+
753+
env, err := plugin.NewEnvironment(app.NewPluginAPI, pluginDir, webappPluginDir, app.Log)
754+
require.NoError(t, err)
755+
756+
app.SetPluginsEnvironment(env)
757+
758+
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
759+
utils.CompileGo(t, pluginCode, backend)
760+
761+
ioutil.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
762+
manifest, activated, reterr := env.Activate(pluginID)
763+
require.Nil(t, reterr)
764+
require.NotNil(t, manifest)
765+
require.True(t, activated)
766+
767+
return func() {
768+
os.RemoveAll(pluginDir)
769+
os.RemoveAll(webappPluginDir)
770+
}, pluginDir
771+
}
772+
773+
th := Setup(t).InitBasic()
774+
defer th.TearDown()
775+
776+
// start an http server to serve plugin's tarball to the test.
777+
path, _ := fileutils.FindDir("tests")
778+
ts := httptest.NewServer(http.FileServer(http.Dir(path)))
779+
defer ts.Close()
780+
781+
th.App.UpdateConfig(func(cfg *model.Config) {
782+
*cfg.PluginSettings.Enable = true
783+
*cfg.PluginSettings.EnableUploads = true
784+
cfg.PluginSettings.Plugins["testinstallplugin"] = map[string]interface{}{
785+
"DownloadURL": ts.URL + "/testplugin.tar.gz",
786+
}
787+
})
788+
789+
tearDown, _ := setupTest(t,
790+
`
791+
package main
792+
793+
import (
794+
"net/http"
795+
796+
"github.com/pkg/errors"
797+
798+
"github.com/mattermost/mattermost-server/v5/plugin"
799+
)
800+
801+
type configuration struct {
802+
DownloadURL string
803+
}
804+
805+
type Plugin struct {
806+
plugin.MattermostPlugin
807+
808+
configuration configuration
809+
}
810+
811+
func (p *Plugin) OnConfigurationChange() error {
812+
if err := p.API.LoadPluginConfiguration(&p.configuration); err != nil {
813+
return err
814+
}
815+
return nil
816+
}
817+
818+
func (p *Plugin) OnActivate() error {
819+
resp, err := http.Get(p.configuration.DownloadURL)
820+
if err != nil {
821+
return err
822+
}
823+
defer resp.Body.Close()
824+
_, aerr := p.API.InstallPlugin(resp.Body, true)
825+
if aerr != nil {
826+
return errors.Wrap(aerr, "cannot install plugin")
827+
}
828+
return nil
829+
}
830+
831+
func main() {
832+
plugin.ClientMain(&Plugin{})
833+
}
834+
835+
`,
836+
`{"id": "testinstallplugin", "backend": {"executable": "backend.exe"}, "settings_schema": {
837+
"settings": [
838+
{
839+
"key": "DownloadURL",
840+
"type": "text"
841+
}
842+
]
843+
}}`, "testinstallplugin", th.App)
844+
defer tearDown()
845+
846+
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testinstallplugin")
847+
require.NoError(t, err)
848+
849+
err = hooks.OnActivate()
850+
require.NoError(t, err)
851+
852+
plugins, aerr := th.App.GetPlugins()
853+
require.Nil(t, aerr)
854+
require.Len(t, plugins.Inactive, 1)
855+
require.Equal(t, "testplugin", plugins.Inactive[0].Id)
856+
}
857+
734858
func TestPluginAPIGetTeamIcon(t *testing.T) {
735859
th := Setup(t).InitBasic()
736860
defer th.TearDown()

plugin/client_rpc.go

+52
Original file line numberDiff line numberDiff line change
@@ -720,3 +720,55 @@ func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns
720720
}
721721
return nil
722722
}
723+
724+
type Z_InstallPluginArgs struct {
725+
PluginStreamID uint32
726+
B bool
727+
}
728+
729+
type Z_InstallPluginReturns struct {
730+
A *model.Manifest
731+
B *model.AppError
732+
}
733+
734+
func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
735+
pluginStreamID := g.muxBroker.NextId()
736+
737+
go func() {
738+
uploadPluginConnection, err := g.muxBroker.Accept(pluginStreamID)
739+
if err != nil {
740+
log.Print("Plugin failed to upload plugin. MuxBroker could not Accept connection", mlog.Err(err))
741+
return
742+
}
743+
defer uploadPluginConnection.Close()
744+
serveIOReader(file, uploadPluginConnection)
745+
}()
746+
747+
_args := &Z_InstallPluginArgs{pluginStreamID, replace}
748+
_returns := &Z_InstallPluginReturns{}
749+
if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil {
750+
log.Print("RPC call InstallPlugin to plugin failed.", mlog.Err(err))
751+
}
752+
753+
return _returns.A, _returns.B
754+
}
755+
756+
func (g *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error {
757+
hook, ok := g.impl.(interface {
758+
InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError)
759+
})
760+
if !ok {
761+
return encodableError(fmt.Errorf("API InstallPlugin called but not implemented."))
762+
}
763+
764+
receivePluginConnection, err := g.muxBroker.Dial(args.PluginStreamID)
765+
if err != nil {
766+
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
767+
return err
768+
}
769+
pluginReader := connectIOReader(receivePluginConnection)
770+
defer pluginReader.Close()
771+
772+
returns.A, returns.B = hook.InstallPlugin(pluginReader, args.B)
773+
return nil
774+
}

plugin/client_rpc_generated.go

-31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugin/interface_generator/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo {
395395
"FileWillBeUploaded",
396396
"Implemented",
397397
"LoadPluginConfiguration",
398+
"InstallPlugin",
398399
"LogDebug",
399400
"LogError",
400401
"LogInfo",

0 commit comments

Comments
 (0)