MM-53355: tidy up plugins (#24194)

* remove feature flag managed plugins

* remove unneeded plugin blocklist

* remove unnecessary wrappers

* documentation and logging improvements

* avoid use of global logger
* leverage wrapped loggers (e.g. consistently log `plugin_id`)
* promote some logs from `Debug` to `Info` for better visibility.
* extract installPluginToFilestore
* rename some variables for consistency / clarity

* make generated
This commit is contained in:
Jesse Hallam 2023-08-08 18:29:57 -03:00 committed by GitHub
parent 0e30d0abb8
commit 8372267739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 268 deletions

View File

@ -248,7 +248,8 @@ type AppIface interface {
HubRegister(webConn *platform.WebConn) HubRegister(webConn *platform.WebConn)
// HubUnregister unregisters a connection from a hub. // HubUnregister unregisters a connection from a hub.
HubUnregister(webConn *platform.WebConn) HubUnregister(webConn *platform.WebConn)
// InstallPlugin unpacks and installs a plugin but does not enable or activate it. // InstallPlugin unpacks and installs a plugin but does not enable or activate it unless the the
// plugin was already enabled.
InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError)
// LogAuditRec logs an audit record using default LvlAuditCLI. // LogAuditRec logs an audit record using default LvlAuditCLI.
LogAuditRec(rec *audit.Record, err error) LogAuditRec(rec *audit.Record, err error)

View File

@ -16,7 +16,7 @@ func (s *Server) clusterInstallPluginHandler(msg *model.ClusterMessage) {
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
} }
s.Channels().installPluginFromData(data) s.Channels().installPluginFromClusterMessage(data.Id)
} }
func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
@ -24,7 +24,7 @@ func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil { if jsonErr := json.Unmarshal(msg.Data, &data); jsonErr != nil {
mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr))
} }
s.Channels().removePluginFromData(data) s.Channels().removePluginFromClusterMessage(data.Id)
} }
func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {

View File

@ -30,11 +30,14 @@ import (
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
) )
// prepackagedPluginsDir is the hard-coded folder name where prepackaged plugins are bundled
// alongside the server.
const prepackagedPluginsDir = "prepackaged_plugins" const prepackagedPluginsDir = "prepackaged_plugins"
// pluginSignaturePath tracks the path to the plugin bundle and signature for the given plugin.
type pluginSignaturePath struct { type pluginSignaturePath struct {
pluginID string pluginID string
path string bundlePath string
signaturePath string signaturePath string
} }
@ -161,16 +164,18 @@ func (ch *Channels) syncPluginsActiveState() {
defer wg.Done() defer wg.Done()
pluginID := plugin.Manifest.Id pluginID := plugin.Manifest.Id
logger := ch.srv.Log().With(mlog.String("plugin_id", pluginID), mlog.String("bundle_path", plugin.Path))
updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID) updatedManifest, activated, err := pluginsEnvironment.Activate(pluginID)
if err != nil { if err != nil {
plugin.WrapLogger(ch.srv.Log()).Error("Unable to activate plugin", mlog.Err(err)) logger.Error("Unable to activate plugin", mlog.Err(err))
return return
} }
if activated { if activated {
// Notify all cluster clients if ready // Notify all cluster clients if ready
if err := ch.notifyPluginEnabled(updatedManifest); err != nil { if err := ch.notifyPluginEnabled(updatedManifest); err != nil {
ch.srv.Log().Error("Failed to notify cluster on plugin enable", mlog.Err(err)) logger.Error("Failed to notify cluster on plugin enable", mlog.Err(err))
} }
} }
}(plugin) }(plugin)
@ -181,7 +186,7 @@ func (ch *Channels) syncPluginsActiveState() {
} }
if err := ch.notifyPluginStatusesChanged(); err != nil { if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) ch.srv.Log().Warn("failed to notify plugin status changed", mlog.Err(err))
} }
} }
@ -213,12 +218,12 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
ch.srv.Log().Info("Starting up plugins") ch.srv.Log().Info("Starting up plugins")
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) { if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err)) ch.srv.Log().Error("Failed to start up plugins", mlog.Err(err))
return return
} }
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) { if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
mlog.Error("Failed to start up plugins", mlog.Err(err)) ch.srv.Log().Error("Failed to start up plugins", mlog.Err(err))
return return
} }
@ -235,7 +240,7 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
ch.srv.GetMetrics(), ch.srv.GetMetrics(),
) )
if err != nil { if err != nil {
mlog.Error("Failed to start up plugins", mlog.Err(err)) ch.srv.Log().Error("Failed to start up plugins", mlog.Err(err))
return return
} }
ch.pluginsLock.Lock() ch.pluginsLock.Lock()
@ -245,19 +250,17 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck) ch.pluginsEnvironment.TogglePluginHealthCheckJob(*ch.cfgSvc.Config().PluginSettings.EnableHealthCheck)
if err := ch.syncPlugins(); err != nil { if err := ch.syncPlugins(); err != nil {
mlog.Error("Failed to sync plugins from the file store", mlog.Err(err)) ch.srv.Log().Error("Failed to sync plugins from the file store", mlog.Err(err))
} }
plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir) plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir)
pluginsEnvironment = ch.GetPluginsEnvironment() pluginsEnvironment = ch.GetPluginsEnvironment()
if pluginsEnvironment == nil { if pluginsEnvironment == nil {
mlog.Info("Plugins environment not found, server is likely shutting down") ch.srv.Log().Info("Plugins environment not found, server is likely shutting down")
return return
} }
pluginsEnvironment.SetPrepackagedPlugins(plugins) pluginsEnvironment.SetPrepackagedPlugins(plugins)
ch.installFeatureFlagPlugins()
// Sync plugin active state when config changes. Also notify plugins. // Sync plugin active state when config changes. Also notify plugins.
ch.pluginsLock.Lock() ch.pluginsLock.Lock()
ch.RemoveConfigListener(ch.pluginConfigListenerID) ch.RemoveConfigListener(ch.pluginConfigListenerID)
@ -265,7 +268,6 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
// If plugin status remains unchanged, only then run this. // If plugin status remains unchanged, only then run this.
// Because (*App).InitPlugins is already run as a config change hook. // Because (*App).InitPlugins is already run as a config change hook.
if *old.PluginSettings.Enable == *new.PluginSettings.Enable { if *old.PluginSettings.Enable == *new.PluginSettings.Enable {
ch.installFeatureFlagPlugins()
ch.syncPluginsActiveState() ch.syncPluginsActiveState()
} }
@ -290,7 +292,7 @@ func (a *App) SyncPlugins() *model.AppError {
// SyncPlugins synchronizes the plugins installed locally // SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store. // with the plugin bundles available in the file store.
func (ch *Channels) syncPlugins() *model.AppError { func (ch *Channels) syncPlugins() *model.AppError {
mlog.Info("Syncing plugins from the file store") ch.srv.Log().Info("Syncing plugins from the file store")
pluginsEnvironment := ch.GetPluginsEnvironment() pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil { if pluginsEnvironment == nil {
@ -307,16 +309,19 @@ func (ch *Channels) syncPlugins() *model.AppError {
wg.Add(1) wg.Add(1)
go func(pluginID string) { go func(pluginID string) {
defer wg.Done() defer wg.Done()
logger := ch.srv.Log().With(mlog.String("plugin_id", pluginID))
// Only handle managed plugins with .filestore flag file. // Only handle managed plugins with .filestore flag file.
_, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName)) _, err := os.Stat(filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, pluginID, managedPluginFileName))
if os.IsNotExist(err) { if os.IsNotExist(err) {
mlog.Warn("Skipping sync for unmanaged plugin", mlog.String("plugin_id", pluginID)) logger.Warn("Skipping sync for unmanaged plugin")
} else if err != nil { } else if err != nil {
mlog.Error("Skipping sync for plugin after failure to check if managed", mlog.String("plugin_id", pluginID), mlog.Err(err)) logger.Error("Skipping sync for plugin after failure to check if managed", mlog.Err(err))
} else { } else {
mlog.Debug("Removing local installation of managed plugin before sync", mlog.String("plugin_id", pluginID)) logger.Info("Removing local installation of managed plugin before sync")
if err := ch.removePluginLocally(pluginID); err != nil { if err := ch.removePluginLocally(pluginID); err != nil {
mlog.Error("Failed to remove local installation of managed plugin before sync", mlog.String("plugin_id", pluginID), mlog.Err(err)) logger.Error("Failed to remove local installation of managed plugin before sync", mlog.Err(err))
} }
} }
}(plugin.Manifest.Id) }(plugin.Manifest.Id)
@ -333,26 +338,28 @@ func (ch *Channels) syncPlugins() *model.AppError {
wg.Add(1) wg.Add(1)
go func(plugin *pluginSignaturePath) { go func(plugin *pluginSignaturePath) {
defer wg.Done() defer wg.Done()
reader, appErr := ch.srv.fileReader(plugin.path) logger := ch.srv.Log().With(mlog.String("plugin_id", plugin.pluginID), mlog.String("bundle_path", plugin.bundlePath))
bundle, appErr := ch.srv.fileReader(plugin.bundlePath)
if appErr != nil { if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) logger.Error("Failed to open plugin bundle from file store.", mlog.Err(appErr))
return return
} }
defer reader.Close() defer bundle.Close()
var signature filestore.ReadCloseSeeker var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath) signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil { if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) logger.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return return
} }
defer signature.Close() defer signature.Close()
} }
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path)) logger.Info("Syncing plugin from file store")
if _, err := ch.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.blocked.app_error" && err.Id != "app.plugin.skip_installation.app_error" { if _, err := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.skip_installation.app_error" {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err)) logger.Error("Failed to sync plugin from file store", mlog.Err(err))
} }
}(plugin) }(plugin)
} }
@ -370,7 +377,7 @@ func (ch *Channels) ShutDownPlugins() {
return return
} }
mlog.Info("Shutting down plugins") ch.srv.Log().Info("Shutting down plugins")
pluginsEnvironment.Shutdown() pluginsEnvironment.Shutdown()
@ -383,7 +390,7 @@ func (ch *Channels) ShutDownPlugins() {
if ch.pluginsEnvironment == pluginsEnvironment { if ch.pluginsEnvironment == pluginsEnvironment {
ch.pluginsEnvironment = nil ch.pluginsEnvironment = nil
} else { } else {
mlog.Warn("Another PluginsEnvironment detected while shutting down plugins.") ch.srv.Log().Warn("Another PluginsEnvironment detected while shutting down plugins.")
} }
} }
@ -735,7 +742,7 @@ func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.Marke
if plugin.Manifest.IconPath != "" { if plugin.Manifest.IconPath != "" {
iconData, err = getIcon(filepath.Join(plugin.Path, plugin.Manifest.IconPath)) iconData, err = getIcon(filepath.Join(plugin.Path, plugin.Manifest.IconPath))
if err != nil { if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err)) a.Log().Warn("Error loading local plugin icon", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
} }
} }
@ -850,7 +857,7 @@ func (ch *Channels) notifyPluginEnabled(manifest *model.Manifest) error {
// which may result in a 404. // which may result in a 404.
for _, status := range statuses { for _, status := range statuses {
if status.PluginId == manifest.Id && status.Version != manifest.Version { if status.PluginId == manifest.Id && status.Version != manifest.Version {
mlog.Debug("Not ready to notify webclients", mlog.String("cluster_id", status.ClusterId), mlog.String("plugin_id", manifest.Id)) ch.srv.Log().Debug("Not ready to notify webclients", mlog.String("cluster_id", status.ClusterId), mlog.String("plugin_id", manifest.Id))
return nil return nil
} }
} }
@ -879,7 +886,7 @@ func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz") id := strings.TrimSuffix(filepath.Base(path), ".tar.gz")
helper := &pluginSignaturePath{ helper := &pluginSignaturePath{
pluginID: id, pluginID: id,
path: path, bundlePath: path,
signaturePath: "", signaturePath: "",
} }
pluginSignaturePathMap[id] = helper pluginSignaturePathMap[id] = helper
@ -911,7 +918,7 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa
return nil return nil
}) })
if err != nil { if err != nil {
mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err)) ch.srv.Log().Error("Failed to walk prepackaged plugins", mlog.Err(err))
return nil return nil
} }
@ -927,11 +934,10 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa
p, err := ch.processPrepackagedPlugin(psPath) p, err := ch.processPrepackagedPlugin(psPath)
if err != nil { if err != nil {
var appErr *model.AppError var appErr *model.AppError
// A log line already appears if the plugin is on the blocklist if errors.As(err, &appErr) && appErr.Id == "app.plugin.skip_installation.app_error" {
if errors.As(err, &appErr) && (appErr.Id == "app.plugin.blocked.app_error" || appErr.Id == "app.plugin.skip_installation.app_error") {
return return
} }
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", psPath.path), mlog.Err(err)) ch.srv.Log().Error("Failed to install prepackaged plugin", mlog.String("plugin_id", p.Manifest.Id), mlog.String("bundle_path", psPath.bundlePath), mlog.Err(err))
return return
} }
prepackagedPlugins <- p prepackagedPlugins <- p
@ -951,11 +957,13 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also // processPrepackagedPlugin will return the prepackaged plugin metadata and will also
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true. // install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path)) logger := ch.srv.Log().With(mlog.String("plugin_id", pluginPath.pluginID), mlog.String("bundle_path", pluginPath.bundlePath))
fileReader, err := os.Open(pluginPath.path) logger.Info("Processing prepackaged plugin")
fileReader, err := os.Open(pluginPath.bundlePath)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path) return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.bundlePath)
} }
defer fileReader.Close() defer fileReader.Close()
@ -965,9 +973,9 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir) plugin, pluginDir, err := ch.buildPrepackagedPlugin(pluginPath, fileReader, tmpDir)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path) return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.bundlePath)
} }
// Skip installing the plugin at all if automatic prepackaged plugins is disabled // Skip installing the plugin at all if automatic prepackaged plugins is disabled
@ -981,87 +989,24 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*
return plugin, nil return plugin, nil
} }
mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path)) logger.Info("Installing prepackaged plugin")
if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path) return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.bundlePath)
} }
return plugin, nil return plugin, nil
} }
// installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags // buildPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
func (ch *Channels) installFeatureFlagPlugins() { func (ch *Channels) buildPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
ffControledPlugins := ch.cfgSvc.Config().FeatureFlags.Plugins()
// Respect the automatic prepackaged disable setting
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
return
}
for pluginID, version := range ffControledPlugins {
// Skip installing if the plugin has been previously disabled.
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[pluginID]
if pluginState != nil && !pluginState.Enable {
ch.srv.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginID), mlog.String("version", version))
continue
}
// Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well.
pluginStatus, err := ch.GetPluginStatus(pluginID)
pluginExists := err == nil
if pluginExists && pluginStatus.Version == version {
continue
}
if version != "" && version != "control" {
// If we are on-prem skip installation if this is a downgrade
license := ch.srv.License()
inCloud := license != nil && *license.Features.Cloud
if !inCloud && pluginExists {
parsedVersion, err := semver.Parse(version)
if err != nil {
ch.srv.Log().Debug("Bad version from feature flag", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
return
}
parsedExistingVersion, err := semver.Parse(pluginStatus.Version)
if err != nil {
ch.srv.Log().Debug("Bad version from plugin manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
if parsedVersion.LTE(parsedExistingVersion) {
ch.srv.Log().Debug("Skip installation because given version was a downgrade and on-prem installations should not downgrade.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", pluginStatus.Version))
return
}
}
_, err := ch.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: version,
})
if err != nil {
ch.srv.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
if err := ch.enablePlugin(pluginID); err != nil {
ch.srv.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginID), mlog.Err(err), mlog.String("version", version))
} else {
ch.srv.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginID), mlog.String("version", version))
}
}
}
}
}
// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil { if appErr != nil {
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path) return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.bundlePath)
} }
plugin := new(plugin.PrepackagedPlugin) plugin := new(plugin.PrepackagedPlugin)
plugin.Manifest = manifest plugin.Manifest = manifest
plugin.Path = pluginPath.path plugin.Path = pluginPath.bundlePath
if pluginPath.signaturePath != "" { if pluginPath.signaturePath != "" {
sig := pluginPath.signaturePath sig := pluginPath.signaturePath
@ -1079,7 +1024,7 @@ func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSee
if manifest.IconPath != "" { if manifest.IconPath != "" {
iconData, err := getIcon(filepath.Join(pluginDir, manifest.IconPath)) iconData, err := getIcon(filepath.Join(pluginDir, manifest.IconPath))
if err != nil { if err != nil {
mlog.Warn("Error loading local plugin icon", mlog.String("plugin", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err)) ch.srv.Log().Warn("Error loading local plugin icon", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
} }
plugin.IconData = iconData plugin.IconData = iconData
} }

View File

@ -46,7 +46,6 @@ import (
"github.com/blang/semver" "github.com/blang/semver"
"github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils" "github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
@ -59,88 +58,93 @@ const managedPluginFileName = ".filestore"
// fileStorePluginFolder is the folder name in the file store of the plugin bundles installed. // fileStorePluginFolder is the folder name in the file store of the plugin bundles installed.
const fileStorePluginFolder = "plugins" const fileStorePluginFolder = "plugins"
func (ch *Channels) installPluginFromData(data model.PluginEventData) { // installPluginFromClusterMessage is called when a peer activates a plugin in the filestore,
mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id)) // signalling all other servers to do the same.
func (ch *Channels) installPluginFromClusterMessage(pluginID string) {
logger := ch.srv.Log().With(mlog.String("plugin_id", pluginID))
logger.Info("Installing plugin as per cluster message")
pluginSignaturePathMap, appErr := ch.getPluginsFromFolder() pluginSignaturePathMap, appErr := ch.getPluginsFromFolder()
if appErr != nil { if appErr != nil {
mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr)) logger.Error("Failed to get plugin signatures from filestore.", mlog.Err(appErr))
return return
} }
plugin, ok := pluginSignaturePathMap[data.Id] plugin, ok := pluginSignaturePathMap[pluginID]
if !ok { if !ok {
mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id)) logger.Error("Failed to get plugin signature from filestore.")
return return
} }
reader, appErr := ch.srv.fileReader(plugin.path) bundle, appErr := ch.srv.fileReader(plugin.bundlePath)
if appErr != nil { if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr)) logger.Error("Failed to open plugin bundle from file store.", mlog.Err(appErr))
return return
} }
defer reader.Close() defer bundle.Close()
var signature filestore.ReadCloseSeeker var signature filestore.ReadCloseSeeker
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature { if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
signature, appErr = ch.srv.fileReader(plugin.signaturePath) signature, appErr = ch.srv.fileReader(plugin.signaturePath)
if appErr != nil { if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr)) logger.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return return
} }
defer signature.Close() defer signature.Close()
} }
manifest, appErr := ch.installPluginLocally(reader, signature, installPluginLocallyAlways) manifest, appErr := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways)
if appErr != nil { if appErr != nil {
// A log line already appears if the plugin is on the blocklist or skipped // A log line already appears if the plugin is on the blocklist or skipped
if appErr.Id != "app.plugin.blocked.app_error" && appErr.Id != "app.plugin.skip_installation.app_error" { if appErr.Id != "app.plugin.blocked.app_error" && appErr.Id != "app.plugin.skip_installation.app_error" {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr)) logger.Error("Failed to sync plugin from file store", mlog.Err(appErr))
} }
return return
} }
if err := ch.notifyPluginEnabled(manifest); err != nil { if err := ch.notifyPluginEnabled(manifest); err != nil {
mlog.Error("Failed notify plugin enabled", mlog.Err(err)) logger.Error("Failed notify plugin enabled", mlog.Err(err))
} }
if err := ch.notifyPluginStatusesChanged(); err != nil { if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Error("Failed to notify plugin status changed", mlog.Err(err)) logger.Error("Failed to notify plugin status changed", mlog.Err(err))
} }
} }
func (ch *Channels) removePluginFromData(data model.PluginEventData) { // removePluginFromClusterMessage is called when a peer removes a plugin, signalling all other
mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id)) // servers to do the same.
func (ch *Channels) removePluginFromClusterMessage(pluginID string) {
logger := ch.srv.Log().With(mlog.String("plugin_id", pluginID))
if err := ch.removePluginLocally(data.Id); err != nil { logger.Info("Removing plugin as per cluster message")
mlog.Warn("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id))
if err := ch.removePluginLocally(pluginID); err != nil {
logger.Error("Failed to remove plugin locally", mlog.Err(err))
} }
if err := ch.notifyPluginStatusesChanged(); err != nil { if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("failed to notify plugin status changed", mlog.Err(err)) logger.Error("failed to notify plugin status changed", mlog.Err(err))
} }
} }
// InstallPluginWithSignature verifies and installs plugin. // InstallPlugin unpacks and installs a plugin but does not enable or activate it unless the the
func (ch *Channels) installPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) { // plugin was already enabled.
return ch.installPlugin(pluginFile, signature, installPluginLocallyAlways)
}
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) { func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
installationStrategy := installPluginLocallyOnlyIfNew installationStrategy := installPluginLocallyOnlyIfNew
if replace { if replace {
installationStrategy = installPluginLocallyAlways installationStrategy = installPluginLocallyAlways
} }
return a.installPlugin(pluginFile, nil, installationStrategy) return a.ch.installPlugin(pluginFile, nil, installationStrategy)
} }
func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { // installPlugin extracts and installs the given plugin bundle (optionally signed) for the
return a.ch.installPlugin(pluginFile, signature, installationStrategy) // current server, activating the plugin if already enabled, installs it to the filestore for
} // cluster peers to use, and then broadcasts the change to connected websockets.
//
func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { // The given installation strategy decides how to handle upgrade scenarios.
manifest, appErr := ch.installPluginLocally(pluginFile, signature, installationStrategy) func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
manifest, appErr := ch.installPluginLocally(bundle, signature, installationStrategy)
if appErr != nil { if appErr != nil {
return nil, appErr return nil, appErr
} }
@ -149,17 +153,42 @@ func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installat
return nil, nil return nil, nil
} }
logger := ch.srv.Log().With(mlog.String("plugin_id", manifest.Id))
appErr = ch.installPluginToFilestore(manifest, bundle, signature)
if appErr != nil {
return nil, appErr
}
if err := ch.notifyPluginEnabled(manifest); err != nil {
logger.Warn("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
logger.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return manifest, nil
}
// installPluginToFilestore saves the given plugin bundle (optionally signed) to the filestore,
// notifying cluster peers accordingly.
func (ch *Channels) installPluginToFilestore(manifest *model.Manifest, bundle, signature io.ReadSeeker) *model.AppError {
if signature != nil { if signature != nil {
signature.Seek(0, 0) _, err := signature.Seek(0, 0)
if _, appErr = ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil { if err != nil {
return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if _, appErr := ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil {
return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
} }
} }
// Store bundle in the file store to allow access from other servers. // Store bundle in the file store to allow access from other servers.
pluginFile.Seek(0, 0) bundle.Seek(0, 0)
if _, appErr := ch.srv.writeFile(pluginFile, getBundleStorePath(manifest.Id)); appErr != nil { if _, appErr := ch.srv.writeFile(bundle, getBundleStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr) return model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
} }
ch.notifyClusterPluginEvent( ch.notifyClusterPluginEvent(
@ -169,20 +198,15 @@ func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installat
}, },
) )
if err := ch.notifyPluginEnabled(manifest); err != nil { return nil
mlog.Warn("Failed notify plugin enabled", mlog.Err(err))
}
if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
}
return manifest, nil
} }
// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle // InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the
// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true. // plugin bundle from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace
// is true.
func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
logger := ch.srv.Log().With(mlog.String("plugin_id", request.Id))
var pluginFile, signatureFile io.ReadSeeker var pluginFile, signatureFile io.ReadSeeker
prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version) prepackagedPlugin, appErr := ch.getPrepackagedPlugin(request.Id, request.Version)
@ -205,7 +229,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
// The plugin might only be prepackaged and not on the Marketplace. // The plugin might only be prepackaged and not on the Marketplace.
if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
mlog.Warn("Failed to reach Marketplace to install plugin", mlog.String("plugin_id", request.Id), mlog.Err(appErr)) logger.Warn("Failed to reach Marketplace to install plugin", mlog.Err(appErr))
} }
if plugin != nil { if plugin != nil {
@ -245,7 +269,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError) return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
} }
manifest, appErr := ch.installPluginWithSignature(pluginFile, signatureFile) manifest, appErr := ch.installPlugin(pluginFile, signatureFile, installPluginLocallyAlways)
if appErr != nil { if appErr != nil {
return nil, appErr return nil, appErr
} }
@ -264,7 +288,11 @@ const (
installPluginLocallyAlways installPluginLocallyAlways
) )
func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { // installPluginLocally extracts and installs the given plugin bundle (optionally signed) for the
// current server, activating the plugin if already enabled.
//
// The given installation strategy decides how to handle upgrade scenarios.
func (ch *Channels) installPluginLocally(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := ch.GetPluginsEnvironment() pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil { if pluginsEnvironment == nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
@ -272,7 +300,7 @@ func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, in
// verify signature // verify signature
if signature != nil { if signature != nil {
if err := ch.verifyPlugin(pluginFile, signature); err != nil { if err := ch.verifyPlugin(bundle, signature); err != nil {
return nil, err return nil, err
} }
} }
@ -283,7 +311,7 @@ func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, in
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) manifest, pluginDir, appErr := extractPlugin(bundle, tmpDir)
if appErr != nil { if appErr != nil {
return nil, appErr return nil, appErr
} }
@ -296,9 +324,10 @@ func (ch *Channels) installPluginLocally(pluginFile, signature io.ReadSeeker, in
return manifest, nil return manifest, nil
} }
func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) { // extractPlugin unpacks the given plugin bundle into the specified directory.
pluginFile.Seek(0, 0) func extractPlugin(bundle io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
if err := extractTarGz(pluginFile, extractDir); err != nil { bundle.Seek(0, 0)
if err := extractTarGz(bundle, extractDir); err != nil {
return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, "", http.StatusBadRequest).Wrap(err) return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, "", http.StatusBadRequest).Wrap(err)
} }
@ -307,6 +336,8 @@ func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest
return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
// If the root of the plugin bundle consists of exactly one directory, assume the plugin
// is contained therein. Otherwise the root directory is expected to contain the plugin.
if len(dir) == 1 && dir[0].IsDir() { if len(dir) == 1 && dir[0].IsDir() {
extractDir = filepath.Join(extractDir, dir[0].Name()) extractDir = filepath.Join(extractDir, dir[0].Name())
} }
@ -317,13 +348,21 @@ func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest
} }
if !model.IsValidPluginId(manifest.Id) { if !model.IsValidPluginId(manifest.Id) {
return nil, "", model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]any{"Min": model.MinIdLength, "Max": model.MaxIdLength, "Regex": model.ValidIdRegex}, "", http.StatusBadRequest) return nil, "", model.NewAppError("extractPlugin", "app.plugin.invalid_id.app_error", map[string]any{"Min": model.MinIdLength, "Max": model.MaxIdLength, "Regex": model.ValidIdRegex}, "", http.StatusBadRequest)
} }
return manifest, extractDir, nil return manifest, extractDir, nil
} }
// installExtractedPlugin installs a plugin previously extracted to a temporary directory,
// activating the plugin automatically if already enabled by the server configuration.
//
// The given installation strategy decides how to handle upgrade scenarios.
func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
logger := ch.srv.Log().With(mlog.String("plugin_id", manifest.Id))
logger.Info("Installing extracted plugin", mlog.String("version", manifest.Version))
pluginsEnvironment := ch.GetPluginsEnvironment() pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil { if pluginsEnvironment == nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
@ -334,12 +373,6 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
// Check plugin id is not blocked
if plugin.PluginIDIsBlocked(manifest.Id) {
mlog.Debug("Skipping installation of plugin since plugin is on blocklist. Some plugins are blocked because they are built into this version of Mattermost.", mlog.String("plugin_id", manifest.Id))
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.blocked.app_error", map[string]any{"Id": manifest.Id}, "", http.StatusInternalServerError)
}
// Check for plugins installed with the same ID. // Check for plugins installed with the same ID.
var existingManifest *model.Manifest var existingManifest *model.Manifest
for _, bundle := range bundles { for _, bundle := range bundles {
@ -366,30 +399,30 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD
existingVersion, err = semver.Parse(existingManifest.Version) existingVersion, err = semver.Parse(existingManifest.Version)
if err != nil { if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusInternalServerError)
} }
if version.LTE(existingVersion) { if version.LTE(existingVersion) {
mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id)) logger.Warn("Skipping local installation of plugin since existing version is newer", mlog.String("version", version.String()), mlog.String("existing_version", existingVersion.String()))
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.skip_installation.app_error", map[string]any{"Id": manifest.Id}, "", http.StatusInternalServerError) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.skip_installation.app_error", map[string]any{"Id": manifest.Id}, "", http.StatusInternalServerError)
} }
} }
// Otherwise remove the existing installation prior to install below. // Otherwise remove the existing installation prior to installing below.
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version)) logger.Info("Removing existing installation of plugin before local install", mlog.String("existing_version", existingManifest.Version))
if err := ch.removePluginLocally(existingManifest.Id); err != nil { if err := ch.removePluginLocally(existingManifest.Id); err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusInternalServerError)
} }
} }
pluginPath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id) bundlePath := filepath.Join(*ch.cfgSvc.Config().PluginSettings.Directory, manifest.Id)
err = utils.CopyDir(fromPluginDir, pluginPath) err = utils.CopyDir(fromPluginDir, bundlePath)
if err != nil { if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
// Flag plugin locally as managed by the filestore. // Flag plugin locally as managed by the filestore.
f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName)) f, err := os.Create(filepath.Join(bundlePath, managedPluginFileName))
if err != nil { if err != nil {
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
@ -419,12 +452,13 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD
manifest = updatedManifest manifest = updatedManifest
} }
mlog.Debug("Installing plugin", mlog.String("plugin_id", manifest.Id), mlog.String("version", manifest.Version))
return manifest, nil return manifest, nil
} }
// RemovePlugin removes a plugin from all servers.
func (ch *Channels) RemovePlugin(id string) *model.AppError { func (ch *Channels) RemovePlugin(id string) *model.AppError {
logger := ch.srv.Log().With(mlog.String("plugin_id", id))
// Disable plugin before removal to make sure this // Disable plugin before removal to make sure this
// plugin remains disabled on re-install. // plugin remains disabled on re-install.
if err := ch.disablePlugin(id); err != nil { if err := ch.disablePlugin(id); err != nil {
@ -436,19 +470,19 @@ func (ch *Channels) RemovePlugin(id string) *model.AppError {
} }
// Remove bundle from the file store. // Remove bundle from the file store.
storePluginFileName := getBundleStorePath(id) bundlePath := getBundleStorePath(id)
bundleExist, err := ch.srv.fileExists(storePluginFileName) bundleExists, err := ch.srv.fileExists(bundlePath)
if err != nil { if err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
if !bundleExist { if !bundleExists {
return nil return nil
} }
if err = ch.srv.removeFile(storePluginFileName); err != nil { if err := ch.srv.removeFile(bundlePath); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
if err = ch.removeSignature(id); err != nil { if err := ch.removeSignature(id); err != nil {
mlog.Warn("Can't remove signature", mlog.Err(err)) logger.Warn("Can't remove signature", mlog.Err(err))
} }
ch.notifyClusterPluginEvent( ch.notifyClusterPluginEvent(
@ -459,12 +493,13 @@ func (ch *Channels) RemovePlugin(id string) *model.AppError {
) )
if err := ch.notifyPluginStatusesChanged(); err != nil { if err := ch.notifyPluginStatusesChanged(); err != nil {
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err)) logger.Warn("Failed to notify plugin status changed", mlog.Err(err))
} }
return nil return nil
} }
// removePluginLocally removes the given plugin from the current server.
func (ch *Channels) removePluginLocally(id string) *model.AppError { func (ch *Channels) removePluginLocally(id string) *model.AppError {
pluginsEnvironment := ch.GetPluginsEnvironment() pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil { if pluginsEnvironment == nil {
@ -477,11 +512,11 @@ func (ch *Channels) removePluginLocally(id string) *model.AppError {
} }
var manifest *model.Manifest var manifest *model.Manifest
var pluginPath string var unpackedBundlePath string
for _, p := range plugins { for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id { if p.Manifest != nil && p.Manifest.Id == id {
manifest = p.Manifest manifest = p.Manifest
pluginPath = filepath.Dir(p.ManifestPath) unpackedBundlePath = filepath.Dir(p.ManifestPath)
break break
} }
} }
@ -494,33 +529,39 @@ func (ch *Channels) removePluginLocally(id string) *model.AppError {
pluginsEnvironment.RemovePlugin(id) pluginsEnvironment.RemovePlugin(id)
ch.unregisterPluginCommands(id) ch.unregisterPluginCommands(id)
if err := os.RemoveAll(pluginPath); err != nil { if err := os.RemoveAll(unpackedBundlePath); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
return nil return nil
} }
// removeSignature removes the signature file installed alongside the plugin.
func (ch *Channels) removeSignature(pluginID string) *model.AppError { func (ch *Channels) removeSignature(pluginID string) *model.AppError {
filePath := getSignatureStorePath(pluginID) logger := ch.srv.Log().With(mlog.String("plugin_id", pluginID))
exists, err := ch.srv.fileExists(filePath)
signaturePath := getSignatureStorePath(pluginID)
exists, err := ch.srv.fileExists(signaturePath)
if err != nil { if err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
if !exists { if !exists {
mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginID)) logger.Debug("no plugin signature to remove")
return nil return nil
} }
if err = ch.srv.removeFile(filePath); err != nil { if err = ch.srv.removeFile(signaturePath); err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err) return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
return nil return nil
} }
// getBundleStorePath maps the given plugin id to the file path of the corresponding plugin bundle.
func getBundleStorePath(id string) string { func getBundleStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id)) return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
} }
// getSignatureStorePath maps the given plugin id to the file path of the corresponding plugin
// signature, if one exists.
func getSignatureStorePath(id string) string { func getSignatureStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz.sig", id)) return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz.sig", id))
} }

View File

@ -167,17 +167,6 @@ func TestInstallPluginLocally(t *testing.T) {
assertBundleInfoManifests(t, th, []*model.Manifest{manifest}) assertBundleInfoManifests(t, th, []*model.Manifest{manifest})
}) })
t.Run("doesn't install if ID on block list", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
cleanExistingBundles(t, th)
_, appErr := installPlugin(t, th, "com.mattermost.plugin-incident-response", "0.0.1", installPluginLocallyAlways)
require.NotNil(t, appErr)
require.Equal(t, "app.plugin.blocked.app_error", appErr.Id)
assertBundleInfoManifests(t, th, []*model.Manifest{})
})
t.Run("different plugin already installed", func(t *testing.T) { t.Run("different plugin already installed", func(t *testing.T) {
th := Setup(t) th := Setup(t)
defer th.TearDown() defer th.TearDown()

View File

@ -6023,10 +6023,6 @@
"id": "app.oauth.update_app.updating.app_error", "id": "app.oauth.update_app.updating.app_error",
"translation": "We encountered an error updating the app." "translation": "We encountered an error updating the app."
}, },
{
"id": "app.plugin.blocked.app_error",
"translation": "Plugin {{.Id}} is on the block list. Some plugins are blocked because they are built into this version of Mattermost."
},
{ {
"id": "app.plugin.cluster.save_config.app_error", "id": "app.plugin.cluster.save_config.app_error",
"translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled."

View File

@ -22,12 +22,6 @@ type FeatureFlags struct {
// AppsEnabled toggles the Apps framework functionalities both in server and client side // AppsEnabled toggles the Apps framework functionalities both in server and client side
AppsEnabled bool AppsEnabled bool
// Feature flags to control plugin versions
PluginPlaybooks string `plugin_id:"playbooks"`
PluginApps string `plugin_id:"com.mattermost.apps"`
PluginFocalboard string `plugin_id:"focalboard"`
PluginCalls string `plugin_id:"com.mattermost.calls"`
PermalinkPreviews bool PermalinkPreviews bool
// CallsEnabled controls whether or not the Calls plugin should be enabled // CallsEnabled controls whether or not the Calls plugin should be enabled
@ -64,8 +58,6 @@ func (f *FeatureFlags) SetDefaults() {
f.TestBoolFeature = false f.TestBoolFeature = false
f.EnableRemoteClusterService = false f.EnableRemoteClusterService = false
f.AppsEnabled = true f.AppsEnabled = true
f.PluginApps = ""
f.PluginFocalboard = ""
f.BoardsDataRetention = false f.BoardsDataRetention = false
f.NormalizeLdapDNs = false f.NormalizeLdapDNs = false
f.GraphQL = false f.GraphQL = false
@ -79,26 +71,6 @@ func (f *FeatureFlags) SetDefaults() {
f.DataRetentionConcurrencyEnabled = true f.DataRetentionConcurrencyEnabled = true
} }
func (f *FeatureFlags) Plugins() map[string]string {
rFFVal := reflect.ValueOf(f).Elem()
rFFType := reflect.TypeOf(f).Elem()
pluginVersions := make(map[string]string)
for i := 0; i < rFFVal.NumField(); i++ {
rFieldVal := rFFVal.Field(i)
rFieldType := rFFType.Field(i)
pluginId, hasPluginId := rFieldType.Tag.Lookup("plugin_id")
if !hasPluginId {
continue
}
pluginVersions[pluginId] = rFieldVal.String()
}
return pluginVersions
}
// ToMap returns the feature flags as a map[string]string // ToMap returns the feature flags as a map[string]string
// Supports boolean and string feature flags. // Supports boolean and string feature flags.
func (f *FeatureFlags) ToMap() map[string]string { func (f *FeatureFlags) ToMap() map[string]string {

View File

@ -103,34 +103,9 @@ func scanSearchPath(path string) ([]*model.BundleInfo, error) {
return ret, nil return ret, nil
} }
var pluginIDBlocklist = map[string]bool{
"com.mattermost.plugin-incident-response": true,
"com.mattermost.plugin-incident-management": true,
}
func PluginIDIsBlocked(id string) bool {
_, ok := pluginIDBlocklist[id]
return ok
}
// Returns a list of all plugins within the environment. // Returns a list of all plugins within the environment.
func (env *Environment) Available() ([]*model.BundleInfo, error) { func (env *Environment) Available() ([]*model.BundleInfo, error) {
rawList, err := scanSearchPath(env.pluginDir) return scanSearchPath(env.pluginDir)
if err != nil {
return nil, err
}
// Filter any plugins that match the blocklist
filteredList := make([]*model.BundleInfo, 0, len(rawList))
for _, bundleInfo := range rawList {
if PluginIDIsBlocked(bundleInfo.Manifest.Id) {
env.logger.Debug("Plugin ignored by blocklist", mlog.String("plugin_id", bundleInfo.Manifest.Id))
} else {
filteredList = append(filteredList, bundleInfo)
}
}
return filteredList, nil
} }
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder. // Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.