From 6dab9eadf896a2cb0fe2fbfe28686c06b66a8687 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Wed, 9 Feb 2022 20:29:00 +0100 Subject: [PATCH] [MM-40935] Add OnInstall() plugin hook (#19499) --- api4/system.go | 2 +- app/app_iface.go | 2 +- app/onboarding.go | 30 ++++++++++++++++------ app/opentracing/opentracing_layer.go | 4 +-- model/plugin_on_install_event.go | 9 +++++++ plugin/client_rpc_generated.go | 36 +++++++++++++++++++++++++++ plugin/hooks.go | 9 +++++++ plugin/hooks_timer_layer_generated.go | 7 ++++++ plugin/plugintest/hooks.go | 14 +++++++++++ 9 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 model/plugin_on_install_event.go diff --git a/api4/system.go b/api4/system.go index 6625c7f0fa..b478f6e05b 100644 --- a/api4/system.go +++ b/api4/system.go @@ -897,7 +897,7 @@ func completeOnboarding(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec.AddMeta("install_plugin", onboardingRequest.InstallPlugins) - appErr := c.App.CompleteOnboarding(onboardingRequest) + appErr := c.App.CompleteOnboarding(c.AppContext, onboardingRequest) if appErr != nil { c.Err = appErr return diff --git a/app/app_iface.go b/app/app_iface.go index 4e834f9af0..ae06ac7b07 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -438,7 +438,7 @@ type AppIface interface { CompareAndDeletePluginKey(pluginID string, key string, oldValue []byte) (bool, *model.AppError) CompareAndSetPluginKey(pluginID string, key string, oldValue, newValue []byte) (bool, *model.AppError) CompleteOAuth(c *request.Context, service string, body io.ReadCloser, teamID string, props map[string]string, tokenUser *model.User) (*model.User, *model.AppError) - CompleteOnboarding(request *model.CompleteOnboardingRequest) *model.AppError + CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError) Compliance() einterfaces.ComplianceInterface Config() *model.Config diff --git a/app/onboarding.go b/app/onboarding.go index 0cb19b103a..092a6ef4fe 100644 --- a/app/onboarding.go +++ b/app/onboarding.go @@ -7,18 +7,21 @@ import ( "net/http" "sync" + "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) -func (a *App) CompleteOnboarding(request *model.CompleteOnboardingRequest) *model.AppError { - var wg sync.WaitGroup - - if !*a.Config().PluginSettings.Enable { - return model.NewAppError("completeOnboarding", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) +func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError { + pluginsEnvironment := a.Channels().GetPluginsEnvironment() + if pluginsEnvironment == nil { + return model.NewAppError("CompleteOnboarding", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - for _, id := range request.InstallPlugins { + pluginContext := pluginContext(c) + + var wg sync.WaitGroup + for _, pluginID := range request.InstallPlugins { wg.Add(1) go func(id string) { @@ -37,7 +40,20 @@ func (a *App) CompleteOnboarding(request *model.CompleteOnboardingRequest) *mode mlog.Error("Failed to enable plugin for onboarding", mlog.String("id", id), mlog.Err(appErr)) return } - }(id) + + hooks, err := pluginsEnvironment.HooksForPlugin(id) + if err != nil { + mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err)) + return + } + + event := model.OnInstallEvent{ + UserId: c.Session().UserId, + } + if err = hooks.OnInstall(pluginContext, event); err != nil { + mlog.Error("Plugin OnInstall hook failed", mlog.String("plugin_id", id), mlog.Err(err)) + } + }(pluginID) } wg.Wait() diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 242e3cc80e..34ac5f18bf 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -1671,7 +1671,7 @@ func (a *OpenTracingAppLayer) CompleteOAuth(c *request.Context, service string, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) CompleteOnboarding(request *model.CompleteOnboardingRequest) *model.AppError { +func (a *OpenTracingAppLayer) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CompleteOnboarding") @@ -1683,7 +1683,7 @@ func (a *OpenTracingAppLayer) CompleteOnboarding(request *model.CompleteOnboardi }() defer span.Finish() - resultVar0 := a.app.CompleteOnboarding(request) + resultVar0 := a.app.CompleteOnboarding(c, request) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/model/plugin_on_install_event.go b/model/plugin_on_install_event.go new file mode 100644 index 0000000000..186fd5bd80 --- /dev/null +++ b/model/plugin_on_install_event.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// OnInstallEvent is sent to the plugin when it gets installed. +type OnInstallEvent struct { + UserId string // The user who installed the plugin +} diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 11382384a7..5ab31c3c46 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -706,6 +706,42 @@ func (s *hooksRPCServer) RunDataRetention(args *Z_RunDataRetentionArgs, returns return nil } +func init() { + hookNameToId["OnInstall"] = OnInstallID +} + +type Z_OnInstallArgs struct { + A *Context + B model.OnInstallEvent +} + +type Z_OnInstallReturns struct { + A error +} + +func (g *hooksRPCClient) OnInstall(c *Context, event model.OnInstallEvent) error { + _args := &Z_OnInstallArgs{c, event} + _returns := &Z_OnInstallReturns{} + if g.implemented[OnInstallID] { + if err := g.client.Call("Plugin.OnInstall", _args, _returns); err != nil { + g.log.Error("RPC call OnInstall to plugin failed.", mlog.Err(err)) + } + } + return _returns.A +} + +func (s *hooksRPCServer) OnInstall(args *Z_OnInstallArgs, returns *Z_OnInstallReturns) error { + if hook, ok := s.impl.(interface { + OnInstall(c *Context, event model.OnInstallEvent) error + }); ok { + returns.A = hook.OnInstall(args.A, args.B) + returns.A = encodableError(returns.A) + } else { + return encodableError(fmt.Errorf("Hook OnInstall called but not implemented.")) + } + return nil +} + type Z_RegisterCommandArgs struct { A *model.Command } diff --git a/plugin/hooks.go b/plugin/hooks.go index ff646c9248..f220004c32 100644 --- a/plugin/hooks.go +++ b/plugin/hooks.go @@ -40,6 +40,7 @@ const ( OnWebSocketDisconnectID = 22 WebSocketMessageHasBeenPostedID = 23 RunDataRetentionID = 24 + OnInstallID = 25 TotalHooksID = iota ) @@ -249,4 +250,12 @@ type Hooks interface { // // Minimum server version: 6.4 RunDataRetention(nowTime, batchSize int64) (int64, error) + + // OnInstall is invoked after the installation of a plugin as part of the onboarding. + // It's called on every installation, not only once. + // + // In the future, other plugin installation methods will trigger this hook, e.g. an installation via the Marketplace. + // + // Minimum server version: 6.5 + OnInstall(c *Context, event model.OnInstallEvent) error } diff --git a/plugin/hooks_timer_layer_generated.go b/plugin/hooks_timer_layer_generated.go index 1567cff7a0..28e5a790a2 100644 --- a/plugin/hooks_timer_layer_generated.go +++ b/plugin/hooks_timer_layer_generated.go @@ -193,3 +193,10 @@ func (hooks *hooksTimerLayer) RunDataRetention(nowTime, batchSize int64) (int64, hooks.recordTime(startTime, "RunDataRetention", _returnsB == nil) return _returnsA, _returnsB } + +func (hooks *hooksTimerLayer) OnInstall(c *Context, event model.OnInstallEvent) error { + startTime := timePkg.Now() + _returnsA := hooks.hooksImpl.OnInstall(c, event) + hooks.recordTime(startTime, "OnInstall", _returnsA == nil) + return _returnsA +} diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index e2ceb616f7..50a5e02a3f 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -194,6 +194,20 @@ func (_m *Hooks) OnDeactivate() error { return r0 } +// OnInstall provides a mock function with given fields: c, event +func (_m *Hooks) OnInstall(c *plugin.Context, event model.OnInstallEvent) error { + ret := _m.Called(c, event) + + var r0 error + if rf, ok := ret.Get(0).(func(*plugin.Context, model.OnInstallEvent) error); ok { + r0 = rf(c, event) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // OnPluginClusterEvent provides a mock function with given fields: c, ev func (_m *Hooks) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) { _m.Called(c, ev)