There were multiple issues at play here. 1. If the plugin was disabled in ch.syncPlugins, then the prepackaged plugin would not be processed. Therefore, if there was an earlier version of the plugin that remained in S3, that would show up instead. Therefore, we have to let the full plugin extraction procedure go ahead, but not enable focalboard. 2. We also need to send the disablePlugin signal to the other node. But the signal won't be sent until the cluster leader is elected and inter-node communication is set up. Therefore, we need to listen to the ClusterLeaderChanged event and send out a disable plugin event as well. When both these issues are taken care of, then we would correctly see the version of the plugin from the prepackaged_plugins directory in the disabled state. https://mattermost.atlassian.net/browse/MM-49485 ```release-note NONE ```
388 строки
12 KiB
Go
388 строки
12 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/app/imaging"
|
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
|
"github.com/mattermost/mattermost-server/v6/config"
|
|
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
|
"github.com/mattermost/mattermost-server/v6/product"
|
|
"github.com/mattermost/mattermost-server/v6/services/imageproxy"
|
|
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
)
|
|
|
|
const ServerKey product.ServiceKey = "server"
|
|
|
|
// licenseSvc is added to act as a starting point for future integrated products.
|
|
// It has the same signature and functionality with the license related APIs of the plugin-api.
|
|
type licenseSvc interface {
|
|
GetLicense() *model.License
|
|
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
|
|
}
|
|
|
|
// Channels contains all channels related state.
|
|
type Channels struct {
|
|
srv *Server
|
|
cfgSvc product.ConfigService
|
|
filestore filestore.FileBackend
|
|
licenseSvc licenseSvc
|
|
routerSvc *routerService
|
|
|
|
postActionCookieSecret []byte
|
|
|
|
pluginCommandsLock sync.RWMutex
|
|
pluginCommands []*PluginCommand
|
|
pluginsLock sync.RWMutex
|
|
pluginsEnvironment *plugin.Environment
|
|
pluginConfigListenerID string
|
|
|
|
imageProxy *imageproxy.ImageProxy
|
|
|
|
// cached counts that are used during notice condition validation
|
|
cachedPostCount int64
|
|
cachedUserCount int64
|
|
cachedDBMSVersion string
|
|
// previously fetched notices
|
|
cachedNotices model.ProductNotices
|
|
|
|
AccountMigration einterfaces.AccountMigrationInterface
|
|
Compliance einterfaces.ComplianceInterface
|
|
DataRetention einterfaces.DataRetentionInterface
|
|
MessageExport einterfaces.MessageExportInterface
|
|
Saml einterfaces.SamlInterface
|
|
Notification einterfaces.NotificationInterface
|
|
Ldap einterfaces.LdapInterface
|
|
|
|
// These are used to prevent concurrent upload requests
|
|
// for a given upload session which could cause inconsistencies
|
|
// and data corruption.
|
|
uploadLockMapMut sync.Mutex
|
|
uploadLockMap map[string]bool
|
|
|
|
imgDecoder *imaging.Decoder
|
|
imgEncoder *imaging.Encoder
|
|
|
|
dndTaskMut sync.Mutex
|
|
dndTask *model.ScheduledTask
|
|
|
|
postReminderMut sync.Mutex
|
|
postReminderTask *model.ScheduledTask
|
|
|
|
// collectionTypes maps from collection types to the registering plugin id
|
|
collectionTypes map[string]string
|
|
// topicTypes maps from topic types to collection types
|
|
topicTypes map[string]string
|
|
collectionAndTopicTypesMut sync.Mutex
|
|
}
|
|
|
|
func init() {
|
|
product.RegisterProduct("channels", product.Manifest{
|
|
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
|
|
return NewChannels(services)
|
|
},
|
|
Dependencies: map[product.ServiceKey]struct{}{
|
|
ServerKey: {},
|
|
product.ConfigKey: {},
|
|
product.LicenseKey: {},
|
|
product.FilestoreKey: {},
|
|
},
|
|
})
|
|
}
|
|
|
|
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
|
|
s, ok := services[ServerKey].(*Server)
|
|
if !ok {
|
|
return nil, errors.New("server not passed")
|
|
}
|
|
ch := &Channels{
|
|
srv: s,
|
|
imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()),
|
|
uploadLockMap: map[string]bool{},
|
|
collectionTypes: map[string]string{},
|
|
topicTypes: map[string]string{},
|
|
}
|
|
|
|
// To get another service:
|
|
// 1. Prepare the service interface
|
|
// 2. Add the field to *Channels
|
|
// 3. Add the service key to the slice.
|
|
// 4. Add a new case in the switch statement.
|
|
requiredServices := []product.ServiceKey{
|
|
product.ConfigKey,
|
|
product.LicenseKey,
|
|
product.FilestoreKey,
|
|
}
|
|
for _, svcKey := range requiredServices {
|
|
svc, ok := services[svcKey]
|
|
if !ok {
|
|
return nil, fmt.Errorf("Service %s not passed", svcKey)
|
|
}
|
|
switch svcKey {
|
|
// Keep adding more services here
|
|
case product.ConfigKey:
|
|
cfgSvc, ok := svc.(product.ConfigService)
|
|
if !ok {
|
|
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
|
|
}
|
|
ch.cfgSvc = cfgSvc
|
|
case product.FilestoreKey:
|
|
filestore, ok := svc.(filestore.FileBackend)
|
|
if !ok {
|
|
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
|
|
}
|
|
ch.filestore = filestore
|
|
case product.LicenseKey:
|
|
svc, ok := svc.(licenseSvc)
|
|
if !ok {
|
|
return nil, errors.New("License service did not satisfy licenseSvc interface")
|
|
}
|
|
ch.licenseSvc = svc
|
|
}
|
|
}
|
|
// We are passing a partially filled Channels struct so that the enterprise
|
|
// methods can have access to app methods.
|
|
// Otherwise, passing server would mean it has to call s.Channels(),
|
|
// which would be nil at this point.
|
|
if complianceInterface != nil {
|
|
ch.Compliance = complianceInterface(New(ServerConnector(ch)))
|
|
}
|
|
if messageExportInterface != nil {
|
|
ch.MessageExport = messageExportInterface(New(ServerConnector(ch)))
|
|
}
|
|
if dataRetentionInterface != nil {
|
|
ch.DataRetention = dataRetentionInterface(New(ServerConnector(ch)))
|
|
}
|
|
if accountMigrationInterface != nil {
|
|
ch.AccountMigration = accountMigrationInterface(New(ServerConnector(ch)))
|
|
}
|
|
if ldapInterface != nil {
|
|
ch.Ldap = ldapInterface(New(ServerConnector(ch)))
|
|
}
|
|
if notificationInterface != nil {
|
|
ch.Notification = notificationInterface(New(ServerConnector(ch)))
|
|
}
|
|
if samlInterfaceNew != nil {
|
|
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
|
|
if err := ch.Saml.ConfigureSP(); err != nil {
|
|
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
|
}
|
|
|
|
ch.AddConfigListener(func(_, _ *model.Config) {
|
|
if err := ch.Saml.ConfigureSP(); err != nil {
|
|
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
var imgErr error
|
|
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)
|
|
if decoderConcurrency == -1 {
|
|
decoderConcurrency = runtime.NumCPU()
|
|
}
|
|
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
|
|
ConcurrencyLevel: decoderConcurrency,
|
|
})
|
|
if imgErr != nil {
|
|
return nil, errors.Wrap(imgErr, "failed to create image decoder")
|
|
}
|
|
ch.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
|
|
ConcurrencyLevel: runtime.NumCPU(),
|
|
})
|
|
if imgErr != nil {
|
|
return nil, errors.Wrap(imgErr, "failed to create image encoder")
|
|
}
|
|
|
|
ch.routerSvc = newRouterService()
|
|
services[product.RouterKey] = ch.routerSvc
|
|
|
|
// Setup routes.
|
|
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
|
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
|
|
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
|
|
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
|
|
|
|
services[product.ChannelKey] = &channelsWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.PostKey] = &postServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.PermissionsKey] = &permissionsServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.TeamKey] = &teamServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.BotKey] = &botServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.HooksKey] = &hooksService{
|
|
ch: ch,
|
|
}
|
|
|
|
services[product.UserKey] = &App{ch: ch}
|
|
|
|
services[product.PreferencesKey] = &preferencesServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.CommandKey] = &App{ch: ch}
|
|
|
|
services[product.ThreadsKey] = &App{ch: ch}
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
func (ch *Channels) Start() error {
|
|
// Start plugins
|
|
ctx := request.EmptyContext(ch.srv.Log())
|
|
ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
|
|
|
ch.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
|
// We compute the difference between configs
|
|
// to ensure we don't re-init plugins unnecessarily.
|
|
diffs, err := config.Diff(prevCfg, cfg)
|
|
if err != nil {
|
|
ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
hasDiff := false
|
|
// TODO: This could be a method on ConfigDiffs itself
|
|
for _, diff := range diffs {
|
|
if strings.HasPrefix(diff.Path, "PluginSettings.") {
|
|
hasDiff = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Do only if some plugin related settings has changed.
|
|
if hasDiff {
|
|
if *cfg.PluginSettings.Enable {
|
|
ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
|
} else {
|
|
ch.ShutDownPlugins()
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
// This needs to be done after initPlugins has completed,
|
|
// because we want the full plugin processing to be complete before disabling it.
|
|
ch.disableBoardsIfNeeded()
|
|
ch.srv.AddClusterLeaderChangedListener(ch.disableBoardsIfNeeded)
|
|
|
|
// TODO: This should be moved to the platform service.
|
|
if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil {
|
|
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
|
|
}
|
|
|
|
if err := ch.ensurePostActionCookieSecret(); err != nil {
|
|
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Channels) Stop() error {
|
|
ch.ShutDownPlugins()
|
|
|
|
ch.dndTaskMut.Lock()
|
|
if ch.dndTask != nil {
|
|
ch.dndTask.Cancel()
|
|
}
|
|
ch.dndTaskMut.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Channels) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
|
return ch.cfgSvc.AddConfigListener(listener)
|
|
}
|
|
|
|
func (ch *Channels) RemoveConfigListener(id string) {
|
|
ch.cfgSvc.RemoveConfigListener(id)
|
|
}
|
|
|
|
func (ch *Channels) License() *model.License {
|
|
return ch.licenseSvc.GetLicense()
|
|
}
|
|
|
|
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
|
|
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
|
|
receiveEmailsAccepted)
|
|
}
|
|
|
|
func (a *App) HooksManager() *product.HooksManager {
|
|
return a.Srv().hooksManager
|
|
}
|
|
|
|
// Ensure hooksService implements `product.HooksService`
|
|
var _ product.HooksService = (*hooksService)(nil)
|
|
|
|
type hooksService struct {
|
|
ch *Channels
|
|
}
|
|
|
|
func (s *hooksService) RegisterHooks(productID string, hooks any) error {
|
|
return s.ch.srv.hooksManager.AddProduct(productID, hooks)
|
|
}
|
|
|
|
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
|
|
if env := ch.GetPluginsEnvironment(); env != nil {
|
|
env.RunMultiPluginHook(hookRunnerFunc, hookId)
|
|
}
|
|
|
|
// run hook for the products
|
|
ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId)
|
|
}
|
|
|
|
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
|
|
var hooks plugin.Hooks
|
|
if env := ch.GetPluginsEnvironment(); env != nil {
|
|
// we intentionally ignore the error here, because the id can be a product id
|
|
// we are going to check if we have the hooks or not
|
|
hooks, _ = env.HooksForPlugin(id)
|
|
if hooks != nil {
|
|
return hooks, nil
|
|
}
|
|
}
|
|
|
|
hooks = ch.srv.hooksManager.HooksForProduct(id)
|
|
if hooks != nil {
|
|
return hooks, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("could not find hooks for id %s", id)
|
|
}
|
|
|
|
func (ch *Channels) disableBoardsIfNeeded() {
|
|
// Disable focalboard in product mode.
|
|
if ch.srv.Config().FeatureFlags.BoardsProduct {
|
|
// disablePlugin automatically checks if the plugin is running or not,
|
|
// and if it isn't, it returns an error. Therefore we ignore those errors.
|
|
// We don't want to check here again if the plugin is enabled or not.
|
|
appErr := ch.disablePlugin(model.PluginIdFocalboard)
|
|
if appErr != nil && appErr.Id != "app.plugin.not_installed.app_error" && appErr.Id != "app.plugin.disabled.app_error" {
|
|
ch.srv.Log().Error("Error disabling plugin in product mode", mlog.Err(appErr))
|
|
}
|
|
}
|
|
}
|