Skip to content

Commit

Permalink
plugin: fix InstallPlugin() API by manually creating RPC code (matter…
Browse files Browse the repository at this point in the history
…most#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.
  • Loading branch information
ilgooz authored and hanzei committed Nov 29, 2019
1 parent e034117 commit 0adbfa8
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 31 deletions.
124 changes: 124 additions & 0 deletions app/plugin_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"image/color"
"image/png"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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()
Expand Down
52 changes: 52 additions & 0 deletions plugin/client_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 0 additions & 31 deletions plugin/client_rpc_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugin/interface_generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo {
"FileWillBeUploaded",
"Implemented",
"LoadPluginConfiguration",
"InstallPlugin",
"LogDebug",
"LogError",
"LogInfo",
Expand Down

0 comments on commit 0adbfa8

Please sign in to comment.